ajaxter-chat 3.0.13 → 3.0.14
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 +55 -16
- 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 +17 -0
- package/dist/utils/widgetPermissions.d.ts +9 -0
- package/dist/utils/widgetPermissions.js +67 -0
- package/package.json +1 -1
- package/src/components/ChatWidget.tsx +58 -11
- package/src/components/PermissionsGateScreen/index.tsx +105 -0
- package/src/index.ts +1 -1
- package/src/types/index.ts +18 -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,7 +20,9 @@ import { CallScreen } from './CallScreen';
|
|
|
20
20
|
import { MaintenanceView } from './MaintenanceView';
|
|
21
21
|
import { BottomTabs } from './Tabs/BottomTabs';
|
|
22
22
|
import { ViewerBlockedScreen } from './ViewerBlockedScreen';
|
|
23
|
-
|
|
23
|
+
import { PermissionsGateScreen } from './PermissionsGateScreen';
|
|
24
|
+
import { hasStoredPermissionsGrant } from '../utils/widgetPermissions';
|
|
25
|
+
export const ChatWidget = ({ theme: localTheme, viewer }) => {
|
|
24
26
|
var _a, _b, _c, _d, _e, _f;
|
|
25
27
|
/* SSR guard */
|
|
26
28
|
const [mounted, setMounted] = useState(false);
|
|
@@ -43,18 +45,21 @@ 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;
|
|
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 inProject = (u) => !pid || u.project === pid;
|
|
62
|
+
const all = [...((_b = data.developers) !== null && _b !== void 0 ? _b : []), ...((_c = data.users) !== null && _c !== void 0 ? _c : [])].filter(inProject);
|
|
58
63
|
const recents = Object.entries(data.sampleChats).map(([uid, msgs]) => {
|
|
59
64
|
const user = all.find(u => u.uid === uid);
|
|
60
65
|
if (!user || msgs.length === 0)
|
|
@@ -71,7 +76,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
71
76
|
}).filter(Boolean);
|
|
72
77
|
setRecentChats(recents);
|
|
73
78
|
}
|
|
74
|
-
}, [data]);
|
|
79
|
+
}, [data, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
|
|
75
80
|
/* Chat hook */
|
|
76
81
|
const { messages, activeUser, isPaused, isReported, selectUser, sendMessage, togglePause, reportChat, clearChat, setMessages, } = useChat();
|
|
77
82
|
/* WebRTC hook */
|
|
@@ -104,9 +109,16 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
104
109
|
setClosing(false);
|
|
105
110
|
}, 300);
|
|
106
111
|
}, [persistWidgetState]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
var _a;
|
|
114
|
+
const id = (_a = data === null || data === void 0 ? void 0 : data.widget) === null || _a === void 0 ? void 0 : _a.id;
|
|
115
|
+
if (!id)
|
|
116
|
+
return;
|
|
117
|
+
setPermissionsOk(hasStoredPermissionsGrant(id));
|
|
118
|
+
}, [(_c = data === null || data === void 0 ? void 0 : data.widget) === null || _c === void 0 ? void 0 : _c.id]);
|
|
107
119
|
const restoredRef = useRef(false);
|
|
108
120
|
useEffect(() => {
|
|
109
|
-
var _a, _b, _c;
|
|
121
|
+
var _a, _b, _c, _d;
|
|
110
122
|
if (!(data === null || data === void 0 ? void 0 : data.widget) || restoredRef.current)
|
|
111
123
|
return;
|
|
112
124
|
const w = data.widget;
|
|
@@ -127,17 +139,21 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
127
139
|
setViewingTicketId((_a = p.viewingTicketId) !== null && _a !== void 0 ? _a : null);
|
|
128
140
|
setChatReturnCtx((_b = p.chatReturnCtx) !== null && _b !== void 0 ? _b : 'conversation');
|
|
129
141
|
if (p.activeUserUid) {
|
|
130
|
-
const
|
|
142
|
+
const pid = (_c = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _c === void 0 ? void 0 : _c.trim();
|
|
143
|
+
const pool = pid
|
|
144
|
+
? [...data.developers, ...data.users].filter(u => u.project === pid)
|
|
145
|
+
: [...data.developers, ...data.users];
|
|
146
|
+
const u = pool.find(x => x.uid === p.activeUserUid);
|
|
131
147
|
if (u) {
|
|
132
148
|
const hist = Array.isArray(p.messages) && p.messages.length
|
|
133
149
|
? p.messages
|
|
134
|
-
: ((
|
|
150
|
+
: ((_d = data.sampleChats[u.uid]) !== null && _d !== void 0 ? _d : []);
|
|
135
151
|
selectUser(u, hist);
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
restoredRef.current = true;
|
|
140
|
-
}, [data, selectUser, clearChat]);
|
|
156
|
+
}, [data, selectUser, clearChat, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
|
|
141
157
|
useEffect(() => {
|
|
142
158
|
var _a;
|
|
143
159
|
if (!((_a = data === null || data === void 0 ? void 0 : data.widget) === null || _a === void 0 ? void 0 : _a.viewerBlocked))
|
|
@@ -146,12 +162,12 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
146
162
|
setScreen('home');
|
|
147
163
|
setActiveTab('home');
|
|
148
164
|
setViewingTicketId(null);
|
|
149
|
-
}, [(
|
|
165
|
+
}, [(_d = data === null || data === void 0 ? void 0 : data.widget) === null || _d === void 0 ? void 0 : _d.viewerBlocked, clearChat]);
|
|
150
166
|
useEffect(() => {
|
|
151
167
|
if (!(data === null || data === void 0 ? void 0 : data.widget))
|
|
152
168
|
return;
|
|
153
169
|
persistWidgetState();
|
|
154
|
-
}, [(
|
|
170
|
+
}, [(_e = data === null || data === void 0 ? void 0 : data.widget) === null || _e === void 0 ? void 0 : _e.id, screen, activeTab, userListCtx, activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid, messages, viewingTicketId, chatReturnCtx, persistWidgetState]);
|
|
155
171
|
const incomingSoundRef = useRef(0);
|
|
156
172
|
useEffect(() => {
|
|
157
173
|
incomingSoundRef.current = messages.length;
|
|
@@ -287,9 +303,31 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
287
303
|
}, [endCall]);
|
|
288
304
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
289
305
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
290
|
-
const widgetConfig =
|
|
306
|
+
const widgetConfig = useMemo(() => {
|
|
307
|
+
var _a;
|
|
308
|
+
if (!(data === null || data === void 0 ? void 0 : data.widget))
|
|
309
|
+
return undefined;
|
|
310
|
+
const w = Object.assign({}, data.widget);
|
|
311
|
+
if (viewer) {
|
|
312
|
+
w.viewerUid = viewer.uid;
|
|
313
|
+
w.viewerName = viewer.name;
|
|
314
|
+
w.viewerType = viewer.type;
|
|
315
|
+
if ((_a = viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim())
|
|
316
|
+
w.viewerProjectId = viewer.projectId.trim();
|
|
317
|
+
}
|
|
318
|
+
return w;
|
|
319
|
+
}, [data === null || data === void 0 ? void 0 : data.widget, viewer]);
|
|
291
320
|
const primaryColor = theme.primaryColor;
|
|
292
|
-
const allUsers =
|
|
321
|
+
const allUsers = useMemo(() => {
|
|
322
|
+
var _a;
|
|
323
|
+
if (!data)
|
|
324
|
+
return [];
|
|
325
|
+
const pid = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.projectId) === null || _a === void 0 ? void 0 : _a.trim();
|
|
326
|
+
const list = [...data.developers, ...data.users];
|
|
327
|
+
if (!pid)
|
|
328
|
+
return list;
|
|
329
|
+
return list.filter(u => u.project === pid);
|
|
330
|
+
}, [data, viewer === null || viewer === void 0 ? void 0 : viewer.projectId]);
|
|
293
331
|
const viewerIsDev = (widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerType) === 'developer';
|
|
294
332
|
const viewerUid = widgetConfig === null || widgetConfig === void 0 ? void 0 : widgetConfig.viewerUid;
|
|
295
333
|
const filteredUsers = screen === 'user-list'
|
|
@@ -305,7 +343,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
305
343
|
return u.type === 'user';
|
|
306
344
|
})
|
|
307
345
|
: [];
|
|
308
|
-
const otherDevelopers = (
|
|
346
|
+
const otherDevelopers = useMemo(() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid), [allUsers, viewerUid]);
|
|
309
347
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
310
348
|
const handleTransferToDeveloper = useCallback((dev) => {
|
|
311
349
|
var _a;
|
|
@@ -396,7 +434,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
396
434
|
right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
|
|
397
435
|
left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
|
|
398
436
|
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' && widgetConfig.viewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey })), widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && (_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: (_f = widgetConfig.viewerType) !== null && _f !== void 0 ? _f : '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 => {
|
|
437
|
+
}, 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' && widgetConfig.viewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey })), widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && 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: (_f = widgetConfig.viewerType) !== null && _f !== void 0 ? _f : '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
438
|
setListEntranceAnimation(false);
|
|
401
439
|
setViewingTicketId(id);
|
|
402
440
|
setScreen('ticket-detail');
|
|
@@ -405,6 +443,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
|
|
|
405
443
|
return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
|
|
406
444
|
})()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
|
|
407
445
|
!widgetConfig.viewerBlocked &&
|
|
446
|
+
permissionsOk &&
|
|
408
447
|
screen !== 'chat' &&
|
|
409
448
|
screen !== 'call' &&
|
|
410
449
|
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 */
|
|
@@ -138,6 +143,18 @@ export interface ChatWidgetTheme {
|
|
|
138
143
|
buttonPosition?: 'bottom-right' | 'bottom-left';
|
|
139
144
|
borderRadius?: string;
|
|
140
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Pass the logged-in user from your React app so the widget matches identity and UI (user vs developer).
|
|
148
|
+
* Overrides `viewerUid`, `viewerName`, `viewerType` from remote `chatData.json` when provided.
|
|
149
|
+
*/
|
|
150
|
+
export interface ChatWidgetViewer {
|
|
151
|
+
uid: string;
|
|
152
|
+
name: string;
|
|
153
|
+
type: UserType;
|
|
154
|
+
/** When set, directory lists only include users whose `ChatUser.project` equals this string (exact match). */
|
|
155
|
+
projectId?: string;
|
|
156
|
+
}
|
|
141
157
|
export interface ChatWidgetProps {
|
|
142
158
|
theme?: ChatWidgetTheme;
|
|
159
|
+
viewer?: ChatWidgetViewer;
|
|
143
160
|
}
|
|
@@ -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.14",
|
|
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",
|
|
@@ -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,9 @@ 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 inProject = (u: ChatUser) => !pid || u.project === pid;
|
|
73
|
+
const all = [...(data.developers ?? []), ...(data.users ?? [])].filter(inProject);
|
|
69
74
|
const recents: RecentChat[] = Object.entries(data.sampleChats).map(([uid, msgs]) => {
|
|
70
75
|
const user = all.find(u => u.uid === uid);
|
|
71
76
|
if (!user || msgs.length === 0) return null;
|
|
@@ -81,7 +86,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
81
86
|
}).filter(Boolean) as RecentChat[];
|
|
82
87
|
setRecentChats(recents);
|
|
83
88
|
}
|
|
84
|
-
}, [data]);
|
|
89
|
+
}, [data, viewer?.projectId]);
|
|
85
90
|
|
|
86
91
|
/* Chat hook */
|
|
87
92
|
const {
|
|
@@ -121,6 +126,12 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
121
126
|
}, 300);
|
|
122
127
|
}, [persistWidgetState]);
|
|
123
128
|
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const id = data?.widget?.id;
|
|
131
|
+
if (!id) return;
|
|
132
|
+
setPermissionsOk(hasStoredPermissionsGrant(id));
|
|
133
|
+
}, [data?.widget?.id]);
|
|
134
|
+
|
|
124
135
|
const restoredRef = useRef(false);
|
|
125
136
|
useEffect(() => {
|
|
126
137
|
if (!data?.widget || restoredRef.current) return;
|
|
@@ -142,7 +153,11 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
142
153
|
setViewingTicketId(p.viewingTicketId ?? null);
|
|
143
154
|
setChatReturnCtx(p.chatReturnCtx ?? 'conversation');
|
|
144
155
|
if (p.activeUserUid) {
|
|
145
|
-
const
|
|
156
|
+
const pid = viewer?.projectId?.trim();
|
|
157
|
+
const pool = pid
|
|
158
|
+
? [...data.developers, ...data.users].filter(u => u.project === pid)
|
|
159
|
+
: [...data.developers, ...data.users];
|
|
160
|
+
const u = pool.find(x => x.uid === p.activeUserUid);
|
|
146
161
|
if (u) {
|
|
147
162
|
const hist = Array.isArray(p.messages) && p.messages.length
|
|
148
163
|
? p.messages
|
|
@@ -152,7 +167,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
restoredRef.current = true;
|
|
155
|
-
}, [data, selectUser, clearChat]);
|
|
170
|
+
}, [data, selectUser, clearChat, viewer?.projectId]);
|
|
156
171
|
|
|
157
172
|
useEffect(() => {
|
|
158
173
|
if (!data?.widget?.viewerBlocked) return;
|
|
@@ -308,10 +323,29 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
308
323
|
|
|
309
324
|
/* ── Derived ─────────────────────────────────────────────────────────── */
|
|
310
325
|
const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
|
|
311
|
-
|
|
326
|
+
|
|
327
|
+
const widgetConfig = useMemo(() => {
|
|
328
|
+
if (!data?.widget) return undefined;
|
|
329
|
+
const w = { ...data.widget };
|
|
330
|
+
if (viewer) {
|
|
331
|
+
w.viewerUid = viewer.uid;
|
|
332
|
+
w.viewerName = viewer.name;
|
|
333
|
+
w.viewerType = viewer.type;
|
|
334
|
+
if (viewer.projectId?.trim()) w.viewerProjectId = viewer.projectId.trim();
|
|
335
|
+
}
|
|
336
|
+
return w;
|
|
337
|
+
}, [data?.widget, viewer]);
|
|
338
|
+
|
|
312
339
|
const primaryColor = theme.primaryColor;
|
|
313
340
|
|
|
314
|
-
const allUsers
|
|
341
|
+
const allUsers = useMemo(() => {
|
|
342
|
+
if (!data) return [];
|
|
343
|
+
const pid = viewer?.projectId?.trim();
|
|
344
|
+
const list = [...data.developers, ...data.users];
|
|
345
|
+
if (!pid) return list;
|
|
346
|
+
return list.filter(u => u.project === pid);
|
|
347
|
+
}, [data, viewer?.projectId]);
|
|
348
|
+
|
|
315
349
|
const viewerIsDev = widgetConfig?.viewerType === 'developer';
|
|
316
350
|
const viewerUid = widgetConfig?.viewerUid;
|
|
317
351
|
|
|
@@ -328,7 +362,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
328
362
|
})
|
|
329
363
|
: [];
|
|
330
364
|
|
|
331
|
-
const otherDevelopers =
|
|
365
|
+
const otherDevelopers = useMemo(
|
|
366
|
+
() => allUsers.filter(u => u.type === 'developer' && u.uid !== viewerUid),
|
|
367
|
+
[allUsers, viewerUid],
|
|
368
|
+
);
|
|
332
369
|
const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
|
|
333
370
|
|
|
334
371
|
const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
|
|
@@ -534,8 +571,17 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
534
571
|
<ViewerBlockedScreen config={widgetConfig} apiKey={apiKey} />
|
|
535
572
|
)}
|
|
536
573
|
|
|
574
|
+
{/* ── ACTIVE: microphone, location, screen share required ── */}
|
|
575
|
+
{widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && !permissionsOk && (
|
|
576
|
+
<PermissionsGateScreen
|
|
577
|
+
primaryColor={primaryColor}
|
|
578
|
+
widgetId={widgetConfig.id}
|
|
579
|
+
onGranted={() => setPermissionsOk(true)}
|
|
580
|
+
/>
|
|
581
|
+
)}
|
|
582
|
+
|
|
537
583
|
{/* ── ACTIVE ── */}
|
|
538
|
-
{widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && (
|
|
584
|
+
{widgetConfig.status === 'ACTIVE' && !widgetConfig.viewerBlocked && permissionsOk && (
|
|
539
585
|
<div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
540
586
|
|
|
541
587
|
{screen === 'home' && (
|
|
@@ -654,6 +700,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
|
|
|
654
700
|
{/* ── Bottom Tabs (hidden during chat/call/user-list/block-list) ── */}
|
|
655
701
|
{widgetConfig.status === 'ACTIVE' &&
|
|
656
702
|
!widgetConfig.viewerBlocked &&
|
|
703
|
+
permissionsOk &&
|
|
657
704
|
screen !== 'chat' &&
|
|
658
705
|
screen !== 'call' &&
|
|
659
706
|
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 */
|
|
@@ -167,6 +172,19 @@ export interface ChatWidgetTheme {
|
|
|
167
172
|
borderRadius?: string;
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Pass the logged-in user from your React app so the widget matches identity and UI (user vs developer).
|
|
177
|
+
* Overrides `viewerUid`, `viewerName`, `viewerType` from remote `chatData.json` when provided.
|
|
178
|
+
*/
|
|
179
|
+
export interface ChatWidgetViewer {
|
|
180
|
+
uid: string;
|
|
181
|
+
name: string;
|
|
182
|
+
type: UserType;
|
|
183
|
+
/** When set, directory lists only include users whose `ChatUser.project` equals this string (exact match). */
|
|
184
|
+
projectId?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
export interface ChatWidgetProps {
|
|
171
188
|
theme?: ChatWidgetTheme;
|
|
189
|
+
viewer?: ChatWidgetViewer;
|
|
172
190
|
}
|
|
@@ -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
|
+
}
|