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