ajaxter-chat 3.0.13 → 3.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/dist/components/ChatWidget.js +91 -22
- package/dist/components/PermissionsGateScreen/index.d.ts +8 -0
- package/dist/components/PermissionsGateScreen/index.js +48 -0
- package/dist/index.d.ts +1 -1
- package/dist/types/index.d.ts +22 -0
- package/dist/utils/widgetPermissions.d.ts +9 -0
- package/dist/utils/widgetPermissions.js +67 -0
- package/package.json +1 -1
- package/public/chatData.json +4 -2
- package/src/components/ChatWidget.tsx +89 -16
- package/src/components/PermissionsGateScreen/index.tsx +105 -0
- package/src/index.ts +1 -1
- package/src/types/index.ts +23 -0
- package/src/utils/widgetPermissions.ts +67 -0
package/README.md
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import { loadLocalConfig } from '../config';
|
|
5
5
|
import { mergeTheme } from '../utils/theme';
|
|
6
6
|
import { useRemoteConfig } from '../hooks/useRemoteConfig';
|
|
@@ -20,8 +20,10 @@ import { CallScreen } from './CallScreen';
|
|
|
20
20
|
import { MaintenanceView } from './MaintenanceView';
|
|
21
21
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
22
22
|
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
import { PermissionsGateScreen } from './PermissionsGateScreen';
|
|
24
|
+
import { hasStoredPermissionsGrant } from '../utils/widgetPermissions';
|
|
25
|
+
export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
26
|
+
var _a, _b, _c, _d, _e;
|
|
25
27
|
/* SSR guard */
|
|
26
28
|
const [mounted, setMounted] = useState(false);
|
|
27
29
|
useEffect(() => { setMounted(true); }, []);
|
|
@@ -43,18 +45,22 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
43
45
|
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
44
46
|
/** Stagger list animation only when opening from home burger menu */
|
|
45
47
|
const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
|
|
48
|
+
/** Microphone, geolocation, and screen capture granted for this tab */
|
|
49
|
+
const [permissionsOk, setPermissionsOk] = useState(false);
|
|
46
50
|
/* App state */
|
|
47
51
|
const [tickets, setTickets] = useState((_a = data === null || data === void 0 ? void 0 : data.sampleTickets) !== null && _a !== void 0 ? _a : []);
|
|
48
52
|
const [recentChats, setRecentChats] = useState([]);
|
|
49
53
|
const [blockedUids, setBlockedUids] = useState((_b = data === null || data === void 0 ? void 0 : data.blockedUsers) !== null && _b !== void 0 ? _b : []);
|
|
50
54
|
/* Sync remote data into local state once loaded */
|
|
51
55
|
useEffect(() => {
|
|
52
|
-
var _a, _b;
|
|
56
|
+
var _a, _b, _c, _d;
|
|
53
57
|
if (data) {
|
|
54
58
|
setTickets(data.sampleTickets);
|
|
55
59
|
setBlockedUids(data.blockedUsers);
|
|
56
|
-
|
|
57
|
-
const
|
|
60
|
+
const pid = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim();
|
|
61
|
+
const devs = (_b = data.developers) !== null && _b !== void 0 ? _b : [];
|
|
62
|
+
const usr = pid ? ((_c = data.users) !== null && _c !== void 0 ? _c : []).filter(u => u.project === pid) : ((_d = data.users) !== null && _d !== void 0 ? _d : []);
|
|
63
|
+
const all = [...devs, ...usr];
|
|
58
64
|
const recents = Object.entries(data.sampleChats).map(([uid, msgs]) => {
|
|
59
65
|
const user = all.find(u => u.uid === uid);
|
|
60
66
|
if (!user || msgs.length === 0)
|
|
@@ -71,7 +77,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
71
77
|
}).filter(Boolean);
|
|
72
78
|
setRecentChats(recents);
|
|
73
79
|
}
|
|
74
|
-
}, [data]);
|
|
80
|
+
}, [data, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
|
|
75
81
|
/* Chat hook */
|
|
76
82
|
const { messages, activeUser, isPaused, isReported, selectUser, sendMessage, togglePause, reportChat, clearChat, setMessages, } = useChat();
|
|
77
83
|
/* WebRTC hook */
|
|
@@ -104,14 +110,27 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
104
110
|
setClosing(false);
|
|
105
111
|
}, 300);
|
|
106
112
|
}, [persistWidgetState]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
var _a;
|
|
115
|
+
const id = (_a = data === null || data === void 0 ? void 0 : data.widget) === null || _a === void 0 ? void 0 : _a.id;
|
|
116
|
+
if (!id)
|
|
117
|
+
return;
|
|
118
|
+
setPermissionsOk(hasStoredPermissionsGrant(id));
|
|
119
|
+
}, [(_c = data === null || data === void 0 ? void 0 : data.widget) === null || _c === void 0 ? void 0 : _c.id]);
|
|
107
120
|
const restoredRef = useRef(false);
|
|
108
121
|
useEffect(() => {
|
|
109
|
-
var _a, _b, _c;
|
|
122
|
+
var _a, _b, _c, _d, _e, _f;
|
|
110
123
|
if (!(data === null || data === void 0 ? void 0 : data.widget) || restoredRef.current)
|
|
111
124
|
return;
|
|
112
125
|
const w = data.widget;
|
|
113
126
|
setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
|
|
114
|
-
|
|
127
|
+
const uidForBlock = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : w.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
|
|
128
|
+
let viewerIsBlocked = w.viewerBlocked === true;
|
|
129
|
+
if (!viewerIsBlocked && uidForBlock) {
|
|
130
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uidForBlock);
|
|
131
|
+
viewerIsBlocked = (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
|
|
132
|
+
}
|
|
133
|
+
if (viewerIsBlocked) {
|
|
115
134
|
clearChat();
|
|
116
135
|
setScreen('home');
|
|
117
136
|
setActiveTab('home');
|
|
@@ -124,29 +143,42 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
124
143
|
setScreen(p.screen);
|
|
125
144
|
setActiveTab(p.activeTab);
|
|
126
145
|
setUserListCtx(p.userListCtx);
|
|
127
|
-
setViewingTicketId((
|
|
128
|
-
setChatReturnCtx((
|
|
146
|
+
setViewingTicketId((_c = p.viewingTicketId) !== null && _c !== void 0 ? _c : null);
|
|
147
|
+
setChatReturnCtx((_d = p.chatReturnCtx) !== null && _d !== void 0 ? _d : 'conversation');
|
|
129
148
|
if (p.activeUserUid) {
|
|
130
|
-
const
|
|
149
|
+
const pid = (_e = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _e === void 0 ? void 0 : _e.trim();
|
|
150
|
+
const pool = pid
|
|
151
|
+
? [...data.developers, ...data.users].filter(u => u.project === pid)
|
|
152
|
+
: [...data.developers, ...data.users];
|
|
153
|
+
const u = pool.find(x => x.uid === p.activeUserUid);
|
|
131
154
|
if (u) {
|
|
132
155
|
const hist = Array.isArray(p.messages) && p.messages.length
|
|
133
156
|
? p.messages
|
|
134
|
-
: ((
|
|
157
|
+
: ((_f = data.sampleChats[u.uid]) !== null && _f !== void 0 ? _f : []);
|
|
135
158
|
selectUser(u, hist);
|
|
136
159
|
}
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
restoredRef.current = true;
|
|
140
|
-
}, [data, selectUser, clearChat]);
|
|
163
|
+
}, [data, selectUser, clearChat, viewer === null || viewer === void 0 ? void 0 : viewer.projectId, viewer === null || viewer === void 0 ? void 0 : viewer.uid]);
|
|
141
164
|
useEffect(() => {
|
|
142
|
-
var _a;
|
|
143
|
-
if (!(
|
|
165
|
+
var _a, _b;
|
|
166
|
+
if (!(data === null || data === void 0 ? void 0 : data.widget))
|
|
167
|
+
return;
|
|
168
|
+
const w = data.widget;
|
|
169
|
+
const uid = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : w.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
|
|
170
|
+
let blocked = w.viewerBlocked === true;
|
|
171
|
+
if (!blocked && uid) {
|
|
172
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
|
|
173
|
+
blocked = (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
|
|
174
|
+
}
|
|
175
|
+
if (!blocked)
|
|
144
176
|
return;
|
|
145
177
|
clearChat();
|
|
146
178
|
setScreen('home');
|
|
147
179
|
setActiveTab('home');
|
|
148
180
|
setViewingTicketId(null);
|
|
149
|
-
}, [
|
|
181
|
+
}, [data === null || data === void 0 ? void 0 : data.widget, data === null || data === void 0 ? void 0 : data.developers, data === null || data === void 0 ? void 0 : data.users, viewer === null || viewer === void 0 ? void 0 : viewer.uid, clearChat]);
|
|
150
182
|
useEffect(() => {
|
|
151
183
|
if (!(data === null || data === void 0 ? void 0 : data.widget))
|
|
152
184
|
return;
|
|
@@ -287,9 +319,45 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
287
319
|
}, [endCall]);
|
|
288
320
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
289
321
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
290
|
-
const widgetConfig =
|
|
322
|
+
const widgetConfig = useMemo(() => {
|
|
323
|
+
var _a;
|
|
324
|
+
if (!(data === null || data === void 0 ? void 0 : data.widget))
|
|
325
|
+
return undefined;
|
|
326
|
+
const w = Object.assign({}, data.widget);
|
|
327
|
+
if (viewer) {
|
|
328
|
+
w.viewerUid = viewer.uid;
|
|
329
|
+
w.viewerName = viewer.name;
|
|
330
|
+
w.viewerType = viewer.type;
|
|
331
|
+
if ((_a = viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim())
|
|
332
|
+
w.viewerProjectId = viewer.projectId.trim();
|
|
333
|
+
}
|
|
334
|
+
return w;
|
|
335
|
+
}, [data === null || data === void 0 ? void 0 : data.widget, viewer]);
|
|
291
336
|
const primaryColor = theme.primaryColor;
|
|
292
|
-
|
|
337
|
+
/** All developers are listed; only end-`user` rows are filtered by `viewer.projectId`. */
|
|
338
|
+
const allUsers = useMemo(() => {
|
|
339
|
+
var _a, _b;
|
|
340
|
+
if (!data)
|
|
341
|
+
return [];
|
|
342
|
+
const pid = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim();
|
|
343
|
+
const devs = (_b = data.developers) !== null && _b !== void 0 ? _b : [];
|
|
344
|
+
if (!pid)
|
|
345
|
+
return [...devs, ...data.users];
|
|
346
|
+
const usersInProject = data.users.filter(u => u.project === pid);
|
|
347
|
+
return [...devs, ...usersInProject];
|
|
348
|
+
}, [data, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
|
|
349
|
+
const effectiveViewerBlocked = useMemo(() => {
|
|
350
|
+
var _a, _b;
|
|
351
|
+
if (!widgetConfig)
|
|
352
|
+
return false;
|
|
353
|
+
if (widgetConfig.viewerBlocked === true)
|
|
354
|
+
return true;
|
|
355
|
+
const uid = (_b = ((_a = viewer === null || viewer === void 0 ? void 0 : viewer.uid) !== null && _a !== void 0 ? _a : widgetConfig.viewerUid)) === null || _b === void 0 ? void 0 : _b.trim();
|
|
356
|
+
if (!uid || !data)
|
|
357
|
+
return false;
|
|
358
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
|
|
359
|
+
return (rec === null || rec === void 0 ? void 0 : rec.viewerBlocked) === true;
|
|
360
|
+
}, [widgetConfig, viewer === null || viewer === void 0 ? void 0 : viewer.uid, data]);
|
|
293
361
|
const viewerIsDev = (widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerType) === 'developer';
|
|
294
362
|
const viewerUid = widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerUid;
|
|
295
363
|
const filteredUsers = screen === 'user-list'
|
|
@@ -305,7 +373,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
305
373
|
return u.type === 'user';
|
|
306
374
|
})
|
|
307
375
|
: [];
|
|
308
|
-
const otherDevelopers = (
|
|
376
|
+
const otherDevelopers = useMemo(() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid), [allUsers, viewerUid]);
|
|
309
377
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
310
378
|
const handleTransferToDeveloper = useCallback((dev) => {
|
|
311
379
|
var _a;
|
|
@@ -396,7 +464,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
396
464
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
397
465
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
398
466
|
zIndex: 20, display: 'flex', gap: 6,
|
|
399
|
-
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' &&
|
|
467
|
+
}, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
|
|
400
468
|
setListEntranceAnimation(false);
|
|
401
469
|
setViewingTicketId(id);
|
|
402
470
|
setScreen('ticket-detail');
|
|
@@ -404,7 +472,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
404
472
|
const t = tickets.find(x => x.id === viewingTicketId);
|
|
405
473
|
return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
|
|
406
474
|
})()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
407
|
-
!
|
|
475
|
+
!effectiveViewerBlocked &&
|
|
476
|
+
permissionsOk &&
|
|
408
477
|
screen !== 'chat' &&
|
|
409
478
|
screen !== 'call' &&
|
|
410
479
|
screen !== 'user-list' &&
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { requestWidgetPermissions, storePermissionsGrant } from '../../utils/widgetPermissions';
|
|
5
|
+
const DENIED = 'You cannot use this widget due to less permission granted by you.';
|
|
6
|
+
export const PermissionsGateScreen = ({ primaryColor, widgetId, onGranted, }) => {
|
|
7
|
+
const [phase, setPhase] = useState('prompt');
|
|
8
|
+
const handleAllow = async () => {
|
|
9
|
+
setPhase('checking');
|
|
10
|
+
const ok = await requestWidgetPermissions();
|
|
11
|
+
if (ok) {
|
|
12
|
+
storePermissionsGrant(widgetId);
|
|
13
|
+
onGranted();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
setPhase('denied');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return (_jsx("div", { style: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
display: 'flex',
|
|
22
|
+
flexDirection: 'column',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
padding: '28px 22px',
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
minHeight: 0,
|
|
28
|
+
}, children: phase === 'denied' ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: 44, marginBottom: 16 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { margin: '0 0 20px', fontSize: 15, fontWeight: 600, color: '#1e293b', lineHeight: 1.55, maxWidth: 320 }, children: DENIED }), _jsx("p", { style: { margin: '0 0 22px', fontSize: 13, color: '#64748b', lineHeight: 1.5, maxWidth: 340 }, children: "Allow microphone, location, and screen sharing in your browser settings for this site, then try again." }), _jsx("button", { type: "button", onClick: () => { setPhase('prompt'); void handleAllow(); }, style: {
|
|
29
|
+
padding: '12px 22px',
|
|
30
|
+
borderRadius: 12,
|
|
31
|
+
border: 'none',
|
|
32
|
+
background: primaryColor,
|
|
33
|
+
color: '#fff',
|
|
34
|
+
fontWeight: 700,
|
|
35
|
+
fontSize: 14,
|
|
36
|
+
cursor: 'pointer',
|
|
37
|
+
}, children: "Try again" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: 44, marginBottom: 16 }, children: "\uD83C\uDF99\uFE0F" }), _jsx("p", { style: { margin: '0 0 10px', fontSize: 16, fontWeight: 700, color: '#0f172a' }, children: "Permissions required" }), _jsxs("p", { style: { margin: '0 0 8px', fontSize: 14, color: '#475569', lineHeight: 1.55, maxWidth: 340 }, children: ["This widget needs ", _jsx("strong", { children: "microphone" }), " (voice & calls), ", _jsx("strong", { children: "location" }), ", and", ' ', _jsx("strong", { children: "screen sharing" }), " to work."] }), _jsx("p", { style: { margin: '0 0 22px', fontSize: 12, color: '#94a3b8', lineHeight: 1.45, maxWidth: 360 }, children: "You will be asked to pick a screen once \u2014 you can stop sharing immediately after; we only verify access." }), _jsx("button", { type: "button", disabled: phase === 'checking', onClick: handleAllow, style: {
|
|
38
|
+
padding: '14px 28px',
|
|
39
|
+
borderRadius: 12,
|
|
40
|
+
border: 'none',
|
|
41
|
+
background: phase === 'checking' ? '#94a3b8' : primaryColor,
|
|
42
|
+
color: '#fff',
|
|
43
|
+
fontWeight: 700,
|
|
44
|
+
fontSize: 15,
|
|
45
|
+
cursor: phase === 'checking' ? 'default' : 'pointer',
|
|
46
|
+
minWidth: 200,
|
|
47
|
+
}, children: phase === 'checking' ? 'Checking…' : 'Allow & continue' })] })) }));
|
|
48
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -19,4 +19,4 @@ export type { ReenableRequestPayload } from './utils/reenableRequest';
|
|
|
19
19
|
export { loadLocalConfig, fetchRemoteChatData } from './config';
|
|
20
20
|
export { mergeTheme, darken } from './utils/theme';
|
|
21
21
|
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
|
|
22
|
-
export type { ChatWidgetProps, ChatWidgetTheme, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, } from './types';
|
|
22
|
+
export type { ChatWidgetProps, ChatWidgetTheme, ChatWidgetViewer, WidgetConfig, RemoteChatData, ChatUser, ChatMessage, Ticket, RecentChat, CallSession, CallState, ChatStatus, ChatType, UserType, OnlineStatus, Screen, BottomTab, UserListContext, MessageType, LocalEnvConfig, } from './types';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface WidgetConfig {
|
|
|
23
23
|
viewerUid?: string;
|
|
24
24
|
/** Display name for transfer notes (optional) */
|
|
25
25
|
viewerName?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Host app project scope (set when embedding passes `viewer.projectId`).
|
|
28
|
+
* Use for API calls; lists can be filtered to users in the same project.
|
|
29
|
+
*/
|
|
30
|
+
viewerProjectId?: string;
|
|
26
31
|
/** Privacy Policy URL (linked from chat consent banner) */
|
|
27
32
|
privacyPolicyUrl?: string;
|
|
28
33
|
/** Set false to hide the consent note above the composer */
|
|
@@ -80,6 +85,11 @@ export interface ChatUser {
|
|
|
80
85
|
avatar: string | null;
|
|
81
86
|
status: OnlineStatus;
|
|
82
87
|
designation: string;
|
|
88
|
+
/**
|
|
89
|
+
* When `true` for the row matching the current viewer (`viewerUid` / `viewer.uid`),
|
|
90
|
+
* the widget shows the spam/blocked screen (same as `widget.viewerBlocked`).
|
|
91
|
+
*/
|
|
92
|
+
viewerBlocked?: boolean;
|
|
83
93
|
}
|
|
84
94
|
export interface ChatMessage {
|
|
85
95
|
id: string;
|
|
@@ -138,6 +148,18 @@ export interface ChatWidgetTheme {
|
|
|
138
148
|
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
139
149
|
borderRadius?: string;
|
|
140
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Pass the logged-in user from your React app so the widget matches identity and UI (user vs developer).
|
|
153
|
+
* Overrides `viewerUid`, `viewerName`, `viewerType` from remote `chatData.json` when provided.
|
|
154
|
+
*/
|
|
155
|
+
export interface ChatWidgetViewer {
|
|
156
|
+
uid: string;
|
|
157
|
+
name: string;
|
|
158
|
+
type: UserType;
|
|
159
|
+
/** When set, directory lists only include users whose `ChatUser.project` equals this string (exact match). */
|
|
160
|
+
projectId?: string;
|
|
161
|
+
}
|
|
141
162
|
export interface ChatWidgetProps {
|
|
142
163
|
theme?: ChatWidgetTheme;
|
|
164
|
+
viewer?: ChatWidgetViewer;
|
|
143
165
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests microphone, geolocation, and screen-capture access required by the widget.
|
|
3
|
+
* Stops all tracks immediately after success (probe only).
|
|
4
|
+
*/
|
|
5
|
+
export declare function requestWidgetPermissions(): Promise<boolean>;
|
|
6
|
+
export declare function permissionsSessionKey(widgetId: string): string;
|
|
7
|
+
export declare function hasStoredPermissionsGrant(widgetId: string): boolean;
|
|
8
|
+
export declare function storePermissionsGrant(widgetId: string): void;
|
|
9
|
+
export declare function clearStoredPermissionsGrant(widgetId: string): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests microphone, geolocation, and screen-capture access required by the widget.
|
|
3
|
+
* Stops all tracks immediately after success (probe only).
|
|
4
|
+
*/
|
|
5
|
+
export async function requestWidgetPermissions() {
|
|
6
|
+
var _a, _b, _c;
|
|
7
|
+
if (typeof navigator === 'undefined')
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
if (!((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia))
|
|
11
|
+
return false;
|
|
12
|
+
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
13
|
+
mic.getTracks().forEach(t => t.stop());
|
|
14
|
+
}
|
|
15
|
+
catch (_d) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
if (!((_b = navigator.geolocation) === null || _b === void 0 ? void 0 : _b.getCurrentPosition))
|
|
20
|
+
return false;
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
navigator.geolocation.getCurrentPosition(() => resolve(), e => reject(e), { enableHighAccuracy: false, timeout: 20000, maximumAge: 60000 });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (_e) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
if (!((_c = navigator.mediaDevices) === null || _c === void 0 ? void 0 : _c.getDisplayMedia))
|
|
30
|
+
return false;
|
|
31
|
+
const screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
32
|
+
screen.getTracks().forEach(t => t.stop());
|
|
33
|
+
}
|
|
34
|
+
catch (_f) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export function permissionsSessionKey(widgetId) {
|
|
40
|
+
return `ajaxter_widget_permissions_ok_${widgetId}`;
|
|
41
|
+
}
|
|
42
|
+
export function hasStoredPermissionsGrant(widgetId) {
|
|
43
|
+
if (typeof sessionStorage === 'undefined')
|
|
44
|
+
return false;
|
|
45
|
+
try {
|
|
46
|
+
return sessionStorage.getItem(permissionsSessionKey(widgetId)) === '1';
|
|
47
|
+
}
|
|
48
|
+
catch (_a) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function storePermissionsGrant(widgetId) {
|
|
53
|
+
try {
|
|
54
|
+
sessionStorage.setItem(permissionsSessionKey(widgetId), '1');
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
/* quota */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function clearStoredPermissionsGrant(widgetId) {
|
|
61
|
+
try {
|
|
62
|
+
sessionStorage.removeItem(permissionsSessionKey(widgetId));
|
|
63
|
+
}
|
|
64
|
+
catch (_a) {
|
|
65
|
+
/* */
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajaxter-chat",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.15",
|
|
4
4
|
"description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/public/chatData.json
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"type": "user",
|
|
63
63
|
"avatar": null,
|
|
64
64
|
"status": "online",
|
|
65
|
-
"designation": "Product Manager"
|
|
65
|
+
"designation": "Product Manager",
|
|
66
|
+
"viewerBlocked": false
|
|
66
67
|
},
|
|
67
68
|
{
|
|
68
69
|
"uid": "usr_002",
|
|
@@ -73,7 +74,8 @@
|
|
|
73
74
|
"type": "user",
|
|
74
75
|
"avatar": null,
|
|
75
76
|
"status": "away",
|
|
76
|
-
"designation": "Business Analyst"
|
|
77
|
+
"designation": "Business Analyst",
|
|
78
|
+
"viewerBlocked": true
|
|
77
79
|
},
|
|
78
80
|
{
|
|
79
81
|
"uid": "usr_003",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
|
|
5
5
|
import { loadLocalConfig } from '../config';
|
|
6
6
|
import { mergeTheme } from '../utils/theme';
|
|
@@ -22,8 +22,10 @@ import { CallScreen } from './CallScreen';
|
|
|
22
22
|
import { MaintenanceView } from './MaintenanceView';
|
|
23
23
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
24
24
|
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
25
|
+
import { PermissionsGateScreen } from './PermissionsGateScreen';
|
|
26
|
+
import { hasStoredPermissionsGrant } from '../utils/widgetPermissions';
|
|
25
27
|
|
|
26
|
-
export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) => {
|
|
28
|
+
export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewer }) => {
|
|
27
29
|
/* SSR guard */
|
|
28
30
|
const [mounted, setMounted] = useState(false);
|
|
29
31
|
useEffect(() => { setMounted(true); }, []);
|
|
@@ -53,6 +55,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
53
55
|
const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
|
|
54
56
|
/** Stagger list animation only when opening from home burger menu */
|
|
55
57
|
const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
|
|
58
|
+
/** Microphone, geolocation, and screen capture granted for this tab */
|
|
59
|
+
const [permissionsOk, setPermissionsOk] = useState(false);
|
|
56
60
|
|
|
57
61
|
/* App state */
|
|
58
62
|
const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
|
|
@@ -64,8 +68,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
64
68
|
if (data) {
|
|
65
69
|
setTickets(data.sampleTickets);
|
|
66
70
|
setBlockedUids(data.blockedUsers);
|
|
67
|
-
|
|
68
|
-
const
|
|
71
|
+
const pid = viewer?.projectId?.trim();
|
|
72
|
+
const devs = data.developers ?? [];
|
|
73
|
+
const usr = pid ? (data.users ?? []).filter(u => u.project === pid) : (data.users ?? []);
|
|
74
|
+
const all = [...devs, ...usr];
|
|
69
75
|
const recents: RecentChat[] = Object.entries(data.sampleChats).map(([uid, msgs]) => {
|
|
70
76
|
const user = all.find(u => u.uid === uid);
|
|
71
77
|
if (!user || msgs.length === 0) return null;
|
|
@@ -81,7 +87,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
81
87
|
}).filter(Boolean) as RecentChat[];
|
|
82
88
|
setRecentChats(recents);
|
|
83
89
|
}
|
|
84
|
-
}, [data]);
|
|
90
|
+
}, [data, viewer?.projectId]);
|
|
85
91
|
|
|
86
92
|
/* Chat hook */
|
|
87
93
|
const {
|
|
@@ -121,12 +127,24 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
121
127
|
}, 300);
|
|
122
128
|
}, [persistWidgetState]);
|
|
123
129
|
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const id = data?.widget?.id;
|
|
132
|
+
if (!id) return;
|
|
133
|
+
setPermissionsOk(hasStoredPermissionsGrant(id));
|
|
134
|
+
}, [data?.widget?.id]);
|
|
135
|
+
|
|
124
136
|
const restoredRef = useRef(false);
|
|
125
137
|
useEffect(() => {
|
|
126
138
|
if (!data?.widget || restoredRef.current) return;
|
|
127
139
|
const w = data.widget;
|
|
128
140
|
setMessageSoundEnabledState(getMessageSoundEnabled(w.id));
|
|
129
|
-
|
|
141
|
+
const uidForBlock = (viewer?.uid ?? w.viewerUid)?.trim();
|
|
142
|
+
let viewerIsBlocked = w.viewerBlocked === true;
|
|
143
|
+
if (!viewerIsBlocked && uidForBlock) {
|
|
144
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uidForBlock);
|
|
145
|
+
viewerIsBlocked = rec?.viewerBlocked === true;
|
|
146
|
+
}
|
|
147
|
+
if (viewerIsBlocked) {
|
|
130
148
|
clearChat();
|
|
131
149
|
setScreen('home');
|
|
132
150
|
setActiveTab('home');
|
|
@@ -142,7 +160,11 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
142
160
|
setViewingTicketId(p.viewingTicketId ?? null);
|
|
143
161
|
setChatReturnCtx(p.chatReturnCtx ?? 'conversation');
|
|
144
162
|
if (p.activeUserUid) {
|
|
145
|
-
const
|
|
163
|
+
const pid = viewer?.projectId?.trim();
|
|
164
|
+
const pool = pid
|
|
165
|
+
? [...data.developers, ...data.users].filter(u => u.project === pid)
|
|
166
|
+
: [...data.developers, ...data.users];
|
|
167
|
+
const u = pool.find(x => x.uid === p.activeUserUid);
|
|
146
168
|
if (u) {
|
|
147
169
|
const hist = Array.isArray(p.messages) && p.messages.length
|
|
148
170
|
? p.messages
|
|
@@ -152,15 +174,23 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
176
|
restoredRef.current = true;
|
|
155
|
-
}, [data, selectUser, clearChat]);
|
|
177
|
+
}, [data, selectUser, clearChat, viewer?.projectId, viewer?.uid]);
|
|
156
178
|
|
|
157
179
|
useEffect(() => {
|
|
158
|
-
if (!data?.widget
|
|
180
|
+
if (!data?.widget) return;
|
|
181
|
+
const w = data.widget;
|
|
182
|
+
const uid = (viewer?.uid ?? w.viewerUid)?.trim();
|
|
183
|
+
let blocked = w.viewerBlocked === true;
|
|
184
|
+
if (!blocked && uid) {
|
|
185
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
|
|
186
|
+
blocked = rec?.viewerBlocked === true;
|
|
187
|
+
}
|
|
188
|
+
if (!blocked) return;
|
|
159
189
|
clearChat();
|
|
160
190
|
setScreen('home');
|
|
161
191
|
setActiveTab('home');
|
|
162
192
|
setViewingTicketId(null);
|
|
163
|
-
}, [data?.widget?.
|
|
193
|
+
}, [data?.widget, data?.developers, data?.users, viewer?.uid, clearChat]);
|
|
164
194
|
|
|
165
195
|
useEffect(() => {
|
|
166
196
|
if (!data?.widget) return;
|
|
@@ -308,10 +338,40 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
308
338
|
|
|
309
339
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
310
340
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
311
|
-
|
|
341
|
+
|
|
342
|
+
const widgetConfig = useMemo(() => {
|
|
343
|
+
if (!data?.widget) return undefined;
|
|
344
|
+
const w = { ...data.widget };
|
|
345
|
+
if (viewer) {
|
|
346
|
+
w.viewerUid = viewer.uid;
|
|
347
|
+
w.viewerName = viewer.name;
|
|
348
|
+
w.viewerType = viewer.type;
|
|
349
|
+
if (viewer.projectId?.trim()) w.viewerProjectId = viewer.projectId.trim();
|
|
350
|
+
}
|
|
351
|
+
return w;
|
|
352
|
+
}, [data?.widget, viewer]);
|
|
353
|
+
|
|
312
354
|
const primaryColor = theme.primaryColor;
|
|
313
355
|
|
|
314
|
-
|
|
356
|
+
/** All developers are listed; only end-`user` rows are filtered by `viewer.projectId`. */
|
|
357
|
+
const allUsers = useMemo(() => {
|
|
358
|
+
if (!data) return [];
|
|
359
|
+
const pid = viewer?.projectId?.trim();
|
|
360
|
+
const devs = data.developers ?? [];
|
|
361
|
+
if (!pid) return [...devs, ...data.users];
|
|
362
|
+
const usersInProject = data.users.filter(u => u.project === pid);
|
|
363
|
+
return [...devs, ...usersInProject];
|
|
364
|
+
}, [data, viewer?.projectId]);
|
|
365
|
+
|
|
366
|
+
const effectiveViewerBlocked = useMemo(() => {
|
|
367
|
+
if (!widgetConfig) return false;
|
|
368
|
+
if (widgetConfig.viewerBlocked === true) return true;
|
|
369
|
+
const uid = (viewer?.uid ?? widgetConfig.viewerUid)?.trim();
|
|
370
|
+
if (!uid || !data) return false;
|
|
371
|
+
const rec = [...data.developers, ...data.users].find(x => x.uid === uid);
|
|
372
|
+
return rec?.viewerBlocked === true;
|
|
373
|
+
}, [widgetConfig, viewer?.uid, data]);
|
|
374
|
+
|
|
315
375
|
const viewerIsDev = widgetConfig?.viewerType === 'developer';
|
|
316
376
|
const viewerUid = widgetConfig?.viewerUid;
|
|
317
377
|
|
|
@@ -328,7 +388,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
328
388
|
})
|
|
329
389
|
: [];
|
|
330
390
|
|
|
331
|
-
const otherDevelopers =
|
|
391
|
+
const otherDevelopers = useMemo(
|
|
392
|
+
() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid),
|
|
393
|
+
[allUsers, viewerUid],
|
|
394
|
+
);
|
|
332
395
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
333
396
|
|
|
334
397
|
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
@@ -530,12 +593,21 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
530
593
|
)}
|
|
531
594
|
|
|
532
595
|
{/* ── ACTIVE: viewer spam-blocked (no chat/tickets UI) ── */}
|
|
533
|
-
{widgetConfig.status === 'ACTIVE' &&
|
|
596
|
+
{widgetConfig.status === 'ACTIVE' && effectiveViewerBlocked && (
|
|
534
597
|
<ViewerBlockedScreen config={widgetConfig} apiKey={apiKey} />
|
|
535
598
|
)}
|
|
536
599
|
|
|
600
|
+
{/* ── ACTIVE: microphone, location, screen share required ── */}
|
|
601
|
+
{widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (
|
|
602
|
+
<PermissionsGateScreen
|
|
603
|
+
primaryColor={primaryColor}
|
|
604
|
+
widgetId={widgetConfig.id}
|
|
605
|
+
onGranted={() => setPermissionsOk(true)}
|
|
606
|
+
/>
|
|
607
|
+
)}
|
|
608
|
+
|
|
537
609
|
{/* ── ACTIVE ── */}
|
|
538
|
-
{widgetConfig.status === 'ACTIVE' && !
|
|
610
|
+
{widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (
|
|
539
611
|
<div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
540
612
|
|
|
541
613
|
{screen === 'home' && (
|
|
@@ -653,7 +725,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
653
725
|
|
|
654
726
|
{/* ── Bottom Tabs (hidden during chat/call/user-list/block-list) ── */}
|
|
655
727
|
{widgetConfig.status === 'ACTIVE' &&
|
|
656
|
-
!
|
|
728
|
+
!effectiveViewerBlocked &&
|
|
729
|
+
permissionsOk &&
|
|
657
730
|
screen !== 'chat' &&
|
|
658
731
|
screen !== 'call' &&
|
|
659
732
|
screen !== 'user-list' &&
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { requestWidgetPermissions, storePermissionsGrant } from '../../utils/widgetPermissions';
|
|
5
|
+
|
|
6
|
+
const DENIED =
|
|
7
|
+
'You cannot use this widget due to less permission granted by you.';
|
|
8
|
+
|
|
9
|
+
interface PermissionsGateScreenProps {
|
|
10
|
+
primaryColor: string;
|
|
11
|
+
widgetId: string;
|
|
12
|
+
onGranted: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const PermissionsGateScreen: React.FC<PermissionsGateScreenProps> = ({
|
|
16
|
+
primaryColor,
|
|
17
|
+
widgetId,
|
|
18
|
+
onGranted,
|
|
19
|
+
}) => {
|
|
20
|
+
const [phase, setPhase] = useState<'prompt' | 'checking' | 'denied'>('prompt');
|
|
21
|
+
|
|
22
|
+
const handleAllow = async () => {
|
|
23
|
+
setPhase('checking');
|
|
24
|
+
const ok = await requestWidgetPermissions();
|
|
25
|
+
if (ok) {
|
|
26
|
+
storePermissionsGrant(widgetId);
|
|
27
|
+
onGranted();
|
|
28
|
+
} else {
|
|
29
|
+
setPhase('denied');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
flex: 1,
|
|
37
|
+
display: 'flex',
|
|
38
|
+
flexDirection: 'column',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
padding: '28px 22px',
|
|
42
|
+
textAlign: 'center',
|
|
43
|
+
minHeight: 0,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{phase === 'denied' ? (
|
|
47
|
+
<>
|
|
48
|
+
<div style={{ fontSize: 44, marginBottom: 16 }}>🔒</div>
|
|
49
|
+
<p style={{ margin: '0 0 20px', fontSize: 15, fontWeight: 600, color: '#1e293b', lineHeight: 1.55, maxWidth: 320 }}>
|
|
50
|
+
{DENIED}
|
|
51
|
+
</p>
|
|
52
|
+
<p style={{ margin: '0 0 22px', fontSize: 13, color: '#64748b', lineHeight: 1.5, maxWidth: 340 }}>
|
|
53
|
+
Allow microphone, location, and screen sharing in your browser settings for this site, then try again.
|
|
54
|
+
</p>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => { setPhase('prompt'); void handleAllow(); }}
|
|
58
|
+
style={{
|
|
59
|
+
padding: '12px 22px',
|
|
60
|
+
borderRadius: 12,
|
|
61
|
+
border: 'none',
|
|
62
|
+
background: primaryColor,
|
|
63
|
+
color: '#fff',
|
|
64
|
+
fontWeight: 700,
|
|
65
|
+
fontSize: 14,
|
|
66
|
+
cursor: 'pointer',
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
Try again
|
|
70
|
+
</button>
|
|
71
|
+
</>
|
|
72
|
+
) : (
|
|
73
|
+
<>
|
|
74
|
+
<div style={{ fontSize: 44, marginBottom: 16 }}>🎙️</div>
|
|
75
|
+
<p style={{ margin: '0 0 10px', fontSize: 16, fontWeight: 700, color: '#0f172a' }}>Permissions required</p>
|
|
76
|
+
<p style={{ margin: '0 0 8px', fontSize: 14, color: '#475569', lineHeight: 1.55, maxWidth: 340 }}>
|
|
77
|
+
This widget needs <strong>microphone</strong> (voice & calls), <strong>location</strong>, and{' '}
|
|
78
|
+
<strong>screen sharing</strong> to work.
|
|
79
|
+
</p>
|
|
80
|
+
<p style={{ margin: '0 0 22px', fontSize: 12, color: '#94a3b8', lineHeight: 1.45, maxWidth: 360 }}>
|
|
81
|
+
You will be asked to pick a screen once — you can stop sharing immediately after; we only verify access.
|
|
82
|
+
</p>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
disabled={phase === 'checking'}
|
|
86
|
+
onClick={handleAllow}
|
|
87
|
+
style={{
|
|
88
|
+
padding: '14px 28px',
|
|
89
|
+
borderRadius: 12,
|
|
90
|
+
border: 'none',
|
|
91
|
+
background: phase === 'checking' ? '#94a3b8' : primaryColor,
|
|
92
|
+
color: '#fff',
|
|
93
|
+
fontWeight: 700,
|
|
94
|
+
fontSize: 15,
|
|
95
|
+
cursor: phase === 'checking' ? 'default' : 'pointer',
|
|
96
|
+
minWidth: 200,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{phase === 'checking' ? 'Checking…' : 'Allow & continue'}
|
|
100
|
+
</button>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { mergeTheme, darken } from './utils/theme';
|
|
|
23
23
|
export { avatarColor, initials, formatTime, formatDate, generateTranscript, downloadText, truncateWords } from './utils/chat';
|
|
24
24
|
|
|
25
25
|
export type {
|
|
26
|
-
ChatWidgetProps, ChatWidgetTheme,
|
|
26
|
+
ChatWidgetProps, ChatWidgetTheme, ChatWidgetViewer,
|
|
27
27
|
WidgetConfig, RemoteChatData,
|
|
28
28
|
ChatUser, ChatMessage, Ticket, RecentChat,
|
|
29
29
|
CallSession, CallState,
|
package/src/types/index.ts
CHANGED
|
@@ -24,6 +24,11 @@ export interface WidgetConfig {
|
|
|
24
24
|
viewerUid?: string;
|
|
25
25
|
/** Display name for transfer notes (optional) */
|
|
26
26
|
viewerName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Host app project scope (set when embedding passes `viewer.projectId`).
|
|
29
|
+
* Use for API calls; lists can be filtered to users in the same project.
|
|
30
|
+
*/
|
|
31
|
+
viewerProjectId?: string;
|
|
27
32
|
/** Privacy Policy URL (linked from chat consent banner) */
|
|
28
33
|
privacyPolicyUrl?: string;
|
|
29
34
|
/** Set false to hide the consent note above the composer */
|
|
@@ -95,6 +100,11 @@ export interface ChatUser {
|
|
|
95
100
|
avatar: string | null;
|
|
96
101
|
status: OnlineStatus;
|
|
97
102
|
designation: string;
|
|
103
|
+
/**
|
|
104
|
+
* When `true` for the row matching the current viewer (`viewerUid` / `viewer.uid`),
|
|
105
|
+
* the widget shows the spam/blocked screen (same as `widget.viewerBlocked`).
|
|
106
|
+
*/
|
|
107
|
+
viewerBlocked?: boolean;
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
// ─── Message ────────────────────────────────────────────────────────────────
|
|
@@ -167,6 +177,19 @@ export interface ChatWidgetTheme {
|
|
|
167
177
|
borderRadius?: string;
|
|
168
178
|
}
|
|
169
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Pass the logged-in user from your React app so the widget matches identity and UI (user vs developer).
|
|
182
|
+
* Overrides `viewerUid`, `viewerName`, `viewerType` from remote `chatData.json` when provided.
|
|
183
|
+
*/
|
|
184
|
+
export interface ChatWidgetViewer {
|
|
185
|
+
uid: string;
|
|
186
|
+
name: string;
|
|
187
|
+
type: UserType;
|
|
188
|
+
/** When set, directory lists only include users whose `ChatUser.project` equals this string (exact match). */
|
|
189
|
+
projectId?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
170
192
|
export interface ChatWidgetProps {
|
|
171
193
|
theme?: ChatWidgetTheme;
|
|
194
|
+
viewer?: ChatWidgetViewer;
|
|
172
195
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests microphone, geolocation, and screen-capture access required by the widget.
|
|
3
|
+
* Stops all tracks immediately after success (probe only).
|
|
4
|
+
*/
|
|
5
|
+
export async function requestWidgetPermissions(): Promise<boolean> {
|
|
6
|
+
if (typeof navigator === 'undefined') return false;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (!navigator.mediaDevices?.getUserMedia) return false;
|
|
10
|
+
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11
|
+
mic.getTracks().forEach(t => t.stop());
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (!navigator.geolocation?.getCurrentPosition) return false;
|
|
18
|
+
await new Promise<void>((resolve, reject) => {
|
|
19
|
+
navigator.geolocation.getCurrentPosition(
|
|
20
|
+
() => resolve(),
|
|
21
|
+
e => reject(e),
|
|
22
|
+
{ enableHighAccuracy: false, timeout: 20_000, maximumAge: 60_000 },
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (!navigator.mediaDevices?.getDisplayMedia) return false;
|
|
31
|
+
const screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
32
|
+
screen.getTracks().forEach(t => t.stop());
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function permissionsSessionKey(widgetId: string): string {
|
|
41
|
+
return `ajaxter_widget_permissions_ok_${widgetId}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasStoredPermissionsGrant(widgetId: string): boolean {
|
|
45
|
+
if (typeof sessionStorage === 'undefined') return false;
|
|
46
|
+
try {
|
|
47
|
+
return sessionStorage.getItem(permissionsSessionKey(widgetId)) === '1';
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function storePermissionsGrant(widgetId: string): void {
|
|
54
|
+
try {
|
|
55
|
+
sessionStorage.setItem(permissionsSessionKey(widgetId), '1');
|
|
56
|
+
} catch {
|
|
57
|
+
/* quota */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function clearStoredPermissionsGrant(widgetId: string): void {
|
|
62
|
+
try {
|
|
63
|
+
sessionStorage.removeItem(permissionsSessionKey(widgetId));
|
|
64
|
+
} catch {
|
|
65
|
+
/* */
|
|
66
|
+
}
|
|
67
|
+
}
|