@buoy-gg/impersonate 2.1.15 → 2.1.16

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.
@@ -52,6 +52,8 @@ function ImpersonateBanner({
52
52
  return null;
53
53
  }
54
54
  const handlePauseResume = async () => {
55
+ // eslint-disable-next-line no-console
56
+ console.log("[impersonate] banner pause/resume pressed");
55
57
  if (state.isPaused) {
56
58
  await _impersonateStore.impersonateStore.resumeImpersonation();
57
59
  } else {
@@ -65,6 +67,8 @@ function ImpersonateBanner({
65
67
  bottom: offset
66
68
  };
67
69
  const handleStop = async () => {
70
+ // eslint-disable-next-line no-console
71
+ console.log("[impersonate] banner STOP pressed");
68
72
  await _impersonateStore.impersonateStore.stopImpersonation();
69
73
  };
70
74
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ImpersonateOverlay = ImpersonateOverlay;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _ImpersonateBanner = require("./ImpersonateBanner");
9
+ var _jsxRuntime = require("react/jsx-runtime");
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ /**
12
+ * ImpersonateOverlay
13
+ *
14
+ * Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
15
+ * `@buoy-gg/impersonate` is installed — the host does NOT need to mount
16
+ * anything. It renders the floating impersonation banner, which self-gates
17
+ * on store state (only visible while an impersonation session is active and
18
+ * `showBanner` is enabled), so it's safe to mount unconditionally.
19
+ */
20
+
21
+ function ImpersonateOverlay() {
22
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateBanner.ImpersonateBanner, {
23
+ position: "top",
24
+ offset: 50
25
+ });
26
+ }
@@ -239,10 +239,10 @@ class ImpersonateStore {
239
239
  this.asyncStorageRef = asyncStorage;
240
240
  try {
241
241
  const stored = await asyncStorage.getItem(STORAGE_KEY);
242
- console.log("[ImpersonateStore] initializeAsync - stored:", stored);
242
+ // eslint-disable-next-line no-console
243
+ console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${stored ? stored.slice(0, 200) : "null"}`);
243
244
  if (stored) {
244
245
  const parsed = JSON.parse(stored);
245
- console.log("[ImpersonateStore] initializeAsync - parsed dataNukeSettings:", parsed.dataNukeSettings);
246
246
  this.state = {
247
247
  ...this.state,
248
248
  headerKey: parsed.headerKey ?? this.state.headerKey,
@@ -258,7 +258,6 @@ class ImpersonateStore {
258
258
  isPaused: parsed.isPaused ?? this.state.isPaused,
259
259
  currentUser: parsed.currentUser ?? this.state.currentUser
260
260
  };
261
- console.log("[ImpersonateStore] initializeAsync - new dataNukeSettings:", this.state.dataNukeSettings);
262
261
 
263
262
  // If we restored an active session, sync with listener
264
263
  if (this.state.isActive && this.state.currentUser) {
@@ -266,8 +265,8 @@ class ImpersonateStore {
266
265
  }
267
266
  this.notify();
268
267
  }
269
- } catch (e) {
270
- console.log("[ImpersonateStore] initializeAsync error:", e);
268
+ } catch {
269
+ // Persisted blob unreadable — fall through with in-memory defaults.
271
270
  }
272
271
  }
273
272
 
@@ -316,29 +315,20 @@ class ImpersonateStore {
316
315
  // ===========================================================================
317
316
 
318
317
  /**
319
- * Start impersonating a user
318
+ * Start impersonating a user.
320
319
  *
321
- * This will:
322
- * 1. Execute data nuking based on settings
323
- * 2. Update state with new user
324
- * 3. Add to history
325
- * 4. Sync with impersonateListener
326
- * 5. Persist settings
320
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
321
+ * so refetches triggered by the cache clear go out with the new user's
322
+ * impersonation header. Doing it the other way around (nuke first) makes
323
+ * react-query refetch active queries with the OLD identity, producing
324
+ * the visual symptom of "I clicked a user and nothing changed."
327
325
  */
328
326
  async startImpersonation(user) {
329
- // Execute data nuking BEFORE switching
330
- await this.executeDataNuke();
331
-
332
- // Create history entry with timestamp
333
327
  const historyEntry = {
334
328
  user,
335
329
  lastUsedAt: new Date().toISOString()
336
330
  };
337
-
338
- // Update history (remove duplicate, add to front)
339
331
  const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
340
-
341
- // Update state
342
332
  this.state = {
343
333
  ...this.state,
344
334
  isActive: true,
@@ -346,44 +336,40 @@ class ImpersonateStore {
346
336
  history: newHistory
347
337
  };
348
338
 
349
- // Sync with listener
339
+ // 1) Point the fetch interceptor at the new user. Any request after this
340
+ // line carries the new header.
350
341
  this.syncWithListener();
351
342
 
352
- // Persist
353
- await this.persist();
354
-
355
- // Notify subscribers
343
+ // 2) Tell React subscribers right away so the UI updates without waiting
344
+ // on AsyncStorage / data nuke.
356
345
  this.notify();
346
+
347
+ // 3) Nuke caches — refetches now use the new identity.
348
+ await this.executeDataNuke();
349
+
350
+ // 4) Persist for next launch.
351
+ await this.persist();
357
352
  }
358
353
 
359
354
  /**
360
- * Stop impersonating
355
+ * Stop impersonating.
361
356
  *
362
- * This will:
363
- * 1. Execute data nuking based on settings
364
- * 2. Clear impersonation state
365
- * 3. Sync with impersonateListener
357
+ * Same ordering rationale as startImpersonation: clear state + listener
358
+ * before nuking caches so subsequent refetches go out without the header.
366
359
  */
367
360
  async stopImpersonation() {
368
- // Execute data nuking
369
- await this.executeDataNuke();
370
-
371
- // Update state
361
+ // eslint-disable-next-line no-console
362
+ console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
372
363
  this.state = {
373
364
  ...this.state,
374
365
  isActive: false,
375
366
  isPaused: false,
376
367
  currentUser: null
377
368
  };
378
-
379
- // Sync with listener
380
369
  this.syncWithListener();
381
-
382
- // Persist
383
- await this.persist();
384
-
385
- // Notify
386
370
  this.notify();
371
+ await this.executeDataNuke();
372
+ await this.persist();
387
373
  }
388
374
 
389
375
  /**
@@ -441,8 +427,6 @@ class ImpersonateStore {
441
427
  * Update settings (header key, ignore patterns, data nuke settings, show banner)
442
428
  */
443
429
  async updateSettings(settings) {
444
- console.log("[ImpersonateStore] updateSettings called with:", settings);
445
- console.log("[ImpersonateStore] Current dataNukeSettings:", this.state.dataNukeSettings);
446
430
  this.state = {
447
431
  ...this.state,
448
432
  headerKey: settings.headerKey ?? this.state.headerKey,
@@ -453,7 +437,6 @@ class ImpersonateStore {
453
437
  ...settings.dataNukeSettings
454
438
  } : this.state.dataNukeSettings
455
439
  };
456
- console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
457
440
 
458
441
  // Sync header key with listener
459
442
  if (settings.headerKey || settings.ignorePatterns) {
@@ -570,19 +553,21 @@ class ImpersonateStore {
570
553
  currentUser: this.state.currentUser
571
554
  };
572
555
  const serialized = JSON.stringify(toStore);
573
- console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
574
556
  try {
575
557
  // Prefer AsyncStorage for React Native
576
558
  if (this.asyncStorageRef) {
577
559
  await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
578
- console.log("[ImpersonateStore] Persisted to AsyncStorage");
560
+ // eslint-disable-next-line no-console
561
+ console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
579
562
  } else {
580
563
  // Fall back to localStorage (web)
581
564
  storage.setItem(STORAGE_KEY, serialized);
582
- console.log("[ImpersonateStore] Persisted to localStorage");
565
+ // eslint-disable-next-line no-console
566
+ console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
583
567
  }
584
568
  } catch (e) {
585
- console.log("[ImpersonateStore] Persist error:", e);
569
+ // eslint-disable-next-line no-console
570
+ console.warn("[impersonate] persist failed", e);
586
571
  }
587
572
  }
588
573
 
@@ -33,6 +33,12 @@ Object.defineProperty(exports, "ImpersonateModal", {
33
33
  return _ImpersonateModal.ImpersonateModal;
34
34
  }
35
35
  });
36
+ Object.defineProperty(exports, "ImpersonateOverlay", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _ImpersonateOverlay.ImpersonateOverlay;
40
+ }
41
+ });
36
42
  Object.defineProperty(exports, "ImpersonateStatusBar", {
37
43
  enumerable: true,
38
44
  get: function () {
@@ -108,6 +114,7 @@ Object.defineProperty(exports, "useImpersonateHistory", {
108
114
  var _preset = require("./preset");
109
115
  var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
110
116
  var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
117
+ var _ImpersonateOverlay = require("./impersonate/components/ImpersonateOverlay");
111
118
  var _UserSearchView = require("./impersonate/components/UserSearchView");
112
119
  var _DataNukeSettings = require("./impersonate/components/DataNukeSettings");
113
120
  var _ImpersonateHistoryList = require("./impersonate/components/ImpersonateHistoryList");
@@ -4,13 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.createImpersonateTool = createImpersonateTool;
7
- var _react = _interopRequireWildcard(require("react"));
7
+ var _react = _interopRequireDefault(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
10
10
  var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
11
11
  var _impersonateStore = require("./impersonate/utils/impersonateStore");
12
+ var _impersonateListener = require("./impersonate/utils/impersonateListener");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
15
  /**
15
16
  * Impersonate Tool Preset
16
17
  *
@@ -18,9 +19,63 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
18
19
  * No default preset is exported because configuration (onSearchUsers) is required.
19
20
  */
20
21
 
21
- // NOTE: The listener is started lazily when the modal opens (via useImpersonate hook)
22
- // This ensures we patch AFTER @buoy-gg/network has patched fetch, so network
23
- // can see our injected headers in its request capture.
22
+ // =============================================================================
23
+ // EAGER BOOTSTRAP
24
+ // =============================================================================
25
+ // Patch fetch/XHR and restore persisted state the moment createImpersonateTool
26
+ // runs (typically at module load in the host app). If we waited until the
27
+ // modal mounts (the original lazy design), every request fired during app
28
+ // boot — bridge polls, user-data fetches, etc. — goes out without the
29
+ // impersonation header, even when the user already had impersonation active
30
+ // from a previous session. React Query then caches the real-account response,
31
+ // so even after the listener does eventually patch, no refetch happens.
32
+ //
33
+ // Side effect: any later fetch interceptor (e.g. @buoy-gg/network) installed
34
+ // AFTER us will wrap our wrapper, so its captured init.headers won't show the
35
+ // impersonation header. The wire request still includes it.
36
+
37
+ let bootstrapStarted = false;
38
+ async function bootstrapImpersonate() {
39
+ if (bootstrapStarted) return;
40
+ bootstrapStarted = true;
41
+
42
+ // eslint-disable-next-line no-console
43
+ console.log("[impersonate] bootstrap: start");
44
+
45
+ // Patch fetch/XHR synchronously so any request between now and the async
46
+ // storage load goes through our wrapper. The wrapper is a no-op until
47
+ // userId is set (which happens once initializeAsync resolves).
48
+ if (!(0, _impersonateListener.impersonateListener)().isListening) {
49
+ (0, _impersonateListener.impersonateListener)().startListening();
50
+ }
51
+
52
+ // React Native: require AsyncStorage. require() is more reliable under
53
+ // Metro than dynamic import(); on web this throws and the store falls
54
+ // back to localStorage handled synchronously in its constructor.
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
57
+ const mod = require("@react-native-async-storage/async-storage");
58
+ const AsyncStorage = mod?.default ?? mod;
59
+ if (!AsyncStorage || typeof AsyncStorage.getItem !== "function") {
60
+ // eslint-disable-next-line no-console
61
+ console.warn("[impersonate] bootstrap: AsyncStorage shape unexpected", Object.keys(mod ?? {}));
62
+ return;
63
+ }
64
+ const adapter = {
65
+ getItem: key => AsyncStorage.getItem(key),
66
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
67
+ };
68
+ _impersonateStore.impersonateStore.setAsyncStorage(adapter);
69
+ // eslint-disable-next-line no-console
70
+ console.log("[impersonate] bootstrap: AsyncStorage adapter installed");
71
+ await _impersonateStore.impersonateStore.initializeAsync(adapter);
72
+ // eslint-disable-next-line no-console
73
+ console.log("[impersonate] bootstrap: initializeAsync done");
74
+ } catch (e) {
75
+ // eslint-disable-next-line no-console
76
+ console.warn("[impersonate] bootstrap: failed", e);
77
+ }
78
+ }
24
79
 
25
80
  // =============================================================================
26
81
  // ICON COMPONENT
@@ -53,43 +108,6 @@ function ImpersonateIcon({
53
108
  // WRAPPER COMPONENT
54
109
  // =============================================================================
55
110
 
56
- /**
57
- * Wrapper component that renders both the modal and the auto-banner.
58
- * The banner handles its own visibility based on store state (isActive + showBanner).
59
- * Clicking the banner opens the modal.
60
- */
61
- function ImpersonateToolWrapper(props) {
62
- const [forceOpen, setForceOpen] = (0, _react.useState)(false);
63
-
64
- // Reset force state when parent closes the modal
65
- (0, _react.useEffect)(() => {
66
- if (!props.visible) {
67
- setForceOpen(false);
68
- }
69
- }, [props.visible]);
70
- const handleBannerPress = () => {
71
- setForceOpen(true);
72
- };
73
- const handleClose = () => {
74
- setForceOpen(false);
75
- // Only notify parent if they had the modal open
76
- if (props.visible) {
77
- props.onClose();
78
- }
79
- };
80
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
81
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateModal.ImpersonateModal, {
82
- ...props,
83
- visible: props.visible || forceOpen,
84
- onClose: handleClose
85
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateBanner.ImpersonateBanner, {
86
- position: "top",
87
- offset: 50,
88
- onPress: handleBannerPress
89
- })]
90
- });
91
- }
92
-
93
111
  // =============================================================================
94
112
  // FACTORY FUNCTION
95
113
  // =============================================================================
@@ -132,8 +150,10 @@ function ImpersonateToolWrapper(props) {
132
150
  * showSettingsTab: true,
133
151
  * });
134
152
  *
135
- * // In your app - banner is automatic, no extra setup needed!
153
+ * // In your app mount the banner globally (outside FloatingDevTools)
154
+ * // so it survives reloads and stays visible after the modal is closed.
136
155
  * <FloatingDevTools apps={[impersonateTool]} />
156
+ * <ImpersonateBanner position="top" offset={50} />
137
157
  * ```
138
158
  */
139
159
  function createImpersonateTool(config) {
@@ -155,6 +175,10 @@ function createImpersonateTool(config) {
155
175
  _impersonateStore.impersonateStore.setDeveloperDefaults(defaults);
156
176
  }
157
177
 
178
+ // Patch fetch and restore persisted impersonation state up-front. Fire and
179
+ // forget — bootstrapImpersonate guards itself against double-invocation.
180
+ void bootstrapImpersonate();
181
+
158
182
  // Create the tool configuration object
159
183
  const tool = {
160
184
  id,
@@ -167,7 +191,7 @@ function createImpersonateTool(config) {
167
191
  size: size,
168
192
  color: "#F59E0B"
169
193
  }),
170
- component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateToolWrapper, {
194
+ component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateModal.ImpersonateModal, {
171
195
  ...props,
172
196
  onSearchUsers: onSearchUsers,
173
197
  onClearReactQuery: onClearReactQuery,
@@ -46,6 +46,8 @@ export function ImpersonateBanner({
46
46
  return null;
47
47
  }
48
48
  const handlePauseResume = async () => {
49
+ // eslint-disable-next-line no-console
50
+ console.log("[impersonate] banner pause/resume pressed");
49
51
  if (state.isPaused) {
50
52
  await impersonateStore.resumeImpersonation();
51
53
  } else {
@@ -59,6 +61,8 @@ export function ImpersonateBanner({
59
61
  bottom: offset
60
62
  };
61
63
  const handleStop = async () => {
64
+ // eslint-disable-next-line no-console
65
+ console.log("[impersonate] banner STOP pressed");
62
66
  await impersonateStore.stopImpersonation();
63
67
  };
64
68
  return /*#__PURE__*/_jsx(View, {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ImpersonateOverlay
5
+ *
6
+ * Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
7
+ * `@buoy-gg/impersonate` is installed — the host does NOT need to mount
8
+ * anything. It renders the floating impersonation banner, which self-gates
9
+ * on store state (only visible while an impersonation session is active and
10
+ * `showBanner` is enabled), so it's safe to mount unconditionally.
11
+ */
12
+
13
+ import React from "react";
14
+ import { ImpersonateBanner } from "./ImpersonateBanner";
15
+ import { jsx as _jsx } from "react/jsx-runtime";
16
+ export function ImpersonateOverlay() {
17
+ return /*#__PURE__*/_jsx(ImpersonateBanner, {
18
+ position: "top",
19
+ offset: 50
20
+ });
21
+ }
@@ -236,10 +236,10 @@ class ImpersonateStore {
236
236
  this.asyncStorageRef = asyncStorage;
237
237
  try {
238
238
  const stored = await asyncStorage.getItem(STORAGE_KEY);
239
- console.log("[ImpersonateStore] initializeAsync - stored:", stored);
239
+ // eslint-disable-next-line no-console
240
+ console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${stored ? stored.slice(0, 200) : "null"}`);
240
241
  if (stored) {
241
242
  const parsed = JSON.parse(stored);
242
- console.log("[ImpersonateStore] initializeAsync - parsed dataNukeSettings:", parsed.dataNukeSettings);
243
243
  this.state = {
244
244
  ...this.state,
245
245
  headerKey: parsed.headerKey ?? this.state.headerKey,
@@ -255,7 +255,6 @@ class ImpersonateStore {
255
255
  isPaused: parsed.isPaused ?? this.state.isPaused,
256
256
  currentUser: parsed.currentUser ?? this.state.currentUser
257
257
  };
258
- console.log("[ImpersonateStore] initializeAsync - new dataNukeSettings:", this.state.dataNukeSettings);
259
258
 
260
259
  // If we restored an active session, sync with listener
261
260
  if (this.state.isActive && this.state.currentUser) {
@@ -263,8 +262,8 @@ class ImpersonateStore {
263
262
  }
264
263
  this.notify();
265
264
  }
266
- } catch (e) {
267
- console.log("[ImpersonateStore] initializeAsync error:", e);
265
+ } catch {
266
+ // Persisted blob unreadable — fall through with in-memory defaults.
268
267
  }
269
268
  }
270
269
 
@@ -313,29 +312,20 @@ class ImpersonateStore {
313
312
  // ===========================================================================
314
313
 
315
314
  /**
316
- * Start impersonating a user
315
+ * Start impersonating a user.
317
316
  *
318
- * This will:
319
- * 1. Execute data nuking based on settings
320
- * 2. Update state with new user
321
- * 3. Add to history
322
- * 4. Sync with impersonateListener
323
- * 5. Persist settings
317
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
318
+ * so refetches triggered by the cache clear go out with the new user's
319
+ * impersonation header. Doing it the other way around (nuke first) makes
320
+ * react-query refetch active queries with the OLD identity, producing
321
+ * the visual symptom of "I clicked a user and nothing changed."
324
322
  */
325
323
  async startImpersonation(user) {
326
- // Execute data nuking BEFORE switching
327
- await this.executeDataNuke();
328
-
329
- // Create history entry with timestamp
330
324
  const historyEntry = {
331
325
  user,
332
326
  lastUsedAt: new Date().toISOString()
333
327
  };
334
-
335
- // Update history (remove duplicate, add to front)
336
328
  const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
337
-
338
- // Update state
339
329
  this.state = {
340
330
  ...this.state,
341
331
  isActive: true,
@@ -343,44 +333,40 @@ class ImpersonateStore {
343
333
  history: newHistory
344
334
  };
345
335
 
346
- // Sync with listener
336
+ // 1) Point the fetch interceptor at the new user. Any request after this
337
+ // line carries the new header.
347
338
  this.syncWithListener();
348
339
 
349
- // Persist
350
- await this.persist();
351
-
352
- // Notify subscribers
340
+ // 2) Tell React subscribers right away so the UI updates without waiting
341
+ // on AsyncStorage / data nuke.
353
342
  this.notify();
343
+
344
+ // 3) Nuke caches — refetches now use the new identity.
345
+ await this.executeDataNuke();
346
+
347
+ // 4) Persist for next launch.
348
+ await this.persist();
354
349
  }
355
350
 
356
351
  /**
357
- * Stop impersonating
352
+ * Stop impersonating.
358
353
  *
359
- * This will:
360
- * 1. Execute data nuking based on settings
361
- * 2. Clear impersonation state
362
- * 3. Sync with impersonateListener
354
+ * Same ordering rationale as startImpersonation: clear state + listener
355
+ * before nuking caches so subsequent refetches go out without the header.
363
356
  */
364
357
  async stopImpersonation() {
365
- // Execute data nuking
366
- await this.executeDataNuke();
367
-
368
- // Update state
358
+ // eslint-disable-next-line no-console
359
+ console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
369
360
  this.state = {
370
361
  ...this.state,
371
362
  isActive: false,
372
363
  isPaused: false,
373
364
  currentUser: null
374
365
  };
375
-
376
- // Sync with listener
377
366
  this.syncWithListener();
378
-
379
- // Persist
380
- await this.persist();
381
-
382
- // Notify
383
367
  this.notify();
368
+ await this.executeDataNuke();
369
+ await this.persist();
384
370
  }
385
371
 
386
372
  /**
@@ -438,8 +424,6 @@ class ImpersonateStore {
438
424
  * Update settings (header key, ignore patterns, data nuke settings, show banner)
439
425
  */
440
426
  async updateSettings(settings) {
441
- console.log("[ImpersonateStore] updateSettings called with:", settings);
442
- console.log("[ImpersonateStore] Current dataNukeSettings:", this.state.dataNukeSettings);
443
427
  this.state = {
444
428
  ...this.state,
445
429
  headerKey: settings.headerKey ?? this.state.headerKey,
@@ -450,7 +434,6 @@ class ImpersonateStore {
450
434
  ...settings.dataNukeSettings
451
435
  } : this.state.dataNukeSettings
452
436
  };
453
- console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
454
437
 
455
438
  // Sync header key with listener
456
439
  if (settings.headerKey || settings.ignorePatterns) {
@@ -567,19 +550,21 @@ class ImpersonateStore {
567
550
  currentUser: this.state.currentUser
568
551
  };
569
552
  const serialized = JSON.stringify(toStore);
570
- console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
571
553
  try {
572
554
  // Prefer AsyncStorage for React Native
573
555
  if (this.asyncStorageRef) {
574
556
  await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
575
- console.log("[ImpersonateStore] Persisted to AsyncStorage");
557
+ // eslint-disable-next-line no-console
558
+ console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
576
559
  } else {
577
560
  // Fall back to localStorage (web)
578
561
  storage.setItem(STORAGE_KEY, serialized);
579
- console.log("[ImpersonateStore] Persisted to localStorage");
562
+ // eslint-disable-next-line no-console
563
+ console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
580
564
  }
581
565
  } catch (e) {
582
- console.log("[ImpersonateStore] Persist error:", e);
566
+ // eslint-disable-next-line no-console
567
+ console.warn("[impersonate] persist failed", e);
583
568
  }
584
569
  }
585
570
 
@@ -34,6 +34,12 @@ export { createImpersonateTool } from "./preset";
34
34
 
35
35
  export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
36
36
  export { ImpersonateBanner, ImpersonateBannerMinimal } from "./impersonate/components/ImpersonateBanner";
37
+ /**
38
+ * Auto-rendered by FloatingDevTools when this package is installed — the
39
+ * host does not need to mount it. Renders the floating impersonation
40
+ * banner, which is only visible while an impersonation session is active.
41
+ */
42
+ export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
37
43
  export { UserSearchView } from "./impersonate/components/UserSearchView";
38
44
  export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
39
45
  export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
@@ -7,20 +7,75 @@
7
7
  * No default preset is exported because configuration (onSearchUsers) is required.
8
8
  */
9
9
 
10
- import React, { useState, useEffect } from "react";
10
+ import React from "react";
11
11
  import { View, Text, StyleSheet } from "react-native";
12
12
  import { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
13
13
  import { ImpersonateBanner } from "./impersonate/components/ImpersonateBanner";
14
14
  import { impersonateStore } from "./impersonate/utils/impersonateStore";
15
+ import { impersonateListener } from "./impersonate/utils/impersonateListener";
16
+ import { jsx as _jsx } from "react/jsx-runtime";
17
+ // =============================================================================
18
+ // EAGER BOOTSTRAP
19
+ // =============================================================================
20
+ // Patch fetch/XHR and restore persisted state the moment createImpersonateTool
21
+ // runs (typically at module load in the host app). If we waited until the
22
+ // modal mounts (the original lazy design), every request fired during app
23
+ // boot — bridge polls, user-data fetches, etc. — goes out without the
24
+ // impersonation header, even when the user already had impersonation active
25
+ // from a previous session. React Query then caches the real-account response,
26
+ // so even after the listener does eventually patch, no refetch happens.
27
+ //
28
+ // Side effect: any later fetch interceptor (e.g. @buoy-gg/network) installed
29
+ // AFTER us will wrap our wrapper, so its captured init.headers won't show the
30
+ // impersonation header. The wire request still includes it.
31
+
32
+ let bootstrapStarted = false;
33
+ async function bootstrapImpersonate() {
34
+ if (bootstrapStarted) return;
35
+ bootstrapStarted = true;
36
+
37
+ // eslint-disable-next-line no-console
38
+ console.log("[impersonate] bootstrap: start");
39
+
40
+ // Patch fetch/XHR synchronously so any request between now and the async
41
+ // storage load goes through our wrapper. The wrapper is a no-op until
42
+ // userId is set (which happens once initializeAsync resolves).
43
+ if (!impersonateListener().isListening) {
44
+ impersonateListener().startListening();
45
+ }
15
46
 
16
- // NOTE: The listener is started lazily when the modal opens (via useImpersonate hook)
17
- // This ensures we patch AFTER @buoy-gg/network has patched fetch, so network
18
- // can see our injected headers in its request capture.
47
+ // React Native: require AsyncStorage. require() is more reliable under
48
+ // Metro than dynamic import(); on web this throws and the store falls
49
+ // back to localStorage handled synchronously in its constructor.
50
+ try {
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const mod = require("@react-native-async-storage/async-storage");
53
+ const AsyncStorage = mod?.default ?? mod;
54
+ if (!AsyncStorage || typeof AsyncStorage.getItem !== "function") {
55
+ // eslint-disable-next-line no-console
56
+ console.warn("[impersonate] bootstrap: AsyncStorage shape unexpected", Object.keys(mod ?? {}));
57
+ return;
58
+ }
59
+ const adapter = {
60
+ getItem: key => AsyncStorage.getItem(key),
61
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
62
+ };
63
+ impersonateStore.setAsyncStorage(adapter);
64
+ // eslint-disable-next-line no-console
65
+ console.log("[impersonate] bootstrap: AsyncStorage adapter installed");
66
+ await impersonateStore.initializeAsync(adapter);
67
+ // eslint-disable-next-line no-console
68
+ console.log("[impersonate] bootstrap: initializeAsync done");
69
+ } catch (e) {
70
+ // eslint-disable-next-line no-console
71
+ console.warn("[impersonate] bootstrap: failed", e);
72
+ }
73
+ }
19
74
 
20
75
  // =============================================================================
21
76
  // ICON COMPONENT
22
77
  // =============================================================================
23
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
78
+
24
79
  /**
25
80
  * Impersonate tool icon - a mask/person symbol
26
81
  */
@@ -48,43 +103,6 @@ function ImpersonateIcon({
48
103
  // WRAPPER COMPONENT
49
104
  // =============================================================================
50
105
 
51
- /**
52
- * Wrapper component that renders both the modal and the auto-banner.
53
- * The banner handles its own visibility based on store state (isActive + showBanner).
54
- * Clicking the banner opens the modal.
55
- */
56
- function ImpersonateToolWrapper(props) {
57
- const [forceOpen, setForceOpen] = useState(false);
58
-
59
- // Reset force state when parent closes the modal
60
- useEffect(() => {
61
- if (!props.visible) {
62
- setForceOpen(false);
63
- }
64
- }, [props.visible]);
65
- const handleBannerPress = () => {
66
- setForceOpen(true);
67
- };
68
- const handleClose = () => {
69
- setForceOpen(false);
70
- // Only notify parent if they had the modal open
71
- if (props.visible) {
72
- props.onClose();
73
- }
74
- };
75
- return /*#__PURE__*/_jsxs(_Fragment, {
76
- children: [/*#__PURE__*/_jsx(ImpersonateModal, {
77
- ...props,
78
- visible: props.visible || forceOpen,
79
- onClose: handleClose
80
- }), /*#__PURE__*/_jsx(ImpersonateBanner, {
81
- position: "top",
82
- offset: 50,
83
- onPress: handleBannerPress
84
- })]
85
- });
86
- }
87
-
88
106
  // =============================================================================
89
107
  // FACTORY FUNCTION
90
108
  // =============================================================================
@@ -127,8 +145,10 @@ function ImpersonateToolWrapper(props) {
127
145
  * showSettingsTab: true,
128
146
  * });
129
147
  *
130
- * // In your app - banner is automatic, no extra setup needed!
148
+ * // In your app mount the banner globally (outside FloatingDevTools)
149
+ * // so it survives reloads and stays visible after the modal is closed.
131
150
  * <FloatingDevTools apps={[impersonateTool]} />
151
+ * <ImpersonateBanner position="top" offset={50} />
132
152
  * ```
133
153
  */
134
154
  export function createImpersonateTool(config) {
@@ -150,6 +170,10 @@ export function createImpersonateTool(config) {
150
170
  impersonateStore.setDeveloperDefaults(defaults);
151
171
  }
152
172
 
173
+ // Patch fetch and restore persisted impersonation state up-front. Fire and
174
+ // forget — bootstrapImpersonate guards itself against double-invocation.
175
+ void bootstrapImpersonate();
176
+
153
177
  // Create the tool configuration object
154
178
  const tool = {
155
179
  id,
@@ -162,7 +186,7 @@ export function createImpersonateTool(config) {
162
186
  size: size,
163
187
  color: "#F59E0B"
164
188
  }),
165
- component: props => /*#__PURE__*/_jsx(ImpersonateToolWrapper, {
189
+ component: props => /*#__PURE__*/_jsx(ImpersonateModal, {
166
190
  ...props,
167
191
  onSearchUsers: onSearchUsers,
168
192
  onClearReactQuery: onClearReactQuery,
@@ -1 +1 @@
1
- {"version":3,"file":"ImpersonateBanner.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AAUpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,WAAW,EACX,QAAgB,EAChB,MAAW,GACZ,EAAE,mBAAmB,4BA4ErB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,GACR,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,4BAqBzC"}
1
+ {"version":3,"file":"ImpersonateBanner.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AAUpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,WAAW,EACX,QAAgB,EAChB,MAAW,GACZ,EAAE,mBAAmB,4BAgFrB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,GACR,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,4BAqBzC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ImpersonateOverlay
3
+ *
4
+ * Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
5
+ * `@buoy-gg/impersonate` is installed — the host does NOT need to mount
6
+ * anything. It renders the floating impersonation banner, which self-gates
7
+ * on store state (only visible while an impersonation session is active and
8
+ * `showBanner` is enabled), so it's safe to mount unconditionally.
9
+ */
10
+ import React from "react";
11
+ export declare function ImpersonateOverlay(): React.JSX.Element;
12
+ //# sourceMappingURL=ImpersonateOverlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonateOverlay.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateOverlay.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,wBAAgB,kBAAkB,sBAEjC"}
@@ -84,23 +84,20 @@ declare class ImpersonateStore {
84
84
  */
85
85
  subscribe: (listener: StateListener) => (() => void);
86
86
  /**
87
- * Start impersonating a user
87
+ * Start impersonating a user.
88
88
  *
89
- * This will:
90
- * 1. Execute data nuking based on settings
91
- * 2. Update state with new user
92
- * 3. Add to history
93
- * 4. Sync with impersonateListener
94
- * 5. Persist settings
89
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
90
+ * so refetches triggered by the cache clear go out with the new user's
91
+ * impersonation header. Doing it the other way around (nuke first) makes
92
+ * react-query refetch active queries with the OLD identity, producing
93
+ * the visual symptom of "I clicked a user and nothing changed."
95
94
  */
96
95
  startImpersonation(user: User): Promise<void>;
97
96
  /**
98
- * Stop impersonating
97
+ * Stop impersonating.
99
98
  *
100
- * This will:
101
- * 1. Execute data nuking based on settings
102
- * 2. Clear impersonation state
103
- * 3. Sync with impersonateListener
99
+ * Same ordering rationale as startImpersonation: clear state + listener
100
+ * before nuking caches so subsequent refetches go out without the header.
104
101
  */
105
102
  stopImpersonation(): Promise<void>;
106
103
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"impersonateStore.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/utils/impersonateStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"impersonateStore.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/utils/impersonateStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAEJ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAmClB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEzD,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAiDD;;;;;;;;GAQG;AACH,cAAM,gBAAgB;IACpB,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAGP;IAChB,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,iBAAiB,CAAoC;;IAqB7D;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;;OAGG;IACH,eAAe,CAAC,YAAY,EAAE;QAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,IAAI;IASR;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAkBzD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,oBAAoB,IAAI,mBAAmB,GAAG,IAAI;IAIlD;;;OAGG;IACG,eAAe,CAAC,YAAY,CAAC,EAAE;QACnC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CjB;;OAEG;IACH,qBAAqB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAQrD;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAI9B;;;OAGG;IACH,WAAW,QAAO,kBAAkB,CAElC;IAEF;;;OAGG;IACH,SAAS,GAAI,UAAU,aAAa,KAAG,CAAC,MAAM,IAAI,CAAC,CAGjD;IAMF;;;;;;;;OAQG;IACG,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCnD;;;;;OAKG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxC;;OAEG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBzC;;OAEG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB1C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,OAAO,CACf,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,gBAAgB,GAAG,YAAY,CAAC,GAAG;QACxE,gBAAgB,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;KAC9C,CACF,GACA,OAAO,CAAC,IAAI,CAAC;IAwBhB;;OAEG;IACG,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAStD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAanC;;OAEG;YACW,eAAe;IAqC7B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;YACW,OAAO;IA0CrB;;OAEG;IACH,OAAO,CAAC,MAAM;CASf;AAMD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
@@ -22,6 +22,12 @@
22
22
  export { createImpersonateTool } from "./preset";
23
23
  export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
24
24
  export { ImpersonateBanner, ImpersonateBannerMinimal, } from "./impersonate/components/ImpersonateBanner";
25
+ /**
26
+ * Auto-rendered by FloatingDevTools when this package is installed — the
27
+ * host does not need to mount it. Renders the floating impersonation
28
+ * banner, which is only visible while an impersonation session is active.
29
+ */
30
+ export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
25
31
  export { UserSearchView } from "./impersonate/components/UserSearchView";
26
32
  export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
27
33
  export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAMjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAC1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAC;AAMrF,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,YAAY,EACV,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,YAAY,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAM7F,YAAY,EACV,IAAI,EACJ,gBAAgB,IAAI,oBAAoB,EACxC,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAM7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,yCAAyC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAMjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,4CAA4C,CAAC;AACpD;;;;GAIG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAC1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAC;AAMrF,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,YAAY,EACV,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,YAAY,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAM7F,YAAY,EACV,IAAI,EACJ,gBAAgB,IAAI,oBAAoB,EACxC,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAM7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,yCAAyC,CAAC"}
@@ -45,8 +45,10 @@ import type { ImpersonateToolConfig } from "./impersonate/types";
45
45
  * showSettingsTab: true,
46
46
  * });
47
47
  *
48
- * // In your app - banner is automatic, no extra setup needed!
48
+ * // In your app mount the banner globally (outside FloatingDevTools)
49
+ * // so it survives reloads and stays visible after the modal is closed.
49
50
  * <FloatingDevTools apps={[impersonateTool]} />
51
+ * <ImpersonateBanner position="top" offset={50} />
50
52
  * ```
51
53
  */
52
54
  export declare function createImpersonateTool(config: ImpersonateToolConfig): {
@@ -1 +1 @@
1
- {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,OAAO,KAAK,EAAE,qBAAqB,EAAyB,MAAM,qBAAqB,CAAC;AA+FxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB;;;;;qBAyB9C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;uBAGd;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE;;;;;EAmB3H"}
1
+ {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAG/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AA+GjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB;;;;;qBA6B9C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;uBAGd;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE;;;;;EAmB3H"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/impersonate",
3
- "version": "2.1.15",
3
+ "version": "2.1.16",
4
4
  "description": "User impersonation tool for Buoy DevTools - inject custom headers for admin testing",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/shared-ui": "2.1.15"
29
+ "@buoy-gg/shared-ui": "2.2.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "*",