@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 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 createPopup = react.useCallback(() => {
63
- if (handleRef.current) return handleRef.current;
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
- handleRef.current = newHandle;
77
- setHandle(newHandle);
78
- return newHandle;
79
- }, [
80
- researchId,
81
- params,
82
- brand,
83
- theme,
84
- host,
85
- stableOnReady,
86
- stableOnSubmit,
87
- stableOnNavigate,
88
- stableOnClose,
89
- stableOnError
90
- ]);
91
- const destroyPopup = react.useCallback(() => {
92
- if (handleRef.current) {
93
- handleRef.current.destroy();
94
- handleRef.current = null;
95
- setHandle(null);
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
- createPopup();
160
+ ensurePopup().show();
103
161
  setInternalOpen(true);
104
162
  }
105
- }, [isControlled, onOpenChange, createPopup]);
163
+ }, [ensurePopup, isControlled, onOpenChange]);
106
164
  const closeFn = react.useCallback(() => {
107
165
  if (isControlled) {
108
166
  onOpenChange?.(false);
109
167
  } else {
110
- destroyPopup();
168
+ handleRef.current?.hide();
111
169
  setInternalOpen(false);
112
170
  }
113
- }, [isControlled, onOpenChange, destroyPopup]);
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 createSlider = react.useCallback(() => {
187
- if (handleRef.current) return handleRef.current;
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
- handleRef.current = newHandle;
201
- setHandle(newHandle);
202
- return newHandle;
203
- }, [
204
- researchId,
205
- params,
206
- brand,
207
- theme,
208
- host,
209
- stableOnReady,
210
- stableOnSubmit,
211
- stableOnNavigate,
212
- stableOnClose,
213
- stableOnError
214
- ]);
215
- const destroySlider = react.useCallback(() => {
216
- if (handleRef.current) {
217
- handleRef.current.destroy();
218
- handleRef.current = null;
219
- setHandle(null);
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
- createSlider();
304
+ ensureSlider().show();
227
305
  setInternalOpen(true);
228
306
  }
229
- }, [isControlled, onOpenChange, createSlider]);
307
+ }, [ensureSlider, isControlled, onOpenChange]);
230
308
  const closeFn = react.useCallback(() => {
231
309
  if (isControlled) {
232
310
  onOpenChange?.(false);
233
311
  } else {
234
- destroySlider();
312
+ handleRef.current?.hide();
235
313
  setInternalOpen(false);
236
314
  }
237
- }, [isControlled, onOpenChange, destroySlider]);
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
- params,
325
- brand,
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 { trigger, showOnce = "session", researchId, ...embedConfig } = options;
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({ researchId, ...embedConfig });
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") {