@sparrowdesk/react-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/README.md +9 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +400 -0
- package/package.json +68 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/sparrowDeskWidget.d.ts
|
|
5
|
+
interface SparrowDeskApi {
|
|
6
|
+
openWidget?: () => void;
|
|
7
|
+
closeWidget?: () => void;
|
|
8
|
+
hideWidget?: () => void;
|
|
9
|
+
onOpen?: (callback: () => void) => void;
|
|
10
|
+
onClose?: (callback: () => void) => void;
|
|
11
|
+
setTags?: (tags: string[]) => void;
|
|
12
|
+
setConversationFields?: (fields: Record<string, unknown>) => void;
|
|
13
|
+
setContactFields?: (fields: Record<string, unknown>) => void;
|
|
14
|
+
status?: 'open' | 'closed';
|
|
15
|
+
}
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
SD_WIDGET_TOKEN?: string;
|
|
19
|
+
SD_WIDGET_DOMAIN?: string;
|
|
20
|
+
sparrowDesk?: SparrowDeskApi;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/Chat.d.ts
|
|
25
|
+
interface ChatProps {
|
|
26
|
+
/** SparrowDesk domain, e.g. "sparrowdesk7975310.sparrowdesk.com" */
|
|
27
|
+
domain: string;
|
|
28
|
+
/** SparrowDesk widget token */
|
|
29
|
+
token: string;
|
|
30
|
+
/** Optional tags (e.g. user identifiers) for the current session. */
|
|
31
|
+
tags?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Contact fields to set during init (expects internal_name keys).
|
|
34
|
+
* Invalid internal_names / invalid values are skipped by the widget itself.
|
|
35
|
+
*/
|
|
36
|
+
contactFields?: Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* Conversation fields to set during init (expects internal_name keys).
|
|
39
|
+
* Invalid internal_names / invalid values are skipped by the widget itself.
|
|
40
|
+
*/
|
|
41
|
+
conversationFields?: Record<string, unknown>;
|
|
42
|
+
/** Called once the widget API is available on `window.sparrowDesk`. */
|
|
43
|
+
onReady?: (api: SparrowDeskApi) => void;
|
|
44
|
+
/** Called when the widget opens (registered via `window.sparrowDesk.onOpen`). */
|
|
45
|
+
onOpen?: () => void;
|
|
46
|
+
/** Called when the widget closes (registered via `window.sparrowDesk.onClose`). */
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
/** If true, calls `window.sparrowDesk.openWidget()` once when ready. */
|
|
49
|
+
openOnInit?: boolean;
|
|
50
|
+
/** If true, calls `window.sparrowDesk.hideWidget()` once when ready. */
|
|
51
|
+
hideOnInit?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Controls whether this component should set globals and inject the widget script.
|
|
54
|
+
* Set to `false` if SparrowDesk is loaded elsewhere and you only want to apply
|
|
55
|
+
* fields/tags + register callbacks.
|
|
56
|
+
*/
|
|
57
|
+
shouldInitialize?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* If `false`, defers injecting the widget script + waiting for the API until
|
|
60
|
+
* the visitor interacts (when `initializeOnInteraction` is enabled).
|
|
61
|
+
*
|
|
62
|
+
* This is a performance optimization implemented at the wrapper level by delaying
|
|
63
|
+
* script injection.
|
|
64
|
+
*/
|
|
65
|
+
connectOnPageLoad?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* When `connectOnPageLoad={false}`, if `true`, initialize the widget on the first
|
|
68
|
+
* user interaction (pointer or keyboard), then remove those listeners.
|
|
69
|
+
*/
|
|
70
|
+
initializeOnInteraction?: boolean;
|
|
71
|
+
/** If true, removes the injected script tag on unmount. */
|
|
72
|
+
cleanupOnUnmount?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* How long to wait (ms) for `window.sparrowDesk` to become available after init.
|
|
75
|
+
* Defaults to 10s.
|
|
76
|
+
*/
|
|
77
|
+
readyTimeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
declare const Chat: React.FC<ChatProps>;
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/SparrowDeskProvider.d.ts
|
|
82
|
+
type SparrowDeskProviderProps = {
|
|
83
|
+
/** SparrowDesk domain, e.g. "sparrowdesk7975310.sparrowdesk.com" */
|
|
84
|
+
domain: string;
|
|
85
|
+
/** SparrowDesk widget token */
|
|
86
|
+
token: string;
|
|
87
|
+
children: React$1.ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* Controls whether this provider should set globals and inject the widget script.
|
|
90
|
+
* Set to `false` if SparrowDesk is loaded elsewhere (e.g. via Segment) and you only
|
|
91
|
+
* want the hook-based API.
|
|
92
|
+
*/
|
|
93
|
+
shouldInitialize?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* If `false`, defers injecting the widget script + waiting for the API until
|
|
96
|
+
* you call `initialize()` (or invoke `openWidget`/`closeWidget`/`hideWidget`/etc),
|
|
97
|
+
* or until the first user interaction when `initializeOnInteraction` is enabled.
|
|
98
|
+
*
|
|
99
|
+
* This is a performance optimization implemented at the wrapper level by delaying
|
|
100
|
+
* script injection until you explicitly initialize.
|
|
101
|
+
*/
|
|
102
|
+
connectOnPageLoad?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* When `connectOnPageLoad={false}`, if `true`, initialize on the first
|
|
105
|
+
* user interaction (pointer or keyboard). Defaults to `true`.
|
|
106
|
+
*/
|
|
107
|
+
initializeOnInteraction?: boolean;
|
|
108
|
+
tags?: string[];
|
|
109
|
+
contactFields?: Record<string, unknown>;
|
|
110
|
+
conversationFields?: Record<string, unknown>;
|
|
111
|
+
onReady?: (api: SparrowDeskApi) => void;
|
|
112
|
+
onOpen?: () => void;
|
|
113
|
+
onClose?: () => void;
|
|
114
|
+
openOnInit?: boolean;
|
|
115
|
+
hideOnInit?: boolean;
|
|
116
|
+
cleanupOnUnmount?: boolean;
|
|
117
|
+
readyTimeoutMs?: number;
|
|
118
|
+
};
|
|
119
|
+
type SparrowDeskContextValue = {
|
|
120
|
+
isReady: boolean;
|
|
121
|
+
api: SparrowDeskApi | null;
|
|
122
|
+
/** Ensures the widget script is injected (if enabled) and begins waiting for the API. */
|
|
123
|
+
initialize: () => void;
|
|
124
|
+
openWidget: () => void;
|
|
125
|
+
closeWidget: () => void;
|
|
126
|
+
hideWidget: () => void;
|
|
127
|
+
setTags: (tags: string[]) => void;
|
|
128
|
+
setContactFields: (fields: Record<string, unknown>) => void;
|
|
129
|
+
setConversationFields: (fields: Record<string, unknown>) => void;
|
|
130
|
+
};
|
|
131
|
+
declare function useSparrowDesk(): SparrowDeskContextValue;
|
|
132
|
+
declare function SparrowDeskProvider({
|
|
133
|
+
domain,
|
|
134
|
+
token,
|
|
135
|
+
children,
|
|
136
|
+
shouldInitialize,
|
|
137
|
+
connectOnPageLoad,
|
|
138
|
+
initializeOnInteraction,
|
|
139
|
+
tags,
|
|
140
|
+
contactFields,
|
|
141
|
+
conversationFields,
|
|
142
|
+
onReady,
|
|
143
|
+
onOpen,
|
|
144
|
+
onClose,
|
|
145
|
+
openOnInit,
|
|
146
|
+
hideOnInit,
|
|
147
|
+
cleanupOnUnmount,
|
|
148
|
+
readyTimeoutMs
|
|
149
|
+
}: SparrowDeskProviderProps): react_jsx_runtime0.JSX.Element;
|
|
150
|
+
//#endregion
|
|
151
|
+
export { Chat, type ChatProps, type SparrowDeskApi, type SparrowDeskContextValue, SparrowDeskProvider, type SparrowDeskProviderProps, useSparrowDesk };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/sparrowDeskWidget.ts
|
|
6
|
+
const DEFAULT_SCRIPT_SRC = "https://assets.cdn.sparrowdesk.com/chatbot/bundle/main.js";
|
|
7
|
+
const DEFAULT_READY_TIMEOUT_MS = 1e4;
|
|
8
|
+
const WIDGET_SCRIPT_SELECTOR = "script[data-sd-chat-widget=\"true\"]";
|
|
9
|
+
function isBrowser() {
|
|
10
|
+
return globalThis.document !== void 0;
|
|
11
|
+
}
|
|
12
|
+
function normalizeRequired(value) {
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
function setWidgetGlobals(domain, token) {
|
|
16
|
+
const w = globalThis;
|
|
17
|
+
w.SD_WIDGET_DOMAIN = domain;
|
|
18
|
+
w.SD_WIDGET_TOKEN = token;
|
|
19
|
+
}
|
|
20
|
+
const scriptEntriesBySrc = /* @__PURE__ */ new Map();
|
|
21
|
+
function removeOtherWidgetScripts(keepSrc) {
|
|
22
|
+
document.querySelectorAll(WIDGET_SCRIPT_SELECTOR).forEach((script) => {
|
|
23
|
+
if (script.src !== keepSrc) script.remove();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function acquireWidgetScript(src, cleanupOnUnmount) {
|
|
27
|
+
const cached = scriptEntriesBySrc.get(src);
|
|
28
|
+
if (cached) {
|
|
29
|
+
cached.refCount += 1;
|
|
30
|
+
cached.cleanupWhenUnused ||= cleanupOnUnmount;
|
|
31
|
+
return { release() {
|
|
32
|
+
cached.refCount -= 1;
|
|
33
|
+
if (cached.refCount > 0) return;
|
|
34
|
+
if (cached.cleanupWhenUnused) cached.script.remove();
|
|
35
|
+
scriptEntriesBySrc.delete(src);
|
|
36
|
+
} };
|
|
37
|
+
}
|
|
38
|
+
const existing = document.querySelector(WIDGET_SCRIPT_SELECTOR);
|
|
39
|
+
const entry = {
|
|
40
|
+
script: existing?.src === src ? existing : (() => {
|
|
41
|
+
const el = document.createElement("script");
|
|
42
|
+
el.async = true;
|
|
43
|
+
el.src = src;
|
|
44
|
+
el.dataset["sdChatWidget"] = "true";
|
|
45
|
+
document.body.appendChild(el);
|
|
46
|
+
return el;
|
|
47
|
+
})(),
|
|
48
|
+
refCount: 1,
|
|
49
|
+
cleanupWhenUnused: cleanupOnUnmount
|
|
50
|
+
};
|
|
51
|
+
scriptEntriesBySrc.set(src, entry);
|
|
52
|
+
return { release() {
|
|
53
|
+
entry.refCount -= 1;
|
|
54
|
+
if (entry.refCount > 0) return;
|
|
55
|
+
if (entry.cleanupWhenUnused) entry.script.remove();
|
|
56
|
+
scriptEntriesBySrc.delete(src);
|
|
57
|
+
} };
|
|
58
|
+
}
|
|
59
|
+
async function waitForSparrowDeskApi(timeoutMs) {
|
|
60
|
+
const w = globalThis;
|
|
61
|
+
if (w.sparrowDesk) return w.sparrowDesk;
|
|
62
|
+
if (timeoutMs <= 0) return null;
|
|
63
|
+
const startedAt = Date.now();
|
|
64
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
65
|
+
if (w.sparrowDesk) return w.sparrowDesk;
|
|
66
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/internal/useLatest.ts
|
|
73
|
+
function useLatest(value) {
|
|
74
|
+
const ref = useRef(value);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
ref.current = value;
|
|
77
|
+
}, [value]);
|
|
78
|
+
return ref;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/Chat.tsx
|
|
83
|
+
const Chat = ({ domain, token, tags, contactFields, conversationFields, onReady, onOpen, onClose, openOnInit = false, hideOnInit = false, shouldInitialize = true, connectOnPageLoad = true, initializeOnInteraction = true, cleanupOnUnmount = false, readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS }) => {
|
|
84
|
+
const normalized = useMemo(() => {
|
|
85
|
+
return {
|
|
86
|
+
domain: normalizeRequired(domain),
|
|
87
|
+
token: normalizeRequired(token)
|
|
88
|
+
};
|
|
89
|
+
}, [domain, token]);
|
|
90
|
+
const onOpenRef = useLatest(onOpen);
|
|
91
|
+
const onCloseRef = useLatest(onClose);
|
|
92
|
+
const onReadyRef = useLatest(onReady);
|
|
93
|
+
const tagsRef = useLatest(tags);
|
|
94
|
+
const contactFieldsRef = useLatest(contactFields);
|
|
95
|
+
const conversationFieldsRef = useLatest(conversationFields);
|
|
96
|
+
const registeredCallbacksRef = useRef(false);
|
|
97
|
+
const apiRef = useRef(null);
|
|
98
|
+
const didOpenOnceRef = useRef(false);
|
|
99
|
+
const didHideOnceRef = useRef(false);
|
|
100
|
+
const [shouldStart, setShouldStart] = useState(connectOnPageLoad);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
didOpenOnceRef.current = false;
|
|
103
|
+
didHideOnceRef.current = false;
|
|
104
|
+
apiRef.current = null;
|
|
105
|
+
registeredCallbacksRef.current = false;
|
|
106
|
+
setShouldStart(connectOnPageLoad);
|
|
107
|
+
}, [normalized.domain, normalized.token]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!isBrowser()) return;
|
|
110
|
+
if (!normalized.domain || !normalized.token) return;
|
|
111
|
+
setWidgetGlobals(normalized.domain, normalized.token);
|
|
112
|
+
if (!shouldInitialize) return;
|
|
113
|
+
if (!shouldStart) return;
|
|
114
|
+
removeOtherWidgetScripts(DEFAULT_SCRIPT_SRC);
|
|
115
|
+
const handle = acquireWidgetScript(DEFAULT_SCRIPT_SRC, cleanupOnUnmount);
|
|
116
|
+
return () => {
|
|
117
|
+
handle.release();
|
|
118
|
+
};
|
|
119
|
+
}, [
|
|
120
|
+
normalized.domain,
|
|
121
|
+
normalized.token,
|
|
122
|
+
cleanupOnUnmount,
|
|
123
|
+
shouldInitialize,
|
|
124
|
+
shouldStart
|
|
125
|
+
]);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!isBrowser()) return;
|
|
128
|
+
if (!normalized.domain || !normalized.token) return;
|
|
129
|
+
if (!shouldStart) return;
|
|
130
|
+
let cancelled = false;
|
|
131
|
+
(async () => {
|
|
132
|
+
const api = await waitForSparrowDeskApi(readyTimeoutMs);
|
|
133
|
+
if (cancelled || !api) return;
|
|
134
|
+
apiRef.current = api;
|
|
135
|
+
if (!registeredCallbacksRef.current) {
|
|
136
|
+
api.onOpen?.(() => onOpenRef.current?.());
|
|
137
|
+
api.onClose?.(() => onCloseRef.current?.());
|
|
138
|
+
registeredCallbacksRef.current = true;
|
|
139
|
+
}
|
|
140
|
+
onReadyRef.current?.(api);
|
|
141
|
+
const latestTags = tagsRef.current;
|
|
142
|
+
const latestContactFields = contactFieldsRef.current;
|
|
143
|
+
const latestConversationFields = conversationFieldsRef.current;
|
|
144
|
+
if (Array.isArray(latestTags) && latestTags.length) api.setTags?.(latestTags);
|
|
145
|
+
if (latestContactFields && Object.keys(latestContactFields).length) api.setContactFields?.(latestContactFields);
|
|
146
|
+
if (latestConversationFields && Object.keys(latestConversationFields).length) api.setConversationFields?.(latestConversationFields);
|
|
147
|
+
if (hideOnInit && !didHideOnceRef.current) {
|
|
148
|
+
api.hideWidget?.();
|
|
149
|
+
didHideOnceRef.current = true;
|
|
150
|
+
}
|
|
151
|
+
if (openOnInit && !didOpenOnceRef.current) {
|
|
152
|
+
api.openWidget?.();
|
|
153
|
+
didOpenOnceRef.current = true;
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
return () => {
|
|
157
|
+
cancelled = true;
|
|
158
|
+
};
|
|
159
|
+
}, [
|
|
160
|
+
normalized.domain,
|
|
161
|
+
normalized.token,
|
|
162
|
+
openOnInit,
|
|
163
|
+
hideOnInit,
|
|
164
|
+
readyTimeoutMs,
|
|
165
|
+
shouldStart
|
|
166
|
+
]);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!isBrowser()) return;
|
|
169
|
+
if (connectOnPageLoad) return;
|
|
170
|
+
if (!initializeOnInteraction) return;
|
|
171
|
+
if (shouldStart) return;
|
|
172
|
+
if (!normalized.domain || !normalized.token) return;
|
|
173
|
+
const onFirstInteraction = () => {
|
|
174
|
+
setShouldStart(true);
|
|
175
|
+
cleanup();
|
|
176
|
+
};
|
|
177
|
+
const cleanup = () => {
|
|
178
|
+
document.removeEventListener("pointerdown", onFirstInteraction, true);
|
|
179
|
+
document.removeEventListener("keydown", onFirstInteraction, true);
|
|
180
|
+
};
|
|
181
|
+
document.addEventListener("pointerdown", onFirstInteraction, true);
|
|
182
|
+
document.addEventListener("keydown", onFirstInteraction, true);
|
|
183
|
+
return cleanup;
|
|
184
|
+
}, [
|
|
185
|
+
connectOnPageLoad,
|
|
186
|
+
initializeOnInteraction,
|
|
187
|
+
normalized.domain,
|
|
188
|
+
normalized.token,
|
|
189
|
+
shouldStart
|
|
190
|
+
]);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const api = apiRef.current;
|
|
193
|
+
if (!api) return;
|
|
194
|
+
if (Array.isArray(tags) && tags.length) api.setTags?.(tags);
|
|
195
|
+
if (contactFields && Object.keys(contactFields).length) api.setContactFields?.(contactFields);
|
|
196
|
+
if (conversationFields && Object.keys(conversationFields).length) api.setConversationFields?.(conversationFields);
|
|
197
|
+
}, [
|
|
198
|
+
tags,
|
|
199
|
+
contactFields,
|
|
200
|
+
conversationFields
|
|
201
|
+
]);
|
|
202
|
+
if (!normalized.domain || !normalized.token) return null;
|
|
203
|
+
return /* @__PURE__ */ jsx("div", { "data-sd-chat-widget-container": "" });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/SparrowDeskProvider.tsx
|
|
208
|
+
const SparrowDeskContext = React.createContext(null);
|
|
209
|
+
function useSparrowDesk() {
|
|
210
|
+
const value = React.useContext(SparrowDeskContext);
|
|
211
|
+
if (!value) throw new Error("useSparrowDesk must be used within <SparrowDeskProvider />");
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function SparrowDeskProvider({ domain, token, children, shouldInitialize = true, connectOnPageLoad = true, initializeOnInteraction = true, tags, contactFields, conversationFields, onReady, onOpen, onClose, openOnInit = false, hideOnInit = false, cleanupOnUnmount = false, readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS }) {
|
|
215
|
+
const normalized = useMemo(() => {
|
|
216
|
+
return {
|
|
217
|
+
domain: normalizeRequired(domain),
|
|
218
|
+
token: normalizeRequired(token)
|
|
219
|
+
};
|
|
220
|
+
}, [domain, token]);
|
|
221
|
+
const onReadyRef = useLatest(onReady);
|
|
222
|
+
const onOpenRef = useLatest(onOpen);
|
|
223
|
+
const onCloseRef = useLatest(onClose);
|
|
224
|
+
const tagsRef = useLatest(tags);
|
|
225
|
+
const contactFieldsRef = useLatest(contactFields);
|
|
226
|
+
const conversationFieldsRef = useLatest(conversationFields);
|
|
227
|
+
const openOnInitRef = useLatest(openOnInit);
|
|
228
|
+
const hideOnInitRef = useLatest(hideOnInit);
|
|
229
|
+
const apiRef = useRef(null);
|
|
230
|
+
const registeredCallbacksRef = useRef(false);
|
|
231
|
+
const didOpenOnceRef = useRef(false);
|
|
232
|
+
const didHideOnceRef = useRef(false);
|
|
233
|
+
const scriptHandleRef = useRef(null);
|
|
234
|
+
const initStartedRef = useRef(false);
|
|
235
|
+
const initCancelRef = useRef(null);
|
|
236
|
+
const pendingCallsRef = useRef([]);
|
|
237
|
+
const [isReady, setIsReady] = useState(false);
|
|
238
|
+
const [shouldStart, setShouldStart] = useState(connectOnPageLoad);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
setShouldStart(connectOnPageLoad);
|
|
241
|
+
}, [connectOnPageLoad]);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
didOpenOnceRef.current = false;
|
|
244
|
+
didHideOnceRef.current = false;
|
|
245
|
+
apiRef.current = null;
|
|
246
|
+
registeredCallbacksRef.current = false;
|
|
247
|
+
initStartedRef.current = false;
|
|
248
|
+
initCancelRef.current?.();
|
|
249
|
+
initCancelRef.current = null;
|
|
250
|
+
pendingCallsRef.current = [];
|
|
251
|
+
scriptHandleRef.current?.release();
|
|
252
|
+
scriptHandleRef.current = null;
|
|
253
|
+
setIsReady(false);
|
|
254
|
+
setShouldStart(connectOnPageLoad);
|
|
255
|
+
}, [
|
|
256
|
+
normalized.domain,
|
|
257
|
+
normalized.token,
|
|
258
|
+
connectOnPageLoad
|
|
259
|
+
]);
|
|
260
|
+
const initialize = React.useCallback(() => {
|
|
261
|
+
if (!isBrowser()) return;
|
|
262
|
+
if (!normalized.domain || !normalized.token) return;
|
|
263
|
+
setWidgetGlobals(normalized.domain, normalized.token);
|
|
264
|
+
if (shouldInitialize && !scriptHandleRef.current) {
|
|
265
|
+
removeOtherWidgetScripts(DEFAULT_SCRIPT_SRC);
|
|
266
|
+
scriptHandleRef.current = acquireWidgetScript(DEFAULT_SCRIPT_SRC, cleanupOnUnmount);
|
|
267
|
+
}
|
|
268
|
+
if (initStartedRef.current) return;
|
|
269
|
+
initStartedRef.current = true;
|
|
270
|
+
let cancelled = false;
|
|
271
|
+
initCancelRef.current = () => {
|
|
272
|
+
cancelled = true;
|
|
273
|
+
};
|
|
274
|
+
(async () => {
|
|
275
|
+
const api = await waitForSparrowDeskApi(readyTimeoutMs);
|
|
276
|
+
if (cancelled || !api) return;
|
|
277
|
+
apiRef.current = api;
|
|
278
|
+
setIsReady(true);
|
|
279
|
+
if (!registeredCallbacksRef.current) {
|
|
280
|
+
api.onOpen?.(() => onOpenRef.current?.());
|
|
281
|
+
api.onClose?.(() => onCloseRef.current?.());
|
|
282
|
+
registeredCallbacksRef.current = true;
|
|
283
|
+
}
|
|
284
|
+
onReadyRef.current?.(api);
|
|
285
|
+
const latestTags = tagsRef.current;
|
|
286
|
+
const latestContactFields = contactFieldsRef.current;
|
|
287
|
+
const latestConversationFields = conversationFieldsRef.current;
|
|
288
|
+
if (Array.isArray(latestTags) && latestTags.length) api.setTags?.(latestTags);
|
|
289
|
+
if (latestContactFields && Object.keys(latestContactFields).length) api.setContactFields?.(latestContactFields);
|
|
290
|
+
if (latestConversationFields && Object.keys(latestConversationFields).length) api.setConversationFields?.(latestConversationFields);
|
|
291
|
+
if (hideOnInitRef.current && !didHideOnceRef.current) {
|
|
292
|
+
api.hideWidget?.();
|
|
293
|
+
didHideOnceRef.current = true;
|
|
294
|
+
}
|
|
295
|
+
if (openOnInitRef.current && !didOpenOnceRef.current) {
|
|
296
|
+
api.openWidget?.();
|
|
297
|
+
didOpenOnceRef.current = true;
|
|
298
|
+
}
|
|
299
|
+
const pending = pendingCallsRef.current;
|
|
300
|
+
pendingCallsRef.current = [];
|
|
301
|
+
pending.forEach((fn) => fn(api));
|
|
302
|
+
})();
|
|
303
|
+
}, [
|
|
304
|
+
cleanupOnUnmount,
|
|
305
|
+
normalized.domain,
|
|
306
|
+
normalized.token,
|
|
307
|
+
readyTimeoutMs,
|
|
308
|
+
shouldInitialize
|
|
309
|
+
]);
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!isBrowser()) return;
|
|
312
|
+
if (!normalized.domain || !normalized.token) return;
|
|
313
|
+
setWidgetGlobals(normalized.domain, normalized.token);
|
|
314
|
+
if (!shouldStart) return;
|
|
315
|
+
initialize();
|
|
316
|
+
return () => {
|
|
317
|
+
initCancelRef.current?.();
|
|
318
|
+
initCancelRef.current = null;
|
|
319
|
+
if (!apiRef.current) initStartedRef.current = false;
|
|
320
|
+
scriptHandleRef.current?.release();
|
|
321
|
+
scriptHandleRef.current = null;
|
|
322
|
+
};
|
|
323
|
+
}, [
|
|
324
|
+
initialize,
|
|
325
|
+
normalized.domain,
|
|
326
|
+
normalized.token,
|
|
327
|
+
shouldStart
|
|
328
|
+
]);
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (!isBrowser()) return;
|
|
331
|
+
if (connectOnPageLoad) return;
|
|
332
|
+
if (!initializeOnInteraction) return;
|
|
333
|
+
if (shouldStart) return;
|
|
334
|
+
if (!normalized.domain || !normalized.token) return;
|
|
335
|
+
const onFirstInteraction = () => {
|
|
336
|
+
setShouldStart(true);
|
|
337
|
+
cleanup();
|
|
338
|
+
};
|
|
339
|
+
const cleanup = () => {
|
|
340
|
+
document.removeEventListener("pointerdown", onFirstInteraction, true);
|
|
341
|
+
document.removeEventListener("keydown", onFirstInteraction, true);
|
|
342
|
+
};
|
|
343
|
+
document.addEventListener("pointerdown", onFirstInteraction, true);
|
|
344
|
+
document.addEventListener("keydown", onFirstInteraction, true);
|
|
345
|
+
return cleanup;
|
|
346
|
+
}, [
|
|
347
|
+
connectOnPageLoad,
|
|
348
|
+
initializeOnInteraction,
|
|
349
|
+
normalized.domain,
|
|
350
|
+
normalized.token,
|
|
351
|
+
shouldStart
|
|
352
|
+
]);
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
const api = apiRef.current;
|
|
355
|
+
if (!api) return;
|
|
356
|
+
if (Array.isArray(tags) && tags.length) api.setTags?.(tags);
|
|
357
|
+
if (contactFields && Object.keys(contactFields).length) api.setContactFields?.(contactFields);
|
|
358
|
+
if (conversationFields && Object.keys(conversationFields).length) api.setConversationFields?.(conversationFields);
|
|
359
|
+
}, [
|
|
360
|
+
tags,
|
|
361
|
+
contactFields,
|
|
362
|
+
conversationFields
|
|
363
|
+
]);
|
|
364
|
+
const methods = useMemo(() => {
|
|
365
|
+
const callOrQueue = (fn) => {
|
|
366
|
+
const api = apiRef.current;
|
|
367
|
+
if (api) {
|
|
368
|
+
fn(api);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!connectOnPageLoad) {
|
|
372
|
+
initialize();
|
|
373
|
+
pendingCallsRef.current.push(fn);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
initialize,
|
|
378
|
+
openWidget: () => callOrQueue((api) => api.openWidget?.()),
|
|
379
|
+
closeWidget: () => callOrQueue((api) => api.closeWidget?.()),
|
|
380
|
+
hideWidget: () => callOrQueue((api) => api.hideWidget?.()),
|
|
381
|
+
setTags: (t) => callOrQueue((api) => api.setTags?.(t)),
|
|
382
|
+
setContactFields: (f) => callOrQueue((api) => api.setContactFields?.(f)),
|
|
383
|
+
setConversationFields: (f) => callOrQueue((api) => api.setConversationFields?.(f))
|
|
384
|
+
};
|
|
385
|
+
}, [connectOnPageLoad, initialize]);
|
|
386
|
+
const value = useMemo(() => {
|
|
387
|
+
return {
|
|
388
|
+
...methods,
|
|
389
|
+
isReady,
|
|
390
|
+
api: apiRef.current
|
|
391
|
+
};
|
|
392
|
+
}, [methods, isReady]);
|
|
393
|
+
return /* @__PURE__ */ jsx(SparrowDeskContext.Provider, {
|
|
394
|
+
value,
|
|
395
|
+
children
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
export { Chat, SparrowDeskProvider, useSparrowDesk };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sparrowdesk/react-chat",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "SparrowDesk Chat Widget for React.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sparrowdesk",
|
|
8
|
+
"chat",
|
|
9
|
+
"chat-widget",
|
|
10
|
+
"widget",
|
|
11
|
+
"support",
|
|
12
|
+
"customer-support",
|
|
13
|
+
"react"
|
|
14
|
+
],
|
|
15
|
+
"author": "SparrowDesk",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/sparrowdesk/sd-chat-widget-react#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/sparrowdesk/sd-chat-widget-react.git",
|
|
21
|
+
"directory": "packages/react-chat"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/sparrowdesk/sd-chat-widget-react/issues"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./dist/index.js",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"dev": "tsdown --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"release": "bumpp && pnpm publish",
|
|
47
|
+
"prepublishOnly": "pnpm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@tsconfig/strictest": "^2.0.8",
|
|
55
|
+
"@types/node": "^25.0.3",
|
|
56
|
+
"@types/react": "^19.2.7",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"@vitest/browser-playwright": "^4.0.16",
|
|
59
|
+
"bumpp": "^10.3.2",
|
|
60
|
+
"playwright": "^1.57.0",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"react-dom": "^19.2.0",
|
|
63
|
+
"tsdown": "^0.18.1",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.0.16",
|
|
66
|
+
"vitest-browser-react": "^2.0.2"
|
|
67
|
+
}
|
|
68
|
+
}
|