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/LICENSE +21 -0
- package/README.md +132 -2
- package/lib/api.d.ts +43 -0
- package/lib/api.js +114 -0
- package/lib/client.d.ts +120 -0
- package/lib/client.js +505 -0
- package/lib/errors.d.ts +8 -0
- package/lib/errors.js +17 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +29 -0
- package/lib/react.d.ts +33 -0
- package/lib/react.js +72 -0
- package/lib/realtime.d.ts +51 -0
- package/lib/realtime.js +124 -0
- package/lib/storage.d.ts +48 -0
- package/lib/storage.js +63 -0
- package/lib/types.d.ts +203 -0
- package/lib/types.js +6 -0
- package/lib/ui/BotFace.d.ts +13 -0
- package/lib/ui/BotFace.js +103 -0
- package/lib/ui/OpenDeskChat.d.ts +27 -0
- package/lib/ui/OpenDeskChat.js +636 -0
- package/lib/ui/OpenDeskLauncher.d.ts +12 -0
- package/lib/ui/OpenDeskLauncher.js +46 -0
- package/lib/ui/icons.d.ts +23 -0
- package/lib/ui/icons.js +95 -0
- package/lib/ui/strings.d.ts +53 -0
- package/lib/ui/strings.js +58 -0
- package/lib/ui/theme.d.ts +51 -0
- package/lib/ui/theme.js +89 -0
- package/package.json +43 -4
- package/src/api.ts +172 -0
- package/src/client.ts +659 -0
- package/src/errors.ts +16 -0
- package/src/index.ts +25 -0
- package/src/react.tsx +109 -0
- package/src/realtime.ts +166 -0
- package/src/storage.ts +93 -0
- package/src/types.ts +200 -0
- package/src/ui/BotFace.tsx +163 -0
- package/src/ui/OpenDeskChat.tsx +1318 -0
- package/src/ui/OpenDeskLauncher.tsx +69 -0
- package/src/ui/icons.tsx +162 -0
- package/src/ui/strings.ts +57 -0
- package/src/ui/theme.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 opendesk-react-native contributors
|
|
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
|
@@ -1,3 +1,133 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opendesk-react-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React Native SDK for [openDesk](https://opendesk.sh) live chat. It uses the same visitor API and realtime gateway as openDesk's web widget (`/widget/v1.js`), with no WebView:
|
|
4
|
+
|
|
5
|
+
- `OpenDesk`: a headless client for the session, conversations, messages, attachments, AI streaming, handoff and unread counts
|
|
6
|
+
- `OpenDeskProvider` and hooks: `useConversations`, `useConversation`, `useUnreadCount` and `useOpenDeskState`
|
|
7
|
+
- `OpenDeskChat` and `OpenDeskLauncher`: drop-in UI that mirrors the web widget. It reads the brand colour, radius, theme, bot, team and copy from your widget's settings, and is built only from React Native core components.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install opendesk-react-native expo-secure-store
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Set up
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import * as SecureStore from 'expo-secure-store';
|
|
19
|
+
import { OpenDesk, OpenDeskProvider, secureStoreStorage } from 'opendesk-react-native';
|
|
20
|
+
|
|
21
|
+
export const support = new OpenDesk({
|
|
22
|
+
baseUrl: 'https://desk.example.com', // your openDesk origin (https://app.opendesk.sh on cloud)
|
|
23
|
+
widgetKey: 'wk_…', // Settings → Brands and widgets → Chat widgets
|
|
24
|
+
storage: secureStoreStorage(SecureStore), // keeps the visitor's conversations across launches
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function App() {
|
|
28
|
+
const identity = useSupportIdentity(); // see below
|
|
29
|
+
return (
|
|
30
|
+
<OpenDeskProvider client={support} identity={identity}>
|
|
31
|
+
<Navigation />
|
|
32
|
+
</OpenDeskProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Where the visitor token is kept
|
|
38
|
+
|
|
39
|
+
The SDK stores one value: the visitor token (`od_vis_…`). It is a bearer credential, so anyone holding it can read that visitor's conversations and post as them. Keep it in the iOS Keychain or Android Keystore with one of the two adapters. Each adapter takes the native module as an argument, so the SDK itself depends on neither:
|
|
40
|
+
|
|
41
|
+
| Your app uses | Install | Pass as `storage` |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Expo | `npx expo install expo-secure-store` | `secureStoreStorage(SecureStore)` (`import * as SecureStore from 'expo-secure-store'`) |
|
|
44
|
+
| Bare React Native | `npm install react-native-keychain`, then `pod install` | `keychainStorage(Keychain)` (`import * as Keychain from 'react-native-keychain'`) |
|
|
45
|
+
|
|
46
|
+
Both adapters store the token as *after first unlock, this device only*. It's readable in the background after the first unlock, and it's left out of iCloud and device-transfer backups.
|
|
47
|
+
|
|
48
|
+
Leave `storage` out and the token lives in memory only, so every launch starts a new visitor. Any `{ getItem, setItem, removeItem }` object also works. AsyncStorage fits that shape, but it writes plain text, so don't use it for this.
|
|
49
|
+
|
|
50
|
+
## Identify the signed-in user
|
|
51
|
+
|
|
52
|
+
To attach chats to a known contact, pass `{ user_id, user_hash, email, name }` as `identity`. `user_hash` is `hex(HMAC-SHA256(widget_secret, user_id))`, as described in [Identity verification](https://docs.opendesk.sh/guides/live-chat/identity-verification/). Compute it on your server, never in the app, and serve it from an authenticated endpoint of your own:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
function useSupportIdentity() {
|
|
56
|
+
const { user } = useAuth();
|
|
57
|
+
const [identity, setIdentity] = useState<Identity | null>(null);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!user) return setIdentity(null);
|
|
61
|
+
// Your backend returns { user_id, user_hash, email, name } for the signed-in user.
|
|
62
|
+
fetchMySupportIdentity().then(setIdentity).catch(() => {});
|
|
63
|
+
}, [user?.id]);
|
|
64
|
+
|
|
65
|
+
return identity;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When the user signs out, pass `null` (or call `support.shutdown()`) so the next person on the device never sees their conversations. If a different `user_id` signs in, the SDK drops the previous visitor by itself. Leave `identity` undefined for anonymous chat.
|
|
70
|
+
|
|
71
|
+
## Show the chat
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import * as DocumentPicker from 'expo-document-picker';
|
|
75
|
+
import { OpenDeskChat, OpenDeskLauncher } from 'opendesk-react-native';
|
|
76
|
+
|
|
77
|
+
<OpenDeskChat
|
|
78
|
+
onClose={() => navigation.goBack()}
|
|
79
|
+
keyboardVerticalOffset={headerHeight}
|
|
80
|
+
pickAttachment={async () => {
|
|
81
|
+
const result = await DocumentPicker.getDocumentAsync({ type: ['image/*', 'application/pdf'] });
|
|
82
|
+
const file = result.assets?.[0];
|
|
83
|
+
return file ? { uri: file.uri, name: file.name, type: file.mimeType ?? 'application/octet-stream', size: file.size } : null;
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<OpenDeskLauncher onPress={() => navigation.navigate('Support')} style={{ position: 'absolute', right: 20, bottom: 20 }} />
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`OpenDeskChat` also takes these props:
|
|
91
|
+
|
|
92
|
+
- `theme`: `'light' | 'dark' | 'auto'`, which overrides the widget setting
|
|
93
|
+
- `accentColor`
|
|
94
|
+
- `strings`: replaces any of the built-in English copy (`defaultStrings` lists every key)
|
|
95
|
+
- `initialConversationId`
|
|
96
|
+
|
|
97
|
+
For an in-app banner while the chat is closed, use `support.onMessage(({ conversationId, from, message }) => …)`.
|
|
98
|
+
|
|
99
|
+
## Build your own UI
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
const { conversations } = useConversations();
|
|
103
|
+
const { messages, aiReplies, typing } = useConversation(conversationId); // marks it read while mounted
|
|
104
|
+
const client = useOpenDesk();
|
|
105
|
+
|
|
106
|
+
await client.startConversation({ body, topic_id });
|
|
107
|
+
await client.sendMessage(conversationId, { body, attachment_ids });
|
|
108
|
+
const attachment = await client.upload({ uri, name, type }); // or a File/Blob on the web
|
|
109
|
+
client.typing(conversationId);
|
|
110
|
+
await client.requestHuman(conversationId); // after an AI answer
|
|
111
|
+
await client.decideAction(conversationId, actionId, 'confirm');
|
|
112
|
+
await client.rateMessage(conversationId, messageId, 'up');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Render messages with `message.client_id ?? message.id` as the key. That keeps a bubble mounted when its optimistic copy or streamed AI answer is replaced by the stored message.
|
|
116
|
+
|
|
117
|
+
## Preview
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run example
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This runs `OpenDeskChat` in the browser on port 8774, through react-native-web, against a mock openDesk (`example/mockDesk.ts`). The mock covers AI streaming, tool checks, an AI action to confirm, agent typing, handoff, attachments and unread badges, and needs no server.
|
|
124
|
+
|
|
125
|
+
## Notes
|
|
126
|
+
|
|
127
|
+
- Realtime uses Phoenix Channels (the `phoenix` package) on `<gateway_url>` and the topic `contact:<wid>:<contact_id>`. The 5-minute `ws_token` is refreshed before every reconnect, and missed messages are re-fetched over REST after a drop.
|
|
128
|
+
- If **Allowed sites** is set on the widget, openDesk checks the `Origin` header. Native requests send none, so check that your app can still reach the API.
|
|
129
|
+
- The widget API has no push notifications for replies sent while the app is closed. Use openDesk webhooks (`message.created`) to your backend for those.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Attachment, Conversation, Identity, Message, SendMessageInput, StartConversationInput, UploadFile, WidgetConfig, WidgetSession } from './types';
|
|
2
|
+
export type Fetcher = typeof fetch;
|
|
3
|
+
export interface OpenDeskApiOptions {
|
|
4
|
+
/** The openDesk origin, e.g. `https://desk.example.com`. `/v1` is added. */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** The chat widget's public key (`wk_…`). */
|
|
7
|
+
widgetKey: string;
|
|
8
|
+
fetcher?: Fetcher;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thin REST client for openDesk's visitor endpoints. Holds the visitor token once a
|
|
12
|
+
* session exists and sends it as `Authorization: Visitor <token>`.
|
|
13
|
+
*/
|
|
14
|
+
export declare class OpenDeskApi {
|
|
15
|
+
readonly baseUrl: string;
|
|
16
|
+
readonly widgetKey: string;
|
|
17
|
+
visitorToken: string | null;
|
|
18
|
+
private readonly fetcher;
|
|
19
|
+
constructor(options: OpenDeskApiOptions);
|
|
20
|
+
config(): Promise<WidgetConfig>;
|
|
21
|
+
/** Starts a session, or resumes the current one when a visitor token is held. */
|
|
22
|
+
session(identity?: Identity): Promise<WidgetSession>;
|
|
23
|
+
endSession(): Promise<void>;
|
|
24
|
+
conversations(): Promise<Conversation[]>;
|
|
25
|
+
startConversation(input: StartConversationInput): Promise<{
|
|
26
|
+
conversation: Conversation;
|
|
27
|
+
message: Message;
|
|
28
|
+
}>;
|
|
29
|
+
messages(conversationId: string): Promise<Message[]>;
|
|
30
|
+
sendMessage(conversationId: string, input: SendMessageInput): Promise<Message>;
|
|
31
|
+
markSeen(conversationId: string): Promise<void>;
|
|
32
|
+
/** Asks for a person after an AI answer. */
|
|
33
|
+
handoff(conversationId: string): Promise<void>;
|
|
34
|
+
/** Confirms or declines an action the AI proposed. */
|
|
35
|
+
decideAction(conversationId: string, actionId: string, decision: 'confirm' | 'decline'): Promise<void>;
|
|
36
|
+
rateMessage(conversationId: string, messageId: string, rating: 'up' | 'down'): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Uploads one file as multipart `file`. React Native's FormData takes `{ uri, name, type }`;
|
|
39
|
+
* a browser needs a real Blob, so web Files are passed through as they are.
|
|
40
|
+
*/
|
|
41
|
+
upload(file: UploadFile): Promise<Attachment>;
|
|
42
|
+
private request;
|
|
43
|
+
}
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenDeskApi = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
/**
|
|
6
|
+
* Thin REST client for openDesk's visitor endpoints. Holds the visitor token once a
|
|
7
|
+
* session exists and sends it as `Authorization: Visitor <token>`.
|
|
8
|
+
*/
|
|
9
|
+
class OpenDeskApi {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.visitorToken = null;
|
|
12
|
+
this.baseUrl = options.baseUrl.replace(/\/widget\/v1\.js$/, '').replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
13
|
+
this.widgetKey = options.widgetKey;
|
|
14
|
+
this.fetcher = options.fetcher ?? ((input, init) => fetch(input, init));
|
|
15
|
+
}
|
|
16
|
+
config() {
|
|
17
|
+
return this.request('GET', `/v1/widget/config?key=${encodeURIComponent(this.widgetKey)}`, undefined, false);
|
|
18
|
+
}
|
|
19
|
+
/** Starts a session, or resumes the current one when a visitor token is held. */
|
|
20
|
+
async session(identity = {}) {
|
|
21
|
+
const session = await this.request('POST', '/v1/widget/session', { key: this.widgetKey, ...(this.visitorToken ? { visitor_token: this.visitorToken } : {}), ...identity }, false);
|
|
22
|
+
this.visitorToken = session.visitor_token;
|
|
23
|
+
return session;
|
|
24
|
+
}
|
|
25
|
+
async endSession() {
|
|
26
|
+
if (this.visitorToken) {
|
|
27
|
+
await this.request('DELETE', '/v1/widget/session');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async conversations() {
|
|
31
|
+
const response = await this.request('GET', '/v1/widget/conversations');
|
|
32
|
+
return response.data ?? [];
|
|
33
|
+
}
|
|
34
|
+
startConversation(input) {
|
|
35
|
+
return this.request('POST', '/v1/widget/conversations', input);
|
|
36
|
+
}
|
|
37
|
+
async messages(conversationId) {
|
|
38
|
+
const response = await this.request('GET', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/messages`);
|
|
39
|
+
return response.data ?? [];
|
|
40
|
+
}
|
|
41
|
+
sendMessage(conversationId, input) {
|
|
42
|
+
return this.request('POST', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/messages`, input);
|
|
43
|
+
}
|
|
44
|
+
async markSeen(conversationId) {
|
|
45
|
+
await this.request('POST', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/seen`, {});
|
|
46
|
+
}
|
|
47
|
+
/** Asks for a person after an AI answer. */
|
|
48
|
+
async handoff(conversationId) {
|
|
49
|
+
await this.request('POST', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/handoff`, {});
|
|
50
|
+
}
|
|
51
|
+
/** Confirms or declines an action the AI proposed. */
|
|
52
|
+
async decideAction(conversationId, actionId, decision) {
|
|
53
|
+
await this.request('POST', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/actions/${encodeURIComponent(actionId)}`, { decision });
|
|
54
|
+
}
|
|
55
|
+
async rateMessage(conversationId, messageId, rating) {
|
|
56
|
+
await this.request('POST', `/v1/widget/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/rating`, { rating });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Uploads one file as multipart `file`. React Native's FormData takes `{ uri, name, type }`;
|
|
60
|
+
* a browser needs a real Blob, so web Files are passed through as they are.
|
|
61
|
+
*/
|
|
62
|
+
upload(file) {
|
|
63
|
+
const form = new FormData();
|
|
64
|
+
if (isBlob(file)) {
|
|
65
|
+
form.append('file', file, 'name' in file && typeof file.name === 'string' ? file.name : 'upload');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
form.append('file', { uri: file.uri, name: file.name, type: file.type || 'application/octet-stream' });
|
|
69
|
+
}
|
|
70
|
+
return this.request('POST', '/v1/widget/attachments', form);
|
|
71
|
+
}
|
|
72
|
+
async request(method, path, body, authenticated = true) {
|
|
73
|
+
const headers = { Accept: 'application/json' };
|
|
74
|
+
let payload;
|
|
75
|
+
if (body instanceof FormData) {
|
|
76
|
+
payload = body;
|
|
77
|
+
}
|
|
78
|
+
else if (body !== undefined) {
|
|
79
|
+
headers['Content-Type'] = 'application/json';
|
|
80
|
+
payload = JSON.stringify(body);
|
|
81
|
+
}
|
|
82
|
+
if (authenticated && this.visitorToken) {
|
|
83
|
+
headers.Authorization = `Visitor ${this.visitorToken}`;
|
|
84
|
+
}
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await this.fetcher(this.baseUrl + path, { method, headers, body: payload });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new errors_1.OpenDeskError(0, 'network_error', "Can't reach the server");
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
let code = 'error';
|
|
94
|
+
let message = `Request failed (${response.status})`;
|
|
95
|
+
try {
|
|
96
|
+
const problem = (await response.json());
|
|
97
|
+
code = problem.code ?? code;
|
|
98
|
+
message = problem.detail || problem.title || message;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Not a problem+json body.
|
|
102
|
+
}
|
|
103
|
+
throw new errors_1.OpenDeskError(response.status, code, message);
|
|
104
|
+
}
|
|
105
|
+
if (response.status === 204 || !(response.headers.get('content-type') ?? '').includes('json')) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return (await response.json());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.OpenDeskApi = OpenDeskApi;
|
|
112
|
+
function isBlob(file) {
|
|
113
|
+
return typeof Blob !== 'undefined' && file instanceof Blob;
|
|
114
|
+
}
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { OpenDeskApi, type Fetcher } from './api';
|
|
2
|
+
import { type SocketFactory } from './realtime';
|
|
3
|
+
import { type TokenStorage } from './storage';
|
|
4
|
+
import type { Attachment, BotRef, Conversation, Identity, Message, SendMessageInput, StartConversationInput, UploadFile, WidgetConfig, WidgetSession } from './types';
|
|
5
|
+
export interface OpenDeskOptions {
|
|
6
|
+
/** The openDesk origin, e.g. `https://desk.example.com`. */
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
/** The chat widget's public key (`wk_…`). */
|
|
9
|
+
widgetKey: string;
|
|
10
|
+
/** Persists the visitor token so conversations survive restarts. Defaults to memory. */
|
|
11
|
+
storage?: TokenStorage;
|
|
12
|
+
fetcher?: Fetcher;
|
|
13
|
+
socketFactory?: SocketFactory;
|
|
14
|
+
}
|
|
15
|
+
/** An AI answer being streamed before it lands as a message. */
|
|
16
|
+
export interface AIReply {
|
|
17
|
+
id: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
text: string;
|
|
20
|
+
phase: 'typing' | 'streaming' | 'done';
|
|
21
|
+
tools: {
|
|
22
|
+
name: string;
|
|
23
|
+
status: 'running' | 'done' | 'error';
|
|
24
|
+
}[];
|
|
25
|
+
bot: BotRef | null;
|
|
26
|
+
messageId: string | null;
|
|
27
|
+
handoff: boolean;
|
|
28
|
+
actionId: string | null;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}
|
|
31
|
+
export interface OpenDeskState {
|
|
32
|
+
status: 'idle' | 'booting' | 'ready' | 'error';
|
|
33
|
+
error: string | null;
|
|
34
|
+
config: WidgetConfig | null;
|
|
35
|
+
session: WidgetSession | null;
|
|
36
|
+
connected: boolean;
|
|
37
|
+
conversations: Conversation[];
|
|
38
|
+
messages: Record<string, Message[]>;
|
|
39
|
+
/** Conversation id → name of the agent currently typing. */
|
|
40
|
+
typing: Record<string, string>;
|
|
41
|
+
aiReplies: Record<string, AIReply>;
|
|
42
|
+
/** Conversation id → agent replies received while that conversation was not open. */
|
|
43
|
+
unread: Record<string, number>;
|
|
44
|
+
activeConversationId: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface IncomingMessage {
|
|
47
|
+
conversationId: string;
|
|
48
|
+
message: Message;
|
|
49
|
+
from: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Headless openDesk chat client: owns the visitor session, the realtime socket and the
|
|
53
|
+
* conversation state. Subscribe with `subscribe` (or the React hooks) to render it.
|
|
54
|
+
*/
|
|
55
|
+
export declare class OpenDesk {
|
|
56
|
+
readonly api: OpenDeskApi;
|
|
57
|
+
private state;
|
|
58
|
+
private identity;
|
|
59
|
+
private realtime;
|
|
60
|
+
private bootPromise;
|
|
61
|
+
private readonly storage;
|
|
62
|
+
private readonly storageKey;
|
|
63
|
+
private readonly socketFactory;
|
|
64
|
+
private readonly subscribers;
|
|
65
|
+
private readonly messageListeners;
|
|
66
|
+
private readonly typingTimers;
|
|
67
|
+
/** Files uploaded this session, so a message can show its attachments before the server answers. */
|
|
68
|
+
private readonly uploads;
|
|
69
|
+
constructor(options: OpenDeskOptions);
|
|
70
|
+
getState: () => OpenDeskState;
|
|
71
|
+
subscribe: (listener: () => void) => (() => void);
|
|
72
|
+
/** Called for every agent or AI reply, e.g. to show an in-app banner. */
|
|
73
|
+
onMessage(listener: (incoming: IncomingMessage) => void): () => void;
|
|
74
|
+
get unreadCount(): number;
|
|
75
|
+
/** Loads the widget config and starts (or resumes) the visitor session. Safe to call repeatedly. */
|
|
76
|
+
boot(): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Attaches the signed-in user. A different `user_id` from the one before drops the stored
|
|
79
|
+
* visitor token, so one user never resumes another's conversations on a shared device.
|
|
80
|
+
*/
|
|
81
|
+
identify(identity: Identity): Promise<void>;
|
|
82
|
+
/** Ends the session and forgets the visitor. Call it when the user signs out. */
|
|
83
|
+
shutdown(): Promise<void>;
|
|
84
|
+
loadConversations(): Promise<Conversation[]>;
|
|
85
|
+
/** Marks a conversation as on screen: loads its messages and clears its unread count. */
|
|
86
|
+
openConversation(conversationId: string): Promise<Message[]>;
|
|
87
|
+
closeConversation(): void;
|
|
88
|
+
refreshMessages(conversationId: string): Promise<Message[]>;
|
|
89
|
+
startConversation(input: StartConversationInput): Promise<Conversation>;
|
|
90
|
+
/** Sends a reply, showing it at once and swapping in the server copy when it returns. */
|
|
91
|
+
sendMessage(conversationId: string, input: SendMessageInput): Promise<Message>;
|
|
92
|
+
upload(file: UploadFile): Promise<Attachment>;
|
|
93
|
+
typing(conversationId: string): void;
|
|
94
|
+
requestHuman(conversationId: string): Promise<void>;
|
|
95
|
+
decideAction(conversationId: string, actionId: string, decision: 'confirm' | 'decline'): Promise<void>;
|
|
96
|
+
rateMessage(conversationId: string, messageId: string, rating: 'up' | 'down'): Promise<void>;
|
|
97
|
+
private runBoot;
|
|
98
|
+
private startSession;
|
|
99
|
+
private fetchConversations;
|
|
100
|
+
/** Resumes the stored session; a revoked or expired token falls back to a new one. */
|
|
101
|
+
private openSession;
|
|
102
|
+
/** Runs a visitor request, re-opening the session once if the token was revoked. */
|
|
103
|
+
private authorized;
|
|
104
|
+
private catchUp;
|
|
105
|
+
private onRealtimeEvent;
|
|
106
|
+
private onLiveEvent;
|
|
107
|
+
private onAgentTyping;
|
|
108
|
+
private clearTyping;
|
|
109
|
+
/**
|
|
110
|
+
* Merges messages by id in time order; `replace` makes the list exactly the server's copy.
|
|
111
|
+
* A stored message takes over the render key of the optimistic copy (`replacesLocalId`, or
|
|
112
|
+
* a pending copy with the same text) or of the AI stream it finishes, so its row never remounts.
|
|
113
|
+
*/
|
|
114
|
+
private applyMessages;
|
|
115
|
+
private touchConversation;
|
|
116
|
+
private resetConversations;
|
|
117
|
+
private forgetToken;
|
|
118
|
+
private set;
|
|
119
|
+
private emitChange;
|
|
120
|
+
}
|