@buoy-gg/impersonate 2.1.15 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -311,34 +310,57 @@ class ImpersonateStore {
311
310
  return () => this.listeners.delete(listener);
312
311
  };
313
312
 
313
+ // ===========================================================================
314
+ // REMOTE MIRROR MODE
315
+ // ===========================================================================
316
+
317
+ remoteHandler = null;
318
+
319
+ /**
320
+ * Remote mirror mode (desktop dashboard): while a handler is set, every
321
+ * mutation forwards (method, params) to the synced device instead of
322
+ * running locally — header patching, data nuking and persistence all
323
+ * happen on the device, and the resulting state comes back via
324
+ * replaceState(). Pass null to restore local behavior.
325
+ */
326
+ setRemoteHandler(handler) {
327
+ this.remoteHandler = handler;
328
+ }
329
+
330
+ /**
331
+ * Replace the mirrored state from a synced device snapshot. Does not
332
+ * persist or touch the fetch listener — the device owns both.
333
+ */
334
+ replaceState(state) {
335
+ this.state = state;
336
+ this.notify();
337
+ }
338
+
314
339
  // ===========================================================================
315
340
  // IMPERSONATION ACTIONS
316
341
  // ===========================================================================
317
342
 
318
343
  /**
319
- * Start impersonating a user
344
+ * Start impersonating a user.
320
345
  *
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
346
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
347
+ * so refetches triggered by the cache clear go out with the new user's
348
+ * impersonation header. Doing it the other way around (nuke first) makes
349
+ * react-query refetch active queries with the OLD identity, producing
350
+ * the visual symptom of "I clicked a user and nothing changed."
327
351
  */
328
352
  async startImpersonation(user) {
329
- // Execute data nuking BEFORE switching
330
- await this.executeDataNuke();
331
-
332
- // Create history entry with timestamp
353
+ if (this.remoteHandler) {
354
+ this.remoteHandler("startImpersonation", {
355
+ user
356
+ });
357
+ return;
358
+ }
333
359
  const historyEntry = {
334
360
  user,
335
361
  lastUsedAt: new Date().toISOString()
336
362
  };
337
-
338
- // Update history (remove duplicate, add to front)
339
363
  const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
340
-
341
- // Update state
342
364
  this.state = {
343
365
  ...this.state,
344
366
  isActive: true,
@@ -346,50 +368,54 @@ class ImpersonateStore {
346
368
  history: newHistory
347
369
  };
348
370
 
349
- // Sync with listener
371
+ // 1) Point the fetch interceptor at the new user. Any request after this
372
+ // line carries the new header.
350
373
  this.syncWithListener();
351
374
 
352
- // Persist
353
- await this.persist();
354
-
355
- // Notify subscribers
375
+ // 2) Tell React subscribers right away so the UI updates without waiting
376
+ // on AsyncStorage / data nuke.
356
377
  this.notify();
378
+
379
+ // 3) Nuke caches — refetches now use the new identity.
380
+ await this.executeDataNuke();
381
+
382
+ // 4) Persist for next launch.
383
+ await this.persist();
357
384
  }
358
385
 
359
386
  /**
360
- * Stop impersonating
387
+ * Stop impersonating.
361
388
  *
362
- * This will:
363
- * 1. Execute data nuking based on settings
364
- * 2. Clear impersonation state
365
- * 3. Sync with impersonateListener
389
+ * Same ordering rationale as startImpersonation: clear state + listener
390
+ * before nuking caches so subsequent refetches go out without the header.
366
391
  */
367
392
  async stopImpersonation() {
368
- // Execute data nuking
369
- await this.executeDataNuke();
370
-
371
- // Update state
393
+ if (this.remoteHandler) {
394
+ this.remoteHandler("stopImpersonation");
395
+ return;
396
+ }
397
+ // eslint-disable-next-line no-console
398
+ console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
372
399
  this.state = {
373
400
  ...this.state,
374
401
  isActive: false,
375
402
  isPaused: false,
376
403
  currentUser: null
377
404
  };
378
-
379
- // Sync with listener
380
405
  this.syncWithListener();
381
-
382
- // Persist
383
- await this.persist();
384
-
385
- // Notify
386
406
  this.notify();
407
+ await this.executeDataNuke();
408
+ await this.persist();
387
409
  }
388
410
 
389
411
  /**
390
412
  * Pause impersonation (temporarily stop injecting headers)
391
413
  */
392
414
  async pauseImpersonation() {
415
+ if (this.remoteHandler) {
416
+ this.remoteHandler("pauseImpersonation");
417
+ return;
418
+ }
393
419
  if (!this.state.isActive || this.state.isPaused) return;
394
420
  this.state = {
395
421
  ...this.state,
@@ -410,6 +436,10 @@ class ImpersonateStore {
410
436
  * Resume impersonation (start injecting headers again)
411
437
  */
412
438
  async resumeImpersonation() {
439
+ if (this.remoteHandler) {
440
+ this.remoteHandler("resumeImpersonation");
441
+ return;
442
+ }
413
443
  if (!this.state.isActive || !this.state.isPaused) return;
414
444
  this.state = {
415
445
  ...this.state,
@@ -441,8 +471,12 @@ class ImpersonateStore {
441
471
  * Update settings (header key, ignore patterns, data nuke settings, show banner)
442
472
  */
443
473
  async updateSettings(settings) {
444
- console.log("[ImpersonateStore] updateSettings called with:", settings);
445
- console.log("[ImpersonateStore] Current dataNukeSettings:", this.state.dataNukeSettings);
474
+ if (this.remoteHandler) {
475
+ this.remoteHandler("updateSettings", {
476
+ settings
477
+ });
478
+ return;
479
+ }
446
480
  this.state = {
447
481
  ...this.state,
448
482
  headerKey: settings.headerKey ?? this.state.headerKey,
@@ -453,7 +487,6 @@ class ImpersonateStore {
453
487
  ...settings.dataNukeSettings
454
488
  } : this.state.dataNukeSettings
455
489
  };
456
- console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
457
490
 
458
491
  // Sync header key with listener
459
492
  if (settings.headerKey || settings.ignorePatterns) {
@@ -471,6 +504,12 @@ class ImpersonateStore {
471
504
  * Remove a user from history
472
505
  */
473
506
  async removeFromHistory(userId) {
507
+ if (this.remoteHandler) {
508
+ this.remoteHandler("removeFromHistory", {
509
+ userId
510
+ });
511
+ return;
512
+ }
474
513
  this.state = {
475
514
  ...this.state,
476
515
  history: this.state.history.filter(entry => entry.user.id !== userId)
@@ -483,6 +522,10 @@ class ImpersonateStore {
483
522
  * Clear all history
484
523
  */
485
524
  async clearHistory() {
525
+ if (this.remoteHandler) {
526
+ this.remoteHandler("clearHistory");
527
+ return;
528
+ }
486
529
  this.state = {
487
530
  ...this.state,
488
531
  history: []
@@ -570,19 +613,21 @@ class ImpersonateStore {
570
613
  currentUser: this.state.currentUser
571
614
  };
572
615
  const serialized = JSON.stringify(toStore);
573
- console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
574
616
  try {
575
617
  // Prefer AsyncStorage for React Native
576
618
  if (this.asyncStorageRef) {
577
619
  await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
578
- console.log("[ImpersonateStore] Persisted to AsyncStorage");
620
+ // eslint-disable-next-line no-console
621
+ console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
579
622
  } else {
580
623
  // Fall back to localStorage (web)
581
624
  storage.setItem(STORAGE_KEY, serialized);
582
- console.log("[ImpersonateStore] Persisted to localStorage");
625
+ // eslint-disable-next-line no-console
626
+ console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
583
627
  }
584
628
  } catch (e) {
585
- console.log("[ImpersonateStore] Persist error:", e);
629
+ // eslint-disable-next-line no-console
630
+ console.warn("[impersonate] persist failed", e);
586
631
  }
587
632
  }
588
633
 
@@ -27,12 +27,24 @@ Object.defineProperty(exports, "ImpersonateHistoryList", {
27
27
  return _ImpersonateHistoryList.ImpersonateHistoryList;
28
28
  }
29
29
  });
30
+ Object.defineProperty(exports, "ImpersonateIcon", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _preset.ImpersonateIcon;
34
+ }
35
+ });
30
36
  Object.defineProperty(exports, "ImpersonateModal", {
31
37
  enumerable: true,
32
38
  get: function () {
33
39
  return _ImpersonateModal.ImpersonateModal;
34
40
  }
35
41
  });
42
+ Object.defineProperty(exports, "ImpersonateOverlay", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _ImpersonateOverlay.ImpersonateOverlay;
46
+ }
47
+ });
36
48
  Object.defineProperty(exports, "ImpersonateStatusBar", {
37
49
  enumerable: true,
38
50
  get: function () {
@@ -45,6 +57,12 @@ Object.defineProperty(exports, "UserSearchView", {
45
57
  return _UserSearchView.UserSearchView;
46
58
  }
47
59
  });
60
+ Object.defineProperty(exports, "createImpersonateSyncAdapter", {
61
+ enumerable: true,
62
+ get: function () {
63
+ return _impersonateSyncAdapter.createImpersonateSyncAdapter;
64
+ }
65
+ });
48
66
  Object.defineProperty(exports, "createImpersonateTool", {
49
67
  enumerable: true,
50
68
  get: function () {
@@ -106,8 +124,10 @@ Object.defineProperty(exports, "useImpersonateHistory", {
106
124
  }
107
125
  });
108
126
  var _preset = require("./preset");
127
+ var _impersonateSyncAdapter = require("./sync/impersonateSyncAdapter");
109
128
  var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
110
129
  var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
130
+ var _ImpersonateOverlay = require("./impersonate/components/ImpersonateOverlay");
111
131
  var _UserSearchView = require("./impersonate/components/UserSearchView");
112
132
  var _DataNukeSettings = require("./impersonate/components/DataNukeSettings");
113
133
  var _ImpersonateHistoryList = require("./impersonate/components/ImpersonateHistoryList");
@@ -3,14 +3,16 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.ImpersonateIcon = ImpersonateIcon;
6
7
  exports.createImpersonateTool = createImpersonateTool;
7
- var _react = _interopRequireWildcard(require("react"));
8
+ var _react = _interopRequireDefault(require("react"));
8
9
  var _reactNative = require("react-native");
9
10
  var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
10
11
  var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
11
12
  var _impersonateStore = require("./impersonate/utils/impersonateStore");
13
+ var _impersonateListener = require("./impersonate/utils/impersonateListener");
12
14
  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); }
15
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
16
  /**
15
17
  * Impersonate Tool Preset
16
18
  *
@@ -18,9 +20,63 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
18
20
  * No default preset is exported because configuration (onSearchUsers) is required.
19
21
  */
20
22
 
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.
23
+ // =============================================================================
24
+ // EAGER BOOTSTRAP
25
+ // =============================================================================
26
+ // Patch fetch/XHR and restore persisted state the moment createImpersonateTool
27
+ // runs (typically at module load in the host app). If we waited until the
28
+ // modal mounts (the original lazy design), every request fired during app
29
+ // boot — bridge polls, user-data fetches, etc. — goes out without the
30
+ // impersonation header, even when the user already had impersonation active
31
+ // from a previous session. React Query then caches the real-account response,
32
+ // so even after the listener does eventually patch, no refetch happens.
33
+ //
34
+ // Side effect: any later fetch interceptor (e.g. @buoy-gg/network) installed
35
+ // AFTER us will wrap our wrapper, so its captured init.headers won't show the
36
+ // impersonation header. The wire request still includes it.
37
+
38
+ let bootstrapStarted = false;
39
+ async function bootstrapImpersonate() {
40
+ if (bootstrapStarted) return;
41
+ bootstrapStarted = true;
42
+
43
+ // eslint-disable-next-line no-console
44
+ console.log("[impersonate] bootstrap: start");
45
+
46
+ // Patch fetch/XHR synchronously so any request between now and the async
47
+ // storage load goes through our wrapper. The wrapper is a no-op until
48
+ // userId is set (which happens once initializeAsync resolves).
49
+ if (!(0, _impersonateListener.impersonateListener)().isListening) {
50
+ (0, _impersonateListener.impersonateListener)().startListening();
51
+ }
52
+
53
+ // React Native: require AsyncStorage. require() is more reliable under
54
+ // Metro than dynamic import(); on web this throws and the store falls
55
+ // back to localStorage handled synchronously in its constructor.
56
+ try {
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
58
+ const mod = require("@react-native-async-storage/async-storage");
59
+ const AsyncStorage = mod?.default ?? mod;
60
+ if (!AsyncStorage || typeof AsyncStorage.getItem !== "function") {
61
+ // eslint-disable-next-line no-console
62
+ console.warn("[impersonate] bootstrap: AsyncStorage shape unexpected", Object.keys(mod ?? {}));
63
+ return;
64
+ }
65
+ const adapter = {
66
+ getItem: key => AsyncStorage.getItem(key),
67
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
68
+ };
69
+ _impersonateStore.impersonateStore.setAsyncStorage(adapter);
70
+ // eslint-disable-next-line no-console
71
+ console.log("[impersonate] bootstrap: AsyncStorage adapter installed");
72
+ await _impersonateStore.impersonateStore.initializeAsync(adapter);
73
+ // eslint-disable-next-line no-console
74
+ console.log("[impersonate] bootstrap: initializeAsync done");
75
+ } catch (e) {
76
+ // eslint-disable-next-line no-console
77
+ console.warn("[impersonate] bootstrap: failed", e);
78
+ }
79
+ }
24
80
 
25
81
  // =============================================================================
26
82
  // ICON COMPONENT
@@ -53,43 +109,6 @@ function ImpersonateIcon({
53
109
  // WRAPPER COMPONENT
54
110
  // =============================================================================
55
111
 
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
112
  // =============================================================================
94
113
  // FACTORY FUNCTION
95
114
  // =============================================================================
@@ -132,8 +151,10 @@ function ImpersonateToolWrapper(props) {
132
151
  * showSettingsTab: true,
133
152
  * });
134
153
  *
135
- * // In your app - banner is automatic, no extra setup needed!
154
+ * // In your app mount the banner globally (outside FloatingDevTools)
155
+ * // so it survives reloads and stays visible after the modal is closed.
136
156
  * <FloatingDevTools apps={[impersonateTool]} />
157
+ * <ImpersonateBanner position="top" offset={50} />
137
158
  * ```
138
159
  */
139
160
  function createImpersonateTool(config) {
@@ -155,6 +176,10 @@ function createImpersonateTool(config) {
155
176
  _impersonateStore.impersonateStore.setDeveloperDefaults(defaults);
156
177
  }
157
178
 
179
+ // Patch fetch and restore persisted impersonation state up-front. Fire and
180
+ // forget — bootstrapImpersonate guards itself against double-invocation.
181
+ void bootstrapImpersonate();
182
+
158
183
  // Create the tool configuration object
159
184
  const tool = {
160
185
  id,
@@ -167,7 +192,7 @@ function createImpersonateTool(config) {
167
192
  size: size,
168
193
  color: "#F59E0B"
169
194
  }),
170
- component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateToolWrapper, {
195
+ component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateModal.ImpersonateModal, {
171
196
  ...props,
172
197
  onSearchUsers: onSearchUsers,
173
198
  onClearReactQuery: onClearReactQuery,
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createImpersonateSyncAdapter = createImpersonateSyncAdapter;
7
+ var _impersonateStore = require("../impersonate/utils/impersonateStore");
8
+ /**
9
+ * Create a sync adapter for the impersonate tool, consumed by
10
+ * @buoy-gg/external-sync's `useExternalSync` (structurally matches its
11
+ * ToolSyncAdapter interface so this package doesn't need a dependency on it).
12
+ *
13
+ * The dashboard mirrors the impersonation state and forwards every mutation
14
+ * back as an action — header patching, data nuking and persistence all run
15
+ * on the device, so impersonating from the desktop behaves exactly like
16
+ * tapping the modal on the phone.
17
+ */
18
+ function createImpersonateSyncAdapter({
19
+ onSearchUsers
20
+ }) {
21
+ return {
22
+ version: 1,
23
+ getSnapshot: () => _impersonateStore.impersonateStore.getState(),
24
+ subscribe: onChange => _impersonateStore.impersonateStore.subscribe(onChange),
25
+ actions: {
26
+ searchUsers: params => onSearchUsers(params.query),
27
+ startImpersonation: params => _impersonateStore.impersonateStore.startImpersonation(params.user),
28
+ stopImpersonation: () => _impersonateStore.impersonateStore.stopImpersonation(),
29
+ pauseImpersonation: () => _impersonateStore.impersonateStore.pauseImpersonation(),
30
+ resumeImpersonation: () => _impersonateStore.impersonateStore.resumeImpersonation(),
31
+ updateSettings: params => _impersonateStore.impersonateStore.updateSettings(params.settings),
32
+ removeFromHistory: params => _impersonateStore.impersonateStore.removeFromHistory(params.userId),
33
+ clearHistory: () => _impersonateStore.impersonateStore.clearHistory()
34
+ }
35
+ };
36
+ }
@@ -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
 
@@ -308,34 +307,57 @@ class ImpersonateStore {
308
307
  return () => this.listeners.delete(listener);
309
308
  };
310
309
 
310
+ // ===========================================================================
311
+ // REMOTE MIRROR MODE
312
+ // ===========================================================================
313
+
314
+ remoteHandler = null;
315
+
316
+ /**
317
+ * Remote mirror mode (desktop dashboard): while a handler is set, every
318
+ * mutation forwards (method, params) to the synced device instead of
319
+ * running locally — header patching, data nuking and persistence all
320
+ * happen on the device, and the resulting state comes back via
321
+ * replaceState(). Pass null to restore local behavior.
322
+ */
323
+ setRemoteHandler(handler) {
324
+ this.remoteHandler = handler;
325
+ }
326
+
327
+ /**
328
+ * Replace the mirrored state from a synced device snapshot. Does not
329
+ * persist or touch the fetch listener — the device owns both.
330
+ */
331
+ replaceState(state) {
332
+ this.state = state;
333
+ this.notify();
334
+ }
335
+
311
336
  // ===========================================================================
312
337
  // IMPERSONATION ACTIONS
313
338
  // ===========================================================================
314
339
 
315
340
  /**
316
- * Start impersonating a user
341
+ * Start impersonating a user.
317
342
  *
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
343
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
344
+ * so refetches triggered by the cache clear go out with the new user's
345
+ * impersonation header. Doing it the other way around (nuke first) makes
346
+ * react-query refetch active queries with the OLD identity, producing
347
+ * the visual symptom of "I clicked a user and nothing changed."
324
348
  */
325
349
  async startImpersonation(user) {
326
- // Execute data nuking BEFORE switching
327
- await this.executeDataNuke();
328
-
329
- // Create history entry with timestamp
350
+ if (this.remoteHandler) {
351
+ this.remoteHandler("startImpersonation", {
352
+ user
353
+ });
354
+ return;
355
+ }
330
356
  const historyEntry = {
331
357
  user,
332
358
  lastUsedAt: new Date().toISOString()
333
359
  };
334
-
335
- // Update history (remove duplicate, add to front)
336
360
  const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
337
-
338
- // Update state
339
361
  this.state = {
340
362
  ...this.state,
341
363
  isActive: true,
@@ -343,50 +365,54 @@ class ImpersonateStore {
343
365
  history: newHistory
344
366
  };
345
367
 
346
- // Sync with listener
368
+ // 1) Point the fetch interceptor at the new user. Any request after this
369
+ // line carries the new header.
347
370
  this.syncWithListener();
348
371
 
349
- // Persist
350
- await this.persist();
351
-
352
- // Notify subscribers
372
+ // 2) Tell React subscribers right away so the UI updates without waiting
373
+ // on AsyncStorage / data nuke.
353
374
  this.notify();
375
+
376
+ // 3) Nuke caches — refetches now use the new identity.
377
+ await this.executeDataNuke();
378
+
379
+ // 4) Persist for next launch.
380
+ await this.persist();
354
381
  }
355
382
 
356
383
  /**
357
- * Stop impersonating
384
+ * Stop impersonating.
358
385
  *
359
- * This will:
360
- * 1. Execute data nuking based on settings
361
- * 2. Clear impersonation state
362
- * 3. Sync with impersonateListener
386
+ * Same ordering rationale as startImpersonation: clear state + listener
387
+ * before nuking caches so subsequent refetches go out without the header.
363
388
  */
364
389
  async stopImpersonation() {
365
- // Execute data nuking
366
- await this.executeDataNuke();
367
-
368
- // Update state
390
+ if (this.remoteHandler) {
391
+ this.remoteHandler("stopImpersonation");
392
+ return;
393
+ }
394
+ // eslint-disable-next-line no-console
395
+ console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
369
396
  this.state = {
370
397
  ...this.state,
371
398
  isActive: false,
372
399
  isPaused: false,
373
400
  currentUser: null
374
401
  };
375
-
376
- // Sync with listener
377
402
  this.syncWithListener();
378
-
379
- // Persist
380
- await this.persist();
381
-
382
- // Notify
383
403
  this.notify();
404
+ await this.executeDataNuke();
405
+ await this.persist();
384
406
  }
385
407
 
386
408
  /**
387
409
  * Pause impersonation (temporarily stop injecting headers)
388
410
  */
389
411
  async pauseImpersonation() {
412
+ if (this.remoteHandler) {
413
+ this.remoteHandler("pauseImpersonation");
414
+ return;
415
+ }
390
416
  if (!this.state.isActive || this.state.isPaused) return;
391
417
  this.state = {
392
418
  ...this.state,
@@ -407,6 +433,10 @@ class ImpersonateStore {
407
433
  * Resume impersonation (start injecting headers again)
408
434
  */
409
435
  async resumeImpersonation() {
436
+ if (this.remoteHandler) {
437
+ this.remoteHandler("resumeImpersonation");
438
+ return;
439
+ }
410
440
  if (!this.state.isActive || !this.state.isPaused) return;
411
441
  this.state = {
412
442
  ...this.state,
@@ -438,8 +468,12 @@ class ImpersonateStore {
438
468
  * Update settings (header key, ignore patterns, data nuke settings, show banner)
439
469
  */
440
470
  async updateSettings(settings) {
441
- console.log("[ImpersonateStore] updateSettings called with:", settings);
442
- console.log("[ImpersonateStore] Current dataNukeSettings:", this.state.dataNukeSettings);
471
+ if (this.remoteHandler) {
472
+ this.remoteHandler("updateSettings", {
473
+ settings
474
+ });
475
+ return;
476
+ }
443
477
  this.state = {
444
478
  ...this.state,
445
479
  headerKey: settings.headerKey ?? this.state.headerKey,
@@ -450,7 +484,6 @@ class ImpersonateStore {
450
484
  ...settings.dataNukeSettings
451
485
  } : this.state.dataNukeSettings
452
486
  };
453
- console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
454
487
 
455
488
  // Sync header key with listener
456
489
  if (settings.headerKey || settings.ignorePatterns) {
@@ -468,6 +501,12 @@ class ImpersonateStore {
468
501
  * Remove a user from history
469
502
  */
470
503
  async removeFromHistory(userId) {
504
+ if (this.remoteHandler) {
505
+ this.remoteHandler("removeFromHistory", {
506
+ userId
507
+ });
508
+ return;
509
+ }
471
510
  this.state = {
472
511
  ...this.state,
473
512
  history: this.state.history.filter(entry => entry.user.id !== userId)
@@ -480,6 +519,10 @@ class ImpersonateStore {
480
519
  * Clear all history
481
520
  */
482
521
  async clearHistory() {
522
+ if (this.remoteHandler) {
523
+ this.remoteHandler("clearHistory");
524
+ return;
525
+ }
483
526
  this.state = {
484
527
  ...this.state,
485
528
  history: []
@@ -567,19 +610,21 @@ class ImpersonateStore {
567
610
  currentUser: this.state.currentUser
568
611
  };
569
612
  const serialized = JSON.stringify(toStore);
570
- console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
571
613
  try {
572
614
  // Prefer AsyncStorage for React Native
573
615
  if (this.asyncStorageRef) {
574
616
  await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
575
- console.log("[ImpersonateStore] Persisted to AsyncStorage");
617
+ // eslint-disable-next-line no-console
618
+ console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
576
619
  } else {
577
620
  // Fall back to localStorage (web)
578
621
  storage.setItem(STORAGE_KEY, serialized);
579
- console.log("[ImpersonateStore] Persisted to localStorage");
622
+ // eslint-disable-next-line no-console
623
+ console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
580
624
  }
581
625
  } catch (e) {
582
- console.log("[ImpersonateStore] Persist error:", e);
626
+ // eslint-disable-next-line no-console
627
+ console.warn("[impersonate] persist failed", e);
583
628
  }
584
629
  }
585
630
 
@@ -26,7 +26,13 @@
26
26
  // FACTORY (Primary entry point)
27
27
  // =============================================================================
28
28
 
29
- export { createImpersonateTool } from "./preset";
29
+ export { createImpersonateTool, ImpersonateIcon } from "./preset";
30
+
31
+ // =============================================================================
32
+ // EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
33
+ // =============================================================================
34
+
35
+ export { createImpersonateSyncAdapter } from "./sync/impersonateSyncAdapter";
30
36
 
31
37
  // =============================================================================
32
38
  // COMPONENTS (For custom UI implementations)
@@ -34,6 +40,12 @@ export { createImpersonateTool } from "./preset";
34
40
 
35
41
  export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
36
42
  export { ImpersonateBanner, ImpersonateBannerMinimal } from "./impersonate/components/ImpersonateBanner";
43
+ /**
44
+ * Auto-rendered by FloatingDevTools when this package is installed — the
45
+ * host does not need to mount it. Renders the floating impersonation
46
+ * banner, which is only visible while an impersonation session is active.
47
+ */
48
+ export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
37
49
  export { UserSearchView } from "./impersonate/components/UserSearchView";
38
50
  export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
39
51
  export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
@@ -7,24 +7,79 @@
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
  */
27
- function ImpersonateIcon({
82
+ export function ImpersonateIcon({
28
83
  size,
29
84
  color = "#F59E0B"
30
85
  }) {
@@ -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,
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ import { impersonateStore } from "../impersonate/utils/impersonateStore";
4
+ /**
5
+ * Create a sync adapter for the impersonate tool, consumed by
6
+ * @buoy-gg/external-sync's `useExternalSync` (structurally matches its
7
+ * ToolSyncAdapter interface so this package doesn't need a dependency on it).
8
+ *
9
+ * The dashboard mirrors the impersonation state and forwards every mutation
10
+ * back as an action — header patching, data nuking and persistence all run
11
+ * on the device, so impersonating from the desktop behaves exactly like
12
+ * tapping the modal on the phone.
13
+ */
14
+ export function createImpersonateSyncAdapter({
15
+ onSearchUsers
16
+ }) {
17
+ return {
18
+ version: 1,
19
+ getSnapshot: () => impersonateStore.getState(),
20
+ subscribe: onChange => impersonateStore.subscribe(onChange),
21
+ actions: {
22
+ searchUsers: params => onSearchUsers(params.query),
23
+ startImpersonation: params => impersonateStore.startImpersonation(params.user),
24
+ stopImpersonation: () => impersonateStore.stopImpersonation(),
25
+ pauseImpersonation: () => impersonateStore.pauseImpersonation(),
26
+ resumeImpersonation: () => impersonateStore.resumeImpersonation(),
27
+ updateSettings: params => impersonateStore.updateSettings(params.settings),
28
+ removeFromHistory: params => impersonateStore.removeFromHistory(params.userId),
29
+ clearHistory: () => impersonateStore.clearHistory()
30
+ }
31
+ };
32
+ }
@@ -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"}
@@ -83,24 +83,35 @@ declare class ImpersonateStore {
83
83
  * Returns unsubscribe function
84
84
  */
85
85
  subscribe: (listener: StateListener) => (() => void);
86
+ private remoteHandler;
86
87
  /**
87
- * Start impersonating a user
88
+ * Remote mirror mode (desktop dashboard): while a handler is set, every
89
+ * mutation forwards (method, params) to the synced device instead of
90
+ * running locally — header patching, data nuking and persistence all
91
+ * happen on the device, and the resulting state comes back via
92
+ * replaceState(). Pass null to restore local behavior.
93
+ */
94
+ setRemoteHandler(handler: ((method: string, params?: unknown) => void) | null): void;
95
+ /**
96
+ * Replace the mirrored state from a synced device snapshot. Does not
97
+ * persist or touch the fetch listener — the device owns both.
98
+ */
99
+ replaceState(state: ImpersonationState): void;
100
+ /**
101
+ * Start impersonating a user.
88
102
  *
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
103
+ * Order matters: we update state + listener + UI BEFORE clearing caches,
104
+ * so refetches triggered by the cache clear go out with the new user's
105
+ * impersonation header. Doing it the other way around (nuke first) makes
106
+ * react-query refetch active queries with the OLD identity, producing
107
+ * the visual symptom of "I clicked a user and nothing changed."
95
108
  */
96
109
  startImpersonation(user: User): Promise<void>;
97
110
  /**
98
- * Stop impersonating
111
+ * Stop impersonating.
99
112
  *
100
- * This will:
101
- * 1. Execute data nuking based on settings
102
- * 2. Clear impersonation state
103
- * 3. Sync with impersonateListener
113
+ * Same ordering rationale as startImpersonation: clear state + listener
114
+ * before nuking caches so subsequent refetches go out without the header.
104
115
  */
105
116
  stopImpersonation(): Promise<void>;
106
117
  /**
@@ -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":"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"}
@@ -19,9 +19,16 @@
19
19
  * <FloatingDevTools apps={[impersonateTool]} />
20
20
  * ```
21
21
  */
22
- export { createImpersonateTool } from "./preset";
22
+ export { createImpersonateTool, ImpersonateIcon } from "./preset";
23
+ export { createImpersonateSyncAdapter } from "./sync/impersonateSyncAdapter";
23
24
  export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
24
25
  export { ImpersonateBanner, ImpersonateBannerMinimal, } from "./impersonate/components/ImpersonateBanner";
26
+ /**
27
+ * Auto-rendered by FloatingDevTools when this package is installed — the
28
+ * host does not need to mount it. Renders the floating impersonation
29
+ * banner, which is only visible while an impersonation session is active.
30
+ */
31
+ export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
25
32
  export { UserSearchView } from "./impersonate/components/UserSearchView";
26
33
  export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
27
34
  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,eAAe,EAAE,MAAM,UAAU,CAAC;AAMlE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAM7E,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"}
@@ -7,6 +7,14 @@
7
7
  import React from "react";
8
8
  import { ImpersonateBanner } from "./impersonate/components/ImpersonateBanner";
9
9
  import type { ImpersonateToolConfig } from "./impersonate/types";
10
+ interface IconProps {
11
+ size: number;
12
+ color?: string;
13
+ }
14
+ /**
15
+ * Impersonate tool icon - a mask/person symbol
16
+ */
17
+ export declare function ImpersonateIcon({ size, color }: IconProps): React.JSX.Element;
10
18
  /**
11
19
  * Create an impersonate tool with custom configuration
12
20
  *
@@ -45,8 +53,10 @@ import type { ImpersonateToolConfig } from "./impersonate/types";
45
53
  * showSettingsTab: true,
46
54
  * });
47
55
  *
48
- * // In your app - banner is automatic, no extra setup needed!
56
+ * // In your app mount the banner globally (outside FloatingDevTools)
57
+ * // so it survives reloads and stays visible after the modal is closed.
49
58
  * <FloatingDevTools apps={[impersonateTool]} />
59
+ * <ImpersonateBanner position="top" offset={50} />
50
60
  * ```
51
61
  */
52
62
  export declare function createImpersonateTool(config: ImpersonateToolConfig): {
@@ -68,4 +78,5 @@ export declare function createImpersonateTool(config: ImpersonateToolConfig): {
68
78
  };
69
79
  Banner: typeof ImpersonateBanner;
70
80
  };
81
+ export {};
71
82
  //# sourceMappingURL=preset.d.ts.map
@@ -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;AAyEjE,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,KAAiB,EAAE,EAAE,SAAS,qBAmBrE;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -0,0 +1,35 @@
1
+ import type { User, ImpersonationState } from "../impersonate/types";
2
+ interface CreateImpersonateSyncAdapterOptions {
3
+ /**
4
+ * Same user-search callback you pass to the ImpersonateModal — the
5
+ * dashboard's search box proxies here.
6
+ */
7
+ onSearchUsers: (query: string) => Promise<User[]>;
8
+ }
9
+ /**
10
+ * Create a sync adapter for the impersonate tool, consumed by
11
+ * @buoy-gg/external-sync's `useExternalSync` (structurally matches its
12
+ * ToolSyncAdapter interface so this package doesn't need a dependency on it).
13
+ *
14
+ * The dashboard mirrors the impersonation state and forwards every mutation
15
+ * back as an action — header patching, data nuking and persistence all run
16
+ * on the device, so impersonating from the desktop behaves exactly like
17
+ * tapping the modal on the phone.
18
+ */
19
+ export declare function createImpersonateSyncAdapter({ onSearchUsers, }: CreateImpersonateSyncAdapterOptions): {
20
+ version: number;
21
+ getSnapshot: () => ImpersonationState;
22
+ subscribe: (onChange: () => void) => () => void;
23
+ actions: {
24
+ searchUsers: (params: unknown) => Promise<User[]>;
25
+ startImpersonation: (params: unknown) => Promise<void>;
26
+ stopImpersonation: () => Promise<void>;
27
+ pauseImpersonation: () => Promise<void>;
28
+ resumeImpersonation: () => Promise<void>;
29
+ updateSettings: (params: unknown) => Promise<void>;
30
+ removeFromHistory: (params: unknown) => Promise<void>;
31
+ clearHistory: () => Promise<void>;
32
+ };
33
+ };
34
+ export {};
35
+ //# sourceMappingURL=impersonateSyncAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impersonateSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/impersonateSyncAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,UAAU,mCAAmC;IAC3C;;;OAGG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CACnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,aAAa,GACd,EAAE,mCAAmC;;uBAGjB,kBAAkB;0BACb,MAAM,IAAI;;8BAER,OAAO;qCAEA,OAAO;;;;iCAKX,OAAO;oCAKJ,OAAO;;;EAOxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/impersonate",
3
- "version": "2.1.15",
3
+ "version": "3.0.0",
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": "3.0.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "*",