@perspective-ai/sdk-react 1.3.0 → 1.4.0-pr-25-20260306102715
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/dist/index.cjs +224 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +225 -108
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/useAutoOpen.test.ts +110 -13
- package/src/hooks/useAutoOpen.ts +63 -4
- package/src/hooks/useFloatBubble.ts +8 -4
- package/src/hooks/usePopup.test.ts +224 -55
- package/src/hooks/usePopup.ts +93 -62
- package/src/hooks/useSlider.test.ts +274 -41
- package/src/hooks/useSlider.ts +93 -62
- package/src/hooks/useStableValue.test.ts +108 -0
- package/src/hooks/useStableValue.ts +34 -0
package/dist/index.cjs
CHANGED
|
@@ -16,6 +16,28 @@ function useStableCallback(callback) {
|
|
|
16
16
|
);
|
|
17
17
|
return callback ? stable : callback;
|
|
18
18
|
}
|
|
19
|
+
function sortObjectKeys(value) {
|
|
20
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
21
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
22
|
+
result[key] = value[key];
|
|
23
|
+
return result;
|
|
24
|
+
}, {});
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function useStableValue(value) {
|
|
29
|
+
const ref = react.useRef(value);
|
|
30
|
+
const serialized = JSON.stringify(
|
|
31
|
+
value,
|
|
32
|
+
(_key, currentValue) => sortObjectKeys(currentValue)
|
|
33
|
+
);
|
|
34
|
+
const prevSerialized = react.useRef(serialized);
|
|
35
|
+
if (prevSerialized.current !== serialized) {
|
|
36
|
+
prevSerialized.current = serialized;
|
|
37
|
+
ref.current = value;
|
|
38
|
+
}
|
|
39
|
+
return ref.current;
|
|
40
|
+
}
|
|
19
41
|
|
|
20
42
|
// src/hooks/usePopup.ts
|
|
21
43
|
function usePopup(options) {
|
|
@@ -38,6 +60,9 @@ function usePopup(options) {
|
|
|
38
60
|
const handleRef = react.useRef(null);
|
|
39
61
|
const isControlled = controlledOpen !== void 0;
|
|
40
62
|
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
63
|
+
const initialHiddenRef = react.useRef(!isOpen);
|
|
64
|
+
const stableParams = useStableValue(params);
|
|
65
|
+
const stableBrand = useStableValue(brand);
|
|
41
66
|
const stableOnReady = useStableCallback(onReady);
|
|
42
67
|
const stableOnSubmit = useStableCallback(onSubmit);
|
|
43
68
|
const stableOnNavigate = useStableCallback(onNavigate);
|
|
@@ -52,65 +77,98 @@ function usePopup(options) {
|
|
|
52
77
|
},
|
|
53
78
|
[isControlled, onOpenChange]
|
|
54
79
|
);
|
|
80
|
+
const destroyPopup = react.useCallback(
|
|
81
|
+
(targetHandle = handleRef.current) => {
|
|
82
|
+
if (!targetHandle) return;
|
|
83
|
+
targetHandle.update({ onClose: void 0 });
|
|
84
|
+
targetHandle.destroy();
|
|
85
|
+
if (handleRef.current === targetHandle) {
|
|
86
|
+
handleRef.current = null;
|
|
87
|
+
setHandle(null);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[]
|
|
91
|
+
);
|
|
55
92
|
const handleClose = react.useCallback(() => {
|
|
56
|
-
handleRef.current = null;
|
|
57
|
-
setHandle(null);
|
|
58
93
|
setOpen(false);
|
|
59
94
|
onClose?.();
|
|
60
95
|
}, [setOpen, onClose]);
|
|
61
96
|
const stableOnClose = useStableCallback(handleClose);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const newHandle = sdk.openPopup({
|
|
97
|
+
const createPopupHandle = react.useCallback(
|
|
98
|
+
(startHidden) => sdk.openPopup({
|
|
65
99
|
researchId,
|
|
66
|
-
params,
|
|
67
|
-
brand,
|
|
100
|
+
params: stableParams,
|
|
101
|
+
brand: stableBrand,
|
|
68
102
|
theme,
|
|
69
103
|
host,
|
|
70
104
|
onReady: stableOnReady,
|
|
71
105
|
onSubmit: stableOnSubmit,
|
|
72
106
|
onNavigate: stableOnNavigate,
|
|
73
107
|
onClose: stableOnClose,
|
|
74
|
-
onError: stableOnError
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
handleRef.current
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
onError: stableOnError,
|
|
109
|
+
_startHidden: startHidden
|
|
110
|
+
}),
|
|
111
|
+
[
|
|
112
|
+
researchId,
|
|
113
|
+
stableParams,
|
|
114
|
+
stableBrand,
|
|
115
|
+
theme,
|
|
116
|
+
host,
|
|
117
|
+
stableOnReady,
|
|
118
|
+
stableOnSubmit,
|
|
119
|
+
stableOnNavigate,
|
|
120
|
+
stableOnClose,
|
|
121
|
+
stableOnError
|
|
122
|
+
]
|
|
123
|
+
);
|
|
124
|
+
const setPopupHandle = react.useCallback(
|
|
125
|
+
(startHidden) => {
|
|
126
|
+
const newHandle = createPopupHandle(startHidden);
|
|
127
|
+
handleRef.current = newHandle;
|
|
128
|
+
setHandle(newHandle);
|
|
129
|
+
return newHandle;
|
|
130
|
+
},
|
|
131
|
+
[createPopupHandle]
|
|
132
|
+
);
|
|
133
|
+
react.useEffect(() => {
|
|
134
|
+
const currentHandle = handleRef.current;
|
|
135
|
+
if (!currentHandle) return;
|
|
136
|
+
if (isOpen) {
|
|
137
|
+
currentHandle.show();
|
|
138
|
+
} else {
|
|
139
|
+
currentHandle.hide();
|
|
96
140
|
}
|
|
97
|
-
}, []);
|
|
141
|
+
}, [isOpen]);
|
|
142
|
+
react.useEffect(() => {
|
|
143
|
+
const nextHandle = setPopupHandle(
|
|
144
|
+
handleRef.current ? !handleRef.current.isOpen : initialHiddenRef.current
|
|
145
|
+
);
|
|
146
|
+
return () => {
|
|
147
|
+
if (handleRef.current === nextHandle) {
|
|
148
|
+
destroyPopup(nextHandle);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}, [destroyPopup, setPopupHandle]);
|
|
152
|
+
const ensurePopup = react.useCallback(() => {
|
|
153
|
+
if (handleRef.current) return handleRef.current;
|
|
154
|
+
return setPopupHandle(false);
|
|
155
|
+
}, [setPopupHandle]);
|
|
98
156
|
const openFn = react.useCallback(() => {
|
|
99
157
|
if (isControlled) {
|
|
100
158
|
onOpenChange?.(true);
|
|
101
159
|
} else {
|
|
102
|
-
|
|
160
|
+
ensurePopup().show();
|
|
103
161
|
setInternalOpen(true);
|
|
104
162
|
}
|
|
105
|
-
}, [isControlled, onOpenChange
|
|
163
|
+
}, [ensurePopup, isControlled, onOpenChange]);
|
|
106
164
|
const closeFn = react.useCallback(() => {
|
|
107
165
|
if (isControlled) {
|
|
108
166
|
onOpenChange?.(false);
|
|
109
167
|
} else {
|
|
110
|
-
|
|
168
|
+
handleRef.current?.hide();
|
|
111
169
|
setInternalOpen(false);
|
|
112
170
|
}
|
|
113
|
-
}, [isControlled, onOpenChange
|
|
171
|
+
}, [isControlled, onOpenChange]);
|
|
114
172
|
const toggleFn = react.useCallback(() => {
|
|
115
173
|
if (isOpen) {
|
|
116
174
|
closeFn();
|
|
@@ -118,22 +176,6 @@ function usePopup(options) {
|
|
|
118
176
|
openFn();
|
|
119
177
|
}
|
|
120
178
|
}, [isOpen, openFn, closeFn]);
|
|
121
|
-
react.useEffect(() => {
|
|
122
|
-
if (!isControlled) return;
|
|
123
|
-
if (controlledOpen && !handleRef.current) {
|
|
124
|
-
createPopup();
|
|
125
|
-
} else if (!controlledOpen && handleRef.current) {
|
|
126
|
-
destroyPopup();
|
|
127
|
-
}
|
|
128
|
-
}, [controlledOpen, isControlled, createPopup, destroyPopup]);
|
|
129
|
-
react.useEffect(() => {
|
|
130
|
-
return () => {
|
|
131
|
-
if (handleRef.current) {
|
|
132
|
-
handleRef.current.destroy();
|
|
133
|
-
handleRef.current = null;
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}, []);
|
|
137
179
|
return {
|
|
138
180
|
open: openFn,
|
|
139
181
|
close: closeFn,
|
|
@@ -162,6 +204,9 @@ function useSlider(options) {
|
|
|
162
204
|
const handleRef = react.useRef(null);
|
|
163
205
|
const isControlled = controlledOpen !== void 0;
|
|
164
206
|
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
207
|
+
const initialHiddenRef = react.useRef(!isOpen);
|
|
208
|
+
const stableParams = useStableValue(params);
|
|
209
|
+
const stableBrand = useStableValue(brand);
|
|
165
210
|
const stableOnReady = useStableCallback(onReady);
|
|
166
211
|
const stableOnSubmit = useStableCallback(onSubmit);
|
|
167
212
|
const stableOnNavigate = useStableCallback(onNavigate);
|
|
@@ -176,65 +221,98 @@ function useSlider(options) {
|
|
|
176
221
|
},
|
|
177
222
|
[isControlled, onOpenChange]
|
|
178
223
|
);
|
|
224
|
+
const destroySlider = react.useCallback(
|
|
225
|
+
(targetHandle = handleRef.current) => {
|
|
226
|
+
if (!targetHandle) return;
|
|
227
|
+
targetHandle.update({ onClose: void 0 });
|
|
228
|
+
targetHandle.destroy();
|
|
229
|
+
if (handleRef.current === targetHandle) {
|
|
230
|
+
handleRef.current = null;
|
|
231
|
+
setHandle(null);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[]
|
|
235
|
+
);
|
|
179
236
|
const handleClose = react.useCallback(() => {
|
|
180
|
-
handleRef.current = null;
|
|
181
|
-
setHandle(null);
|
|
182
237
|
setOpen(false);
|
|
183
238
|
onClose?.();
|
|
184
239
|
}, [setOpen, onClose]);
|
|
185
240
|
const stableOnClose = useStableCallback(handleClose);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const newHandle = sdk.openSlider({
|
|
241
|
+
const createSliderHandle = react.useCallback(
|
|
242
|
+
(startHidden) => sdk.openSlider({
|
|
189
243
|
researchId,
|
|
190
|
-
params,
|
|
191
|
-
brand,
|
|
244
|
+
params: stableParams,
|
|
245
|
+
brand: stableBrand,
|
|
192
246
|
theme,
|
|
193
247
|
host,
|
|
194
248
|
onReady: stableOnReady,
|
|
195
249
|
onSubmit: stableOnSubmit,
|
|
196
250
|
onNavigate: stableOnNavigate,
|
|
197
251
|
onClose: stableOnClose,
|
|
198
|
-
onError: stableOnError
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
handleRef.current
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
onError: stableOnError,
|
|
253
|
+
_startHidden: startHidden
|
|
254
|
+
}),
|
|
255
|
+
[
|
|
256
|
+
researchId,
|
|
257
|
+
stableParams,
|
|
258
|
+
stableBrand,
|
|
259
|
+
theme,
|
|
260
|
+
host,
|
|
261
|
+
stableOnReady,
|
|
262
|
+
stableOnSubmit,
|
|
263
|
+
stableOnNavigate,
|
|
264
|
+
stableOnClose,
|
|
265
|
+
stableOnError
|
|
266
|
+
]
|
|
267
|
+
);
|
|
268
|
+
const setSliderHandle = react.useCallback(
|
|
269
|
+
(startHidden) => {
|
|
270
|
+
const newHandle = createSliderHandle(startHidden);
|
|
271
|
+
handleRef.current = newHandle;
|
|
272
|
+
setHandle(newHandle);
|
|
273
|
+
return newHandle;
|
|
274
|
+
},
|
|
275
|
+
[createSliderHandle]
|
|
276
|
+
);
|
|
277
|
+
react.useEffect(() => {
|
|
278
|
+
const currentHandle = handleRef.current;
|
|
279
|
+
if (!currentHandle) return;
|
|
280
|
+
if (isOpen) {
|
|
281
|
+
currentHandle.show();
|
|
282
|
+
} else {
|
|
283
|
+
currentHandle.hide();
|
|
220
284
|
}
|
|
221
|
-
}, []);
|
|
285
|
+
}, [isOpen]);
|
|
286
|
+
react.useEffect(() => {
|
|
287
|
+
const nextHandle = setSliderHandle(
|
|
288
|
+
handleRef.current ? !handleRef.current.isOpen : initialHiddenRef.current
|
|
289
|
+
);
|
|
290
|
+
return () => {
|
|
291
|
+
if (handleRef.current === nextHandle) {
|
|
292
|
+
destroySlider(nextHandle);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}, [destroySlider, setSliderHandle]);
|
|
296
|
+
const ensureSlider = react.useCallback(() => {
|
|
297
|
+
if (handleRef.current) return handleRef.current;
|
|
298
|
+
return setSliderHandle(false);
|
|
299
|
+
}, [setSliderHandle]);
|
|
222
300
|
const openFn = react.useCallback(() => {
|
|
223
301
|
if (isControlled) {
|
|
224
302
|
onOpenChange?.(true);
|
|
225
303
|
} else {
|
|
226
|
-
|
|
304
|
+
ensureSlider().show();
|
|
227
305
|
setInternalOpen(true);
|
|
228
306
|
}
|
|
229
|
-
}, [isControlled, onOpenChange
|
|
307
|
+
}, [ensureSlider, isControlled, onOpenChange]);
|
|
230
308
|
const closeFn = react.useCallback(() => {
|
|
231
309
|
if (isControlled) {
|
|
232
310
|
onOpenChange?.(false);
|
|
233
311
|
} else {
|
|
234
|
-
|
|
312
|
+
handleRef.current?.hide();
|
|
235
313
|
setInternalOpen(false);
|
|
236
314
|
}
|
|
237
|
-
}, [isControlled, onOpenChange
|
|
315
|
+
}, [isControlled, onOpenChange]);
|
|
238
316
|
const toggleFn = react.useCallback(() => {
|
|
239
317
|
if (isOpen) {
|
|
240
318
|
closeFn();
|
|
@@ -242,22 +320,6 @@ function useSlider(options) {
|
|
|
242
320
|
openFn();
|
|
243
321
|
}
|
|
244
322
|
}, [isOpen, openFn, closeFn]);
|
|
245
|
-
react.useEffect(() => {
|
|
246
|
-
if (!isControlled) return;
|
|
247
|
-
if (controlledOpen && !handleRef.current) {
|
|
248
|
-
createSlider();
|
|
249
|
-
} else if (!controlledOpen && handleRef.current) {
|
|
250
|
-
destroySlider();
|
|
251
|
-
}
|
|
252
|
-
}, [controlledOpen, isControlled, createSlider, destroySlider]);
|
|
253
|
-
react.useEffect(() => {
|
|
254
|
-
return () => {
|
|
255
|
-
if (handleRef.current) {
|
|
256
|
-
handleRef.current.destroy();
|
|
257
|
-
handleRef.current = null;
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
}, []);
|
|
261
323
|
return {
|
|
262
324
|
open: openFn,
|
|
263
325
|
close: closeFn,
|
|
@@ -285,6 +347,8 @@ function useFloatBubble(options) {
|
|
|
285
347
|
const [internalOpen, setInternalOpen] = react.useState(false);
|
|
286
348
|
const handleRef = react.useRef(null);
|
|
287
349
|
const isControlled = controlledOpen !== void 0;
|
|
350
|
+
const stableParams = useStableValue(params);
|
|
351
|
+
const stableBrand = useStableValue(brand);
|
|
288
352
|
const stableOnReady = useStableCallback(onReady);
|
|
289
353
|
const stableOnSubmit = useStableCallback(onSubmit);
|
|
290
354
|
const stableOnNavigate = useStableCallback(onNavigate);
|
|
@@ -300,8 +364,8 @@ function useFloatBubble(options) {
|
|
|
300
364
|
react.useEffect(() => {
|
|
301
365
|
const newHandle = sdk.createFloatBubble({
|
|
302
366
|
researchId,
|
|
303
|
-
params,
|
|
304
|
-
brand,
|
|
367
|
+
params: stableParams,
|
|
368
|
+
brand: stableBrand,
|
|
305
369
|
theme,
|
|
306
370
|
host,
|
|
307
371
|
onReady: stableOnReady,
|
|
@@ -321,8 +385,8 @@ function useFloatBubble(options) {
|
|
|
321
385
|
};
|
|
322
386
|
}, [
|
|
323
387
|
researchId,
|
|
324
|
-
|
|
325
|
-
|
|
388
|
+
stableParams,
|
|
389
|
+
stableBrand,
|
|
326
390
|
theme,
|
|
327
391
|
host,
|
|
328
392
|
stableOnReady,
|
|
@@ -380,14 +444,66 @@ function useFloatBubble(options) {
|
|
|
380
444
|
};
|
|
381
445
|
}
|
|
382
446
|
function useAutoOpen(options) {
|
|
383
|
-
const {
|
|
447
|
+
const {
|
|
448
|
+
trigger,
|
|
449
|
+
showOnce = "session",
|
|
450
|
+
researchId,
|
|
451
|
+
params,
|
|
452
|
+
brand,
|
|
453
|
+
theme,
|
|
454
|
+
host,
|
|
455
|
+
onReady,
|
|
456
|
+
onSubmit,
|
|
457
|
+
onNavigate,
|
|
458
|
+
onClose,
|
|
459
|
+
onError,
|
|
460
|
+
onAuth,
|
|
461
|
+
channel,
|
|
462
|
+
welcomeMessage
|
|
463
|
+
} = options;
|
|
384
464
|
const cleanupRef = react.useRef(null);
|
|
385
465
|
const [triggered, setTriggered] = react.useState(false);
|
|
386
466
|
const triggerDelay = trigger.type === "timeout" ? trigger.delay : void 0;
|
|
467
|
+
const stableParams = useStableValue(params);
|
|
468
|
+
const stableBrand = useStableValue(brand);
|
|
469
|
+
const stableOnReady = useStableCallback(onReady);
|
|
470
|
+
const stableOnSubmit = useStableCallback(onSubmit);
|
|
471
|
+
const stableOnNavigate = useStableCallback(onNavigate);
|
|
472
|
+
const stableOnClose = useStableCallback(onClose);
|
|
473
|
+
const stableOnError = useStableCallback(onError);
|
|
474
|
+
const stableOnAuth = useStableCallback(onAuth);
|
|
387
475
|
const stableOnTrigger = useStableCallback(() => {
|
|
388
476
|
sdk.markShown(researchId, showOnce);
|
|
389
|
-
sdk.openPopup({
|
|
477
|
+
sdk.openPopup({
|
|
478
|
+
researchId,
|
|
479
|
+
params: stableParams,
|
|
480
|
+
brand: stableBrand,
|
|
481
|
+
theme,
|
|
482
|
+
host,
|
|
483
|
+
onReady: stableOnReady,
|
|
484
|
+
onSubmit: stableOnSubmit,
|
|
485
|
+
onNavigate: stableOnNavigate,
|
|
486
|
+
onClose: stableOnClose,
|
|
487
|
+
onError: stableOnError,
|
|
488
|
+
onAuth: stableOnAuth,
|
|
489
|
+
channel,
|
|
490
|
+
welcomeMessage
|
|
491
|
+
});
|
|
390
492
|
});
|
|
493
|
+
react.useEffect(() => {
|
|
494
|
+
if (triggered || !sdk.shouldShow(researchId, showOnce)) return;
|
|
495
|
+
sdk.preloadIframe(
|
|
496
|
+
researchId,
|
|
497
|
+
"popup",
|
|
498
|
+
sdk.getHost(host),
|
|
499
|
+
stableParams,
|
|
500
|
+
stableBrand,
|
|
501
|
+
theme
|
|
502
|
+
);
|
|
503
|
+
return () => {
|
|
504
|
+
sdk.destroyPreloadedByType(researchId, "popup");
|
|
505
|
+
};
|
|
506
|
+
}, [researchId, showOnce, host, stableParams, stableBrand, theme, triggered]);
|
|
391
507
|
react.useEffect(() => {
|
|
392
508
|
if (!sdk.shouldShow(researchId, showOnce)) return;
|
|
393
509
|
cleanupRef.current = sdk.setupTrigger(trigger, () => {
|
|
@@ -405,7 +521,8 @@ function useAutoOpen(options) {
|
|
|
405
521
|
const cancel = react.useCallback(() => {
|
|
406
522
|
cleanupRef.current?.();
|
|
407
523
|
cleanupRef.current = null;
|
|
408
|
-
|
|
524
|
+
sdk.destroyPreloadedByType(researchId, "popup");
|
|
525
|
+
}, [researchId]);
|
|
409
526
|
return { cancel, triggered };
|
|
410
527
|
}
|
|
411
528
|
function useThemeSync(theme = "system") {
|