@buoy-gg/impersonate 2.1.15 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/impersonate/components/ImpersonateBanner.js +4 -0
- package/lib/commonjs/impersonate/components/ImpersonateOverlay.js +26 -0
- package/lib/commonjs/impersonate/utils/impersonateStore.js +33 -48
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/preset.js +68 -44
- package/lib/module/impersonate/components/ImpersonateBanner.js +4 -0
- package/lib/module/impersonate/components/ImpersonateOverlay.js +21 -0
- package/lib/module/impersonate/utils/impersonateStore.js +33 -48
- package/lib/module/index.js +6 -0
- package/lib/module/preset.js +68 -44
- package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -1
- package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts +12 -0
- package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts.map +1 -0
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts +9 -12
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/preset.d.ts +3 -1
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
|
270
|
-
|
|
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
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
353
|
-
|
|
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
|
-
*
|
|
363
|
-
*
|
|
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
|
-
//
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
+
// eslint-disable-next-line no-console
|
|
570
|
+
console.warn("[impersonate] persist failed", e);
|
|
586
571
|
}
|
|
587
572
|
}
|
|
588
573
|
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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");
|
package/lib/commonjs/preset.js
CHANGED
|
@@ -4,13 +4,14 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.createImpersonateTool = createImpersonateTool;
|
|
7
|
-
var _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
|
|
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
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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
|
|
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)(
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
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
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
350
|
-
|
|
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
|
-
*
|
|
360
|
-
*
|
|
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
|
-
//
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
+
// eslint-disable-next-line no-console
|
|
567
|
+
console.warn("[impersonate] persist failed", e);
|
|
583
568
|
}
|
|
584
569
|
}
|
|
585
570
|
|
package/lib/module/index.js
CHANGED
|
@@ -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";
|
package/lib/module/preset.js
CHANGED
|
@@ -7,20 +7,75 @@
|
|
|
7
7
|
* No default preset is exported because configuration (onSearchUsers) is required.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import 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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
*
|
|
101
|
-
*
|
|
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":"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
|
|
1
|
+
{"version":3,"file":"impersonateStore.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/utils/impersonateStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
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,
|
|
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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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": "
|
|
29
|
+
"@buoy-gg/shared-ui": "3.0.1"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "*",
|