@regantis-sdk/react-native-chat 0.1.0

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/src/client.js ADDED
@@ -0,0 +1,630 @@
1
+ 'use strict';
2
+
3
+ const { Platform } = require('react-native');
4
+ const native = require('./native');
5
+ const { DEFAULT_SERVER_URL, DEFAULT_SETTINGS, SDK_VERSION } = require('./defaults');
6
+
7
+ function normalizeServerUrl(value) {
8
+ return String(value || DEFAULT_SERVER_URL).trim().replace(/\/+$/, '');
9
+ }
10
+
11
+ function normalizeLanguage(value) {
12
+ const code = String(value || 'en').trim().toLowerCase().replace('_', '-').split('-')[0];
13
+ return /^[a-z]{2,3}$/.test(code) ? code : 'en';
14
+ }
15
+
16
+ function mergeMessages(current, incoming) {
17
+ const map = new Map();
18
+ [...(current || []), ...(incoming || [])].forEach(message => {
19
+ const id = Number(message && message.chat_message_id) || 0;
20
+ if (id > 0) map.set(id, message);
21
+ });
22
+ return Array.from(map.values()).sort((a, b) => Number(a.chat_message_id) - Number(b.chat_message_id));
23
+ }
24
+
25
+ function isSupportMessage(message) {
26
+ const type = String(message && message.sender_type || '').toLowerCase();
27
+ return type === 'agent' || type === 'admin' || type === 'chatbot';
28
+ }
29
+
30
+ function timezone() {
31
+ try {
32
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || '';
33
+ } catch (_) {
34
+ return '';
35
+ }
36
+ }
37
+
38
+ function normalizeSettings(value) {
39
+ const settings = { ...DEFAULT_SETTINGS, ...(value || {}) };
40
+ [
41
+ 'use_profile_names',
42
+ 'chat_background_opacity',
43
+ 'desktop_side_spacing',
44
+ 'desktop_bottom_spacing',
45
+ 'mobile_enabled',
46
+ 'mobile_same_as_desktop',
47
+ 'mobile_side_spacing',
48
+ 'mobile_bottom_spacing',
49
+ 'show_logo',
50
+ 'show_agent_photo',
51
+ 'sound_enabled',
52
+ 'rating_enabled',
53
+ 'transcript_enabled',
54
+ 'before_you_go_enabled',
55
+ 'white_label',
56
+ ].forEach(key => {
57
+ if (settings[key] !== undefined && settings[key] !== null && settings[key] !== '') settings[key] = Number(settings[key]);
58
+ });
59
+ return settings;
60
+ }
61
+
62
+ class RegantisChatClient {
63
+ constructor(config) {
64
+ config = config || {};
65
+ if (!/^[a-f0-9]{64}$/i.test(String(config.apiKey || ''))) {
66
+ throw new Error('RegantisChat requires a valid apiKey');
67
+ }
68
+
69
+ this.options = {
70
+ apiKey: String(config.apiKey).toLowerCase(),
71
+ serverUrl: normalizeServerUrl(config.serverUrl),
72
+ language: normalizeLanguage(config.language),
73
+ translationLanguage: String(config.translationLanguage || ''),
74
+ screen: String(config.screen || 'chat'),
75
+ customer: config.customer || null,
76
+ theme: config.theme || null,
77
+ texts: config.texts || null,
78
+ onMessage: typeof config.onMessage === 'function' ? config.onMessage : null,
79
+ onUnreadChange: typeof config.onUnreadChange === 'function' ? config.onUnreadChange : null,
80
+ onError: typeof config.onError === 'function' ? config.onError : null,
81
+ };
82
+
83
+ this.listeners = new Set();
84
+ this.appInfo = { appId: '', appVersion: '', platform: Platform.OS };
85
+ this.ws = null;
86
+ this.wsReady = false;
87
+ this.wsReconnectTimer = null;
88
+ this.wsBackoff = 1000;
89
+ this.pollingTimer = null;
90
+ this.typingTimer = null;
91
+ this.destroyed = false;
92
+ this.syncing = false;
93
+ this.visible = true;
94
+ this.pageInstanceId = `${Platform.OS}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
95
+
96
+ this.state = {
97
+ loading: true,
98
+ initialized: false,
99
+ error: '',
100
+ banned: false,
101
+ config: null,
102
+ settings: { ...DEFAULT_SETTINGS },
103
+ texts: {},
104
+ languages: [],
105
+ translationCustomerEnabled: false,
106
+ visitorKey: '',
107
+ conversation: {
108
+ conversation_id: 0,
109
+ status: 1,
110
+ contact_name: '',
111
+ contact_email: '',
112
+ contact_confirmed: 0,
113
+ translation_language_code: '',
114
+ routing_mode: '',
115
+ feedback_submitted: false,
116
+ },
117
+ messages: [],
118
+ hasMore: false,
119
+ upload: null,
120
+ realtime: null,
121
+ realtimeConnected: false,
122
+ remoteTyping: '',
123
+ sending: false,
124
+ uploading: false,
125
+ unread: 0,
126
+ customerDataProvided: false,
127
+ };
128
+ }
129
+
130
+ subscribe(listener) {
131
+ this.listeners.add(listener);
132
+ listener(this.state);
133
+ return () => this.listeners.delete(listener);
134
+ }
135
+
136
+ getState() {
137
+ return this.state;
138
+ }
139
+
140
+ setState(patch) {
141
+ if (this.destroyed) return;
142
+ this.state = { ...this.state, ...patch };
143
+ this.listeners.forEach(listener => listener(this.state));
144
+ }
145
+
146
+ emitError(error) {
147
+ const message = error && error.message ? error.message : String(error || 'UNKNOWN_ERROR');
148
+ if (this.options.onError) this.options.onError(error);
149
+ return message;
150
+ }
151
+
152
+ storageKey() {
153
+ return `regantis_chat_visitor_${this.options.apiKey}`;
154
+ }
155
+
156
+ async start() {
157
+ this.destroyed = false;
158
+ this.setState({ loading: true, error: '' });
159
+
160
+ try {
161
+ this.appInfo = await native.getAppInfo();
162
+ await this.loadConfig();
163
+ const visitorKey = String(await native.getItem(this.storageKey()) || '').toLowerCase();
164
+ await this.initialize(visitorKey);
165
+ this.setState({ loading: false, initialized: true });
166
+ this.connectRealtime();
167
+ this.startPolling();
168
+ return this.state;
169
+ } catch (error) {
170
+ const message = this.emitError(error);
171
+ this.setState({ loading: false, error: message });
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ stop() {
177
+ this.destroyed = true;
178
+ this.disconnectRealtime();
179
+ this.stopPolling();
180
+ if (this.typingTimer) clearTimeout(this.typingTimer);
181
+ this.listeners.clear();
182
+ }
183
+
184
+ setVisible(visible) {
185
+ this.visible = !!visible;
186
+ if (this.visible) {
187
+ this.markRead().catch(() => {});
188
+ this.syncHistory().catch(() => {});
189
+ }
190
+ }
191
+
192
+ setSoundEnabled(enabled) {
193
+ this.setState({ settings: { ...this.state.settings, sound_enabled: enabled ? 1 : 0 } });
194
+ }
195
+
196
+ async loadConfig() {
197
+ const url = `${this.options.serverUrl}/chat/widget/sdkConfig?key=${encodeURIComponent(this.options.apiKey)}&lang=${encodeURIComponent(this.options.language)}`;
198
+ const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json' } });
199
+ const data = await this.readJson(response);
200
+ if (!response.ok || !data.success) throw new Error(data.error || `HTTP_${response.status}`);
201
+
202
+ const settings = normalizeSettings({ ...(data.settings || {}), ...(this.options.theme || {}) });
203
+ const texts = { ...(data.texts || {}), ...(this.options.texts || {}) };
204
+ this.setState({
205
+ config: data,
206
+ settings,
207
+ texts,
208
+ languages: Array.isArray(data.languages) ? data.languages : [],
209
+ translationCustomerEnabled: Number(data.translation_customer_enabled || 0) === 1,
210
+ });
211
+ return data;
212
+ }
213
+
214
+ basePayload(extra) {
215
+ const payload = {
216
+ sdk_key: this.options.apiKey,
217
+ sdk_platform: Platform.OS,
218
+ sdk_version: SDK_VERSION,
219
+ app_id: String(this.appInfo.appId || ''),
220
+ app_version: String(this.appInfo.appVersion || ''),
221
+ screen: this.options.screen,
222
+ browser_language: this.options.language,
223
+ timezone: timezone(),
224
+ visitor_key: this.state.visitorKey,
225
+ page_instance_id: this.pageInstanceId,
226
+ ...(extra || {}),
227
+ };
228
+
229
+ const customer = this.options.customer || {};
230
+ if (customer.id || customer.customerId) payload.customer_id = String(customer.id || customer.customerId || '');
231
+ if (customer.name) payload.customer_name = String(customer.name);
232
+ if (customer.email) payload.customer_email = String(customer.email);
233
+ if (customer.telephone || customer.phone) payload.customer_telephone = String(customer.telephone || customer.phone);
234
+ if (customer.identityTimestamp) payload.customer_identity_timestamp = String(customer.identityTimestamp);
235
+ if (customer.identitySignature) payload.customer_identity_signature = String(customer.identitySignature);
236
+ return payload;
237
+ }
238
+
239
+ async request(endpoint, extra) {
240
+ const api = this.state.config && this.state.config.api;
241
+ const url = api && api[endpoint];
242
+ if (!url) throw new Error(`INVALID_ENDPOINT_${endpoint}`);
243
+
244
+ const body = new FormData();
245
+ const payload = this.basePayload(extra);
246
+ Object.keys(payload).forEach(key => {
247
+ const value = payload[key];
248
+ if (value !== undefined && value !== null) body.append(key, String(value));
249
+ });
250
+
251
+ const response = await fetch(url, { method: 'POST', body, headers: { Accept: 'application/json' } });
252
+ const data = await this.readJson(response);
253
+ if (!response.ok || !data.success) {
254
+ const error = new Error(data.error || `HTTP_${response.status}`);
255
+ error.code = data.error || '';
256
+ error.status = response.status;
257
+ if (error.code === 'BANNED') this.setState({ banned: true });
258
+ throw error;
259
+ }
260
+ return data;
261
+ }
262
+
263
+ async readJson(response) {
264
+ const text = await response.text();
265
+ try {
266
+ return text ? JSON.parse(text) : {};
267
+ } catch (_) {
268
+ throw new Error(`INVALID_SERVER_RESPONSE_${response.status}`);
269
+ }
270
+ }
271
+
272
+ async initialize(visitorKey) {
273
+ this.state.visitorKey = /^[a-f0-9]{64}$/.test(visitorKey || '') ? visitorKey : '';
274
+ let data = await this.request('initialize');
275
+
276
+ if (data.reset_visitor_key) {
277
+ await native.removeItem(this.storageKey());
278
+ this.state.visitorKey = '';
279
+ data = await this.request('initialize');
280
+ }
281
+
282
+ if (data.visitor_key) {
283
+ await native.setItem(this.storageKey(), data.visitor_key);
284
+ }
285
+
286
+ const messages = Array.isArray(data.messages) ? data.messages : [];
287
+ const conversation = data.conversation || this.state.conversation;
288
+ const patch = {
289
+ visitorKey: String(data.visitor_key || this.state.visitorKey || ''),
290
+ conversation,
291
+ messages,
292
+ hasMore: !!data.has_more,
293
+ upload: data.upload || null,
294
+ realtime: data.realtime || null,
295
+ customerDataProvided: !!data.customer_data_provided,
296
+ banned: false,
297
+ };
298
+ this.setState(patch);
299
+
300
+ if (!conversation.translation_language_code && this.options.translationLanguage && this.state.translationCustomerEnabled && Number(conversation.conversation_id || 0) > 0) {
301
+ await this.setTranslationLanguage(this.options.translationLanguage).catch(() => {});
302
+ }
303
+ }
304
+
305
+ async syncHistory() {
306
+ if (this.syncing || !this.state.visitorKey || Number(this.state.conversation.conversation_id || 0) < 1) return;
307
+ this.syncing = true;
308
+ try {
309
+ const last = this.state.messages.length ? this.state.messages[this.state.messages.length - 1] : null;
310
+ const afterId = Number(last && last.chat_message_id) || 0;
311
+ const data = await this.request('history', afterId ? { after_id: afterId } : {});
312
+ const incoming = Array.isArray(data.messages) ? data.messages : [];
313
+ const previousIds = new Set(this.state.messages.map(item => Number(item.chat_message_id)));
314
+ const newSupport = incoming.filter(item => !previousIds.has(Number(item.chat_message_id)) && isSupportMessage(item));
315
+ const messages = mergeMessages(this.state.messages, incoming);
316
+ const conversation = data.conversation || this.state.conversation;
317
+ let unread = this.state.unread;
318
+
319
+ if (newSupport.length) {
320
+ newSupport.forEach(message => this.options.onMessage && this.options.onMessage(message));
321
+ if (Number(this.state.settings.sound_enabled || 0) === 1) native.playIncomingSound();
322
+ if (!this.visible) unread += newSupport.length;
323
+ }
324
+
325
+ this.setState({ messages, conversation, hasMore: !!data.has_more, unread, customerDataProvided: !!data.customer_data_provided });
326
+ this.notifyUnread();
327
+ if (this.visible && newSupport.length) await this.markRead().catch(() => {});
328
+ } catch (error) {
329
+ if (error.code === 'CONVERSATION_NOT_FOUND') return;
330
+ this.emitError(error);
331
+ } finally {
332
+ this.syncing = false;
333
+ }
334
+ }
335
+
336
+ async loadOlder() {
337
+ if (!this.state.hasMore || !this.state.messages.length) return;
338
+ const firstId = Number(this.state.messages[0].chat_message_id) || 0;
339
+ const data = await this.request('history', { before_id: firstId });
340
+ this.setState({
341
+ messages: mergeMessages(data.messages || [], this.state.messages),
342
+ hasMore: !!data.has_more,
343
+ conversation: data.conversation || this.state.conversation,
344
+ });
345
+ }
346
+
347
+ async saveContact(name, email) {
348
+ const languageCode = this.state.conversation.translation_language_code || this.options.translationLanguage || '';
349
+ const data = await this.request('contact', { name, email, language_code: languageCode });
350
+ const messages = data.chatbot_message ? mergeMessages(this.state.messages, [data.chatbot_message]) : this.state.messages;
351
+ this.setState({
352
+ conversation: data.conversation || this.state.conversation,
353
+ customerDataProvided: !!data.customer_data_provided,
354
+ upload: data.upload || this.state.upload,
355
+ messages,
356
+ });
357
+ return data;
358
+ }
359
+
360
+ async sendMessage(message) {
361
+ message = String(message || '').trim();
362
+ if (!message || message.length > 5000 || this.state.sending) return null;
363
+ this.setState({ sending: true });
364
+ try {
365
+ const conversation = this.state.conversation || {};
366
+ const data = await this.request('send', {
367
+ message,
368
+ contact_name: conversation.contact_name || '',
369
+ contact_email: conversation.contact_email || '',
370
+ language_code: conversation.translation_language_code || '',
371
+ });
372
+ let messages = mergeMessages(this.state.messages, [data.welcome_message, data.message].filter(Boolean));
373
+ this.setState({
374
+ messages,
375
+ conversation: data.conversation || conversation,
376
+ upload: data.upload || this.state.upload,
377
+ realtime: data.realtime || this.state.realtime,
378
+ sending: false,
379
+ });
380
+ this.connectRealtime();
381
+ this.startPolling();
382
+ if (data.message && this.wsReady) this.sendRealtime({ type: 'message', roomId: this.roomId(), conversationId: this.conversationId(), messageId: Number(data.message.chat_message_id) });
383
+ if (Number(data.chatbot_enabled || 0) === 1 && data.message) this.requestChatbot(data.message.chat_message_id);
384
+ return data.message;
385
+ } catch (error) {
386
+ this.setState({ sending: false });
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ async requestChatbot(messageId) {
392
+ this.setState({ remoteTyping: 'chatbot' });
393
+ try {
394
+ const data = await this.request('chatbot', { trigger_message_id: Number(messageId) });
395
+ if (data.message) {
396
+ const already = this.state.messages.some(item => Number(item.chat_message_id) === Number(data.message.chat_message_id));
397
+ if (!already) {
398
+ this.setState({ messages: mergeMessages(this.state.messages, [data.message]) });
399
+ if (this.options.onMessage) this.options.onMessage(data.message);
400
+ if (Number(this.state.settings.sound_enabled || 0) === 1) native.playIncomingSound();
401
+ }
402
+ }
403
+ if (data.handoff && data.handoff.routing_mode) {
404
+ this.setState({ conversation: { ...this.state.conversation, routing_mode: data.handoff.routing_mode } });
405
+ }
406
+ } catch (error) {
407
+ this.emitError(error);
408
+ } finally {
409
+ this.setState({ remoteTyping: '' });
410
+ this.syncHistory().catch(() => {});
411
+ }
412
+ }
413
+
414
+ async uploadAttachment(file, kind) {
415
+ if (!file || !file.uri || !this.state.upload || !this.state.upload.enabled || !this.state.upload.token) {
416
+ throw new Error('UPLOAD_UNAVAILABLE');
417
+ }
418
+ this.setState({ uploading: true });
419
+ try {
420
+ const body = new FormData();
421
+ body.append('upload_token', String(this.state.upload.token));
422
+ body.append('upload_type', kind === 'image' ? 'image' : 'file');
423
+ body.append('attachment', {
424
+ uri: file.uri,
425
+ name: file.name || (kind === 'image' ? 'image.jpg' : 'attachment'),
426
+ type: file.type || (kind === 'image' ? 'image/jpeg' : 'application/octet-stream'),
427
+ });
428
+ const response = await fetch(this.state.upload.url, { method: 'POST', body, headers: { Accept: 'application/json' } });
429
+ const data = await this.readJson(response);
430
+ if (!response.ok || !data.success) throw new Error(data.error || `HTTP_${response.status}`);
431
+ this.setState({ messages: mergeMessages(this.state.messages, data.message ? [data.message] : []), uploading: false });
432
+ if (data.message && this.wsReady) this.sendRealtime({ type: 'message', roomId: this.roomId(), conversationId: this.conversationId(), messageId: Number(data.message.chat_message_id) });
433
+ return data.message;
434
+ } catch (error) {
435
+ this.setState({ uploading: false });
436
+ throw error;
437
+ }
438
+ }
439
+
440
+ async pickAndUpload(kind) {
441
+ const type = kind === 'image' ? 'image' : 'file';
442
+ const file = kind === 'screenshot' ? await native.captureScreenshot() : await native.pickAttachment(type);
443
+ if (!file) return null;
444
+ return this.uploadAttachment(file, kind === 'screenshot' ? 'image' : type);
445
+ }
446
+
447
+ async setTranslationLanguage(languageCode) {
448
+ if (!this.state.translationCustomerEnabled || this.conversationId() < 1) return;
449
+ const data = await this.request('language', { language_code: String(languageCode || '') });
450
+ this.setState({ conversation: data.conversation || this.state.conversation, messages: Array.isArray(data.messages) ? data.messages : this.state.messages });
451
+ await this.reloadHistory();
452
+ }
453
+
454
+ async reloadHistory() {
455
+ if (this.conversationId() < 1) return;
456
+ const data = await this.request('history');
457
+ this.setState({ messages: data.messages || [], conversation: data.conversation || this.state.conversation, hasMore: !!data.has_more });
458
+ }
459
+
460
+ async closeChat() {
461
+ if (this.conversationId() < 1) return;
462
+ const data = await this.request('end');
463
+ this.setState({ conversation: data.conversation || this.state.conversation, upload: null });
464
+ this.disconnectRealtime();
465
+ return data;
466
+ }
467
+
468
+ async reopenChat() {
469
+ const c = this.state.conversation || {};
470
+ const data = await this.request('reopen', { name: c.contact_name || '', email: c.contact_email || '' });
471
+ this.setState({
472
+ conversation: data.conversation || c,
473
+ customerDataProvided: !!data.customer_data_provided,
474
+ upload: data.upload || null,
475
+ messages: data.chatbot_message ? mergeMessages(this.state.messages, [data.chatbot_message]) : this.state.messages,
476
+ });
477
+ await this.initialize(this.state.visitorKey);
478
+ this.connectRealtime();
479
+ return data;
480
+ }
481
+
482
+ async rate(rating) {
483
+ const data = await this.request('feedback', { action: 'rate', rating: Number(rating) === 1 ? '1' : '0' });
484
+ this.setState({ conversation: data.conversation || this.state.conversation, messages: data.message ? mergeMessages(this.state.messages, [data.message]) : this.state.messages });
485
+ return data;
486
+ }
487
+
488
+ async rateComment(comment) {
489
+ const data = await this.request('feedback', { action: 'comment', comment: String(comment || '') });
490
+ this.setState({ conversation: data.conversation || this.state.conversation, messages: data.message ? mergeMessages(this.state.messages, [data.message]) : this.state.messages });
491
+ return data;
492
+ }
493
+
494
+ sendTranscript(email) {
495
+ return this.request('transcript', { email: String(email || '') });
496
+ }
497
+
498
+ async markRead() {
499
+ if (this.conversationId() < 1) return;
500
+ const data = await this.request('read');
501
+ if (this.wsReady) this.sendRealtime({ type: 'read', roomId: this.roomId(), conversationId: this.conversationId(), messageId: Number(data.message_id || 0) });
502
+ if (this.state.unread !== 0) {
503
+ this.setState({ unread: 0 });
504
+ this.notifyUnread();
505
+ }
506
+ }
507
+
508
+ notifyUnread() {
509
+ if (this.options.onUnreadChange) this.options.onUnreadChange(this.state.unread);
510
+ }
511
+
512
+ conversationId() {
513
+ return Number(this.state.conversation && this.state.conversation.conversation_id) || 0;
514
+ }
515
+
516
+ roomId() {
517
+ return this.state.realtime && this.state.realtime.room_id || '';
518
+ }
519
+
520
+ connectRealtime() {
521
+ const realtime = this.state.realtime;
522
+ if (this.destroyed || Number(this.state.conversation.status || 0) !== 1 || !realtime || !realtime.enabled || !realtime.url || !realtime.token || this.conversationId() < 1) return;
523
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) return;
524
+
525
+ const separator = String(realtime.url).includes('?') ? '&' : '?';
526
+ try {
527
+ this.ws = new WebSocket(`${realtime.url}${separator}token=${encodeURIComponent(realtime.token)}`);
528
+ } catch (_) {
529
+ this.scheduleReconnect();
530
+ return;
531
+ }
532
+
533
+ this.ws.onopen = () => {
534
+ this.wsReady = true;
535
+ this.wsBackoff = 1000;
536
+ this.setState({ realtimeConnected: true });
537
+ this.sendRealtime({ type: 'join', roomId: this.roomId() });
538
+ this.startPolling();
539
+ };
540
+
541
+ this.ws.onmessage = event => {
542
+ let payload;
543
+ try { payload = JSON.parse(event.data); } catch (_) { return; }
544
+ this.handleRealtime(payload);
545
+ };
546
+
547
+ this.ws.onclose = () => {
548
+ this.wsReady = false;
549
+ this.setState({ realtimeConnected: false });
550
+ this.startPolling();
551
+ this.scheduleReconnect();
552
+ };
553
+
554
+ this.ws.onerror = () => {};
555
+ }
556
+
557
+ disconnectRealtime() {
558
+ if (this.wsReconnectTimer) clearTimeout(this.wsReconnectTimer);
559
+ this.wsReconnectTimer = null;
560
+ this.wsReady = false;
561
+ if (this.ws) {
562
+ try { this.ws.close(1000, 'SDK closed'); } catch (_) {}
563
+ }
564
+ this.ws = null;
565
+ if (!this.destroyed) this.setState({ realtimeConnected: false });
566
+ }
567
+
568
+ scheduleReconnect() {
569
+ if (this.destroyed || Number(this.state.conversation.status || 0) !== 1 || this.wsReconnectTimer || this.conversationId() < 1) return;
570
+ const delay = Math.min(this.wsBackoff, 20000);
571
+ this.wsBackoff = Math.min(this.wsBackoff * 2, 20000);
572
+ this.wsReconnectTimer = setTimeout(() => {
573
+ this.wsReconnectTimer = null;
574
+ this.connectRealtime();
575
+ }, delay);
576
+ }
577
+
578
+ sendRealtime(payload) {
579
+ if (!this.wsReady || !this.ws || this.ws.readyState !== 1) return false;
580
+ try {
581
+ this.ws.send(JSON.stringify(payload));
582
+ return true;
583
+ } catch (_) {
584
+ return false;
585
+ }
586
+ }
587
+
588
+ sendTyping(text) {
589
+ const value = String(text || '');
590
+ if (this.conversationId() < 1) return;
591
+ this.sendRealtime({ type: 'typing', roomId: this.roomId(), conversationId: this.conversationId(), isTyping: value.trim().length > 0, draftText: value.slice(0, 5000) });
592
+ if (this.typingTimer) clearTimeout(this.typingTimer);
593
+ if (value.trim()) {
594
+ this.typingTimer = setTimeout(() => this.sendRealtime({ type: 'typing', roomId: this.roomId(), conversationId: this.conversationId(), isTyping: false, draftText: '' }), 1800);
595
+ }
596
+ }
597
+
598
+ handleRealtime(payload) {
599
+ if (!payload || typeof payload !== 'object') return;
600
+ if (payload.type === 'message' || payload.type === 'conversation') {
601
+ this.syncHistory().catch(() => {});
602
+ return;
603
+ }
604
+ if (payload.type === 'typing') {
605
+ const role = String(payload.senderRole || payload.senderType || '').toLowerCase();
606
+ if (role === 'agent' || role === 'admin' || role === 'chatbot') {
607
+ this.setState({ remoteTyping: payload.isTyping ? role : '' });
608
+ if (payload.isTyping && role !== 'chatbot') {
609
+ setTimeout(() => {
610
+ if (this.state.remoteTyping === role) this.setState({ remoteTyping: '' });
611
+ }, 4000);
612
+ }
613
+ }
614
+ }
615
+ }
616
+
617
+ startPolling() {
618
+ this.stopPolling();
619
+ if (this.destroyed || Number(this.state.conversation.status || 0) !== 1 || this.conversationId() < 1) return;
620
+ const delay = this.wsReady ? 60000 : 8000;
621
+ this.pollingTimer = setInterval(() => this.syncHistory().catch(() => {}), delay);
622
+ }
623
+
624
+ stopPolling() {
625
+ if (this.pollingTimer) clearInterval(this.pollingTimer);
626
+ this.pollingTimer = null;
627
+ }
628
+ }
629
+
630
+ module.exports = { RegantisChatClient };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ DEFAULT_SERVER_URL: 'https://www.crm.regantis.technology',
5
+ SDK_VERSION: '0.1.0',
6
+ DEFAULT_SETTINGS: {
7
+ operator_name: 'Operator',
8
+ use_profile_names: 0,
9
+ minimized_type: 'bubble',
10
+ theme_mode: 'light',
11
+ color_mode: 'theme',
12
+ theme_color: '#0B63E5',
13
+ launcher_background: '#0B63E5',
14
+ launcher_icon_color: '#FFFFFF',
15
+ chat_background: '#F5F7FB',
16
+ chat_background_opacity: 100,
17
+ primary_color: '#0B63E5',
18
+ customer_bubble: '#0B63E5',
19
+ customer_text: '#FFFFFF',
20
+ agent_bubble: '#FFFFFF',
21
+ agent_text: '#17181B',
22
+ system_text: '#6F7480',
23
+ desktop_align: 'right',
24
+ desktop_side_spacing: 20,
25
+ desktop_bottom_spacing: 20,
26
+ desktop_visibility: 'always',
27
+ mobile_enabled: 1,
28
+ mobile_same_as_desktop: 1,
29
+ mobile_minimized_type: 'bubble',
30
+ mobile_align: 'right',
31
+ mobile_side_spacing: 0,
32
+ mobile_bottom_spacing: 0,
33
+ mobile_visibility: 'always',
34
+ show_logo: 1,
35
+ logo_url: '',
36
+ show_agent_photo: 1,
37
+ sound_enabled: 1,
38
+ rating_enabled: 1,
39
+ transcript_enabled: 1,
40
+ before_you_go_enabled: 0,
41
+ white_label: 0,
42
+ },
43
+ };