@buoy-gg/impersonate 1.0.3-beta.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.
Files changed (95) hide show
  1. package/LICENSE +58 -0
  2. package/lib/commonjs/impersonate/components/DataNukeSettings.js +715 -0
  3. package/lib/commonjs/impersonate/components/ImpersonateBanner.js +217 -0
  4. package/lib/commonjs/impersonate/components/ImpersonateHistoryList.js +173 -0
  5. package/lib/commonjs/impersonate/components/ImpersonateModal.js +304 -0
  6. package/lib/commonjs/impersonate/components/ImpersonateStatusBar.js +130 -0
  7. package/lib/commonjs/impersonate/components/UserAvatar.js +146 -0
  8. package/lib/commonjs/impersonate/components/UserCard.js +200 -0
  9. package/lib/commonjs/impersonate/components/UserSearchView.js +227 -0
  10. package/lib/commonjs/impersonate/components/index.js +85 -0
  11. package/lib/commonjs/impersonate/hooks/index.js +64 -0
  12. package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +144 -0
  13. package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +155 -0
  14. package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +188 -0
  15. package/lib/commonjs/impersonate/hooks/useImpersonate.js +215 -0
  16. package/lib/commonjs/impersonate/hooks/useImpersonateHistory.js +56 -0
  17. package/lib/commonjs/impersonate/index.js +49 -0
  18. package/lib/commonjs/impersonate/types/index.js +16 -0
  19. package/lib/commonjs/impersonate/types/types.js +1 -0
  20. package/lib/commonjs/impersonate/utils/impersonateListener.js +280 -0
  21. package/lib/commonjs/impersonate/utils/impersonateStore.js +607 -0
  22. package/lib/commonjs/impersonate/utils/index.js +49 -0
  23. package/lib/commonjs/index.js +118 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/preset.js +214 -0
  26. package/lib/module/impersonate/components/DataNukeSettings.js +710 -0
  27. package/lib/module/impersonate/components/ImpersonateBanner.js +211 -0
  28. package/lib/module/impersonate/components/ImpersonateHistoryList.js +168 -0
  29. package/lib/module/impersonate/components/ImpersonateModal.js +300 -0
  30. package/lib/module/impersonate/components/ImpersonateStatusBar.js +125 -0
  31. package/lib/module/impersonate/components/UserAvatar.js +140 -0
  32. package/lib/module/impersonate/components/UserCard.js +195 -0
  33. package/lib/module/impersonate/components/UserSearchView.js +222 -0
  34. package/lib/module/impersonate/components/index.js +11 -0
  35. package/lib/module/impersonate/hooks/index.js +7 -0
  36. package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +140 -0
  37. package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +151 -0
  38. package/lib/module/impersonate/hooks/useAutoClearRedux.js +183 -0
  39. package/lib/module/impersonate/hooks/useImpersonate.js +212 -0
  40. package/lib/module/impersonate/hooks/useImpersonateHistory.js +52 -0
  41. package/lib/module/impersonate/index.js +13 -0
  42. package/lib/module/impersonate/types/index.js +3 -0
  43. package/lib/module/impersonate/types/types.js +1 -0
  44. package/lib/module/impersonate/utils/impersonateListener.js +271 -0
  45. package/lib/module/impersonate/utils/impersonateStore.js +604 -0
  46. package/lib/module/impersonate/utils/index.js +4 -0
  47. package/lib/module/index.js +103 -0
  48. package/lib/module/preset.js +209 -0
  49. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts +37 -0
  50. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +1 -0
  51. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts +40 -0
  52. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -0
  53. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts +24 -0
  54. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +1 -0
  55. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts +10 -0
  56. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +1 -0
  57. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts +15 -0
  58. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +1 -0
  59. package/lib/typescript/impersonate/components/UserAvatar.d.ts +32 -0
  60. package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +1 -0
  61. package/lib/typescript/impersonate/components/UserCard.d.ts +28 -0
  62. package/lib/typescript/impersonate/components/UserCard.d.ts.map +1 -0
  63. package/lib/typescript/impersonate/components/UserSearchView.d.ts +31 -0
  64. package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +1 -0
  65. package/lib/typescript/impersonate/components/index.d.ts +16 -0
  66. package/lib/typescript/impersonate/components/index.d.ts.map +1 -0
  67. package/lib/typescript/impersonate/hooks/index.d.ts +11 -0
  68. package/lib/typescript/impersonate/hooks/index.d.ts.map +1 -0
  69. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts +48 -0
  70. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +1 -0
  71. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts +48 -0
  72. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +1 -0
  73. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts +78 -0
  74. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +1 -0
  75. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts +76 -0
  76. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +1 -0
  77. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts +43 -0
  78. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +1 -0
  79. package/lib/typescript/impersonate/index.d.ts +5 -0
  80. package/lib/typescript/impersonate/index.d.ts.map +1 -0
  81. package/lib/typescript/impersonate/types/index.d.ts +2 -0
  82. package/lib/typescript/impersonate/types/index.d.ts.map +1 -0
  83. package/lib/typescript/impersonate/types/types.d.ts +177 -0
  84. package/lib/typescript/impersonate/types/types.d.ts.map +1 -0
  85. package/lib/typescript/impersonate/utils/impersonateListener.d.ts +115 -0
  86. package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +1 -0
  87. package/lib/typescript/impersonate/utils/impersonateStore.d.ts +151 -0
  88. package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -0
  89. package/lib/typescript/impersonate/utils/index.d.ts +3 -0
  90. package/lib/typescript/impersonate/utils/index.d.ts.map +1 -0
  91. package/lib/typescript/index.d.ts +80 -0
  92. package/lib/typescript/index.d.ts.map +1 -0
  93. package/lib/typescript/preset.d.ts +71 -0
  94. package/lib/typescript/preset.d.ts.map +1 -0
  95. package/package.json +78 -0
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useAutoClearAsyncStorage Hook
5
+ *
6
+ * Auto-detects AsyncStorage and provides a clear function.
7
+ * Follows the same pattern as useAutoClearReactQuery and useAutoClearRedux.
8
+ *
9
+ * This hook:
10
+ * 1. Dynamically requires @react-native-async-storage/async-storage
11
+ * 2. Returns a clearStorage function that removes all non-buoy keys
12
+ *
13
+ * IMPORTANT: This preserves all @buoy/* keys to avoid clearing our own state.
14
+ */
15
+
16
+ import { useCallback, useMemo, useState, useEffect } from "react";
17
+
18
+ // ============================================
19
+ // Types
20
+ // ============================================
21
+
22
+ // Flag to prevent double-firing (synchronous check)
23
+ let isClearing = false;
24
+ // ============================================
25
+ // Safe Module Loading
26
+ // ============================================
27
+
28
+ /**
29
+ * Safely attempt to load @react-native-async-storage/async-storage
30
+ */
31
+ function getAsyncStorage() {
32
+ try {
33
+ // Dynamic require to handle cases where async-storage isn't installed
34
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
35
+ const AsyncStorage = require("@react-native-async-storage/async-storage").default;
36
+ return AsyncStorage;
37
+ } catch {
38
+ // @react-native-async-storage/async-storage not available
39
+ return null;
40
+ }
41
+ }
42
+
43
+ // ============================================
44
+ // Main Hook
45
+ // ============================================
46
+
47
+ /**
48
+ * Hook to auto-detect AsyncStorage and provide clearing functionality.
49
+ *
50
+ * This clears all keys EXCEPT those starting with @buoy/ to preserve
51
+ * our own persisted state (impersonation settings, history, etc.).
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * function MyComponent() {
56
+ * const { isAvailable, clearStorage } = useAutoClearAsyncStorage();
57
+ *
58
+ * if (isAvailable && clearStorage) {
59
+ * // Clear all app data except Buoy state
60
+ * await clearStorage();
61
+ * }
62
+ * }
63
+ * ```
64
+ */
65
+ export function useAutoClearAsyncStorage() {
66
+ const [asyncStorage, setAsyncStorage] = useState(null);
67
+ const [error, setError] = useState(null);
68
+
69
+ // Check for AsyncStorage on mount
70
+ useEffect(() => {
71
+ const storage = getAsyncStorage();
72
+ if (storage) {
73
+ setAsyncStorage(storage);
74
+ setError(null);
75
+ } else {
76
+ setError("@react-native-async-storage/async-storage is not installed");
77
+ }
78
+ }, []);
79
+
80
+ // Create the clearStorage function with guard to prevent double-firing
81
+ const clearStorage = useCallback(async () => {
82
+ // Synchronous guard - set flag immediately before any async work
83
+ if (isClearing) {
84
+ return;
85
+ }
86
+ isClearing = true;
87
+ try {
88
+ if (asyncStorage) {
89
+ // Get all keys
90
+ const allKeys = await asyncStorage.getAllKeys();
91
+
92
+ // Filter out @buoy/* keys to preserve our own state
93
+ const keysToRemove = allKeys.filter(key => !key.startsWith("@buoy/") && !key.startsWith("@buoy-"));
94
+ if (keysToRemove.length > 0) {
95
+ await asyncStorage.multiRemove([...keysToRemove]);
96
+ }
97
+ }
98
+ } finally {
99
+ // Reset flag after a short delay to allow next legitimate call
100
+ setTimeout(() => {
101
+ isClearing = false;
102
+ }, 100);
103
+ }
104
+ }, [asyncStorage]);
105
+
106
+ // Memoize result
107
+ const result = useMemo(() => {
108
+ if (!asyncStorage || error) {
109
+ return {
110
+ isAvailable: false,
111
+ error: error || "@react-native-async-storage/async-storage is not available",
112
+ clearStorage: null
113
+ };
114
+ }
115
+ return {
116
+ isAvailable: true,
117
+ error: null,
118
+ clearStorage
119
+ };
120
+ }, [asyncStorage, error, clearStorage]);
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Lightweight check to see if AsyncStorage is available
126
+ * without creating the clear function
127
+ */
128
+ export function useAsyncStorageAvailability() {
129
+ const storage = getAsyncStorage();
130
+ if (!storage) {
131
+ return {
132
+ isInstalled: false,
133
+ error: "@react-native-async-storage/async-storage is not installed"
134
+ };
135
+ }
136
+ return {
137
+ isInstalled: true,
138
+ error: null
139
+ };
140
+ }
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useAutoClearReactQuery Hook
5
+ *
6
+ * Auto-detects React Query and provides a clear function.
7
+ * Uses the same pattern as @buoy-gg/redux's useAutoInstrumentRedux.
8
+ *
9
+ * This hook:
10
+ * 1. Dynamically requires @tanstack/react-query
11
+ * 2. Uses useQueryClient() to get the client from context
12
+ * 3. Returns a clearCache function that clears queries + mutations
13
+ */
14
+
15
+ import { useCallback, useMemo } from "react";
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ // Flag to prevent double-firing (synchronous check)
22
+ let isClearing = false;
23
+ // ============================================
24
+ // Safe Module Loading
25
+ // ============================================
26
+
27
+ /**
28
+ * Safely attempt to load @tanstack/react-query and get useQueryClient hook
29
+ */
30
+ function getReactQueryUseQueryClient() {
31
+ try {
32
+ // Dynamic require to handle cases where react-query isn't installed
33
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
34
+ const reactQuery = require("@tanstack/react-query");
35
+ return reactQuery.useQueryClient;
36
+ } catch {
37
+ // @tanstack/react-query not available
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // ============================================
43
+ // Main Hook
44
+ // ============================================
45
+
46
+ /**
47
+ * Hook to auto-detect React Query and provide cache clearing functionality.
48
+ *
49
+ * This mirrors how the react-query package's QueryBrowserModal.tsx clears cache:
50
+ * `queryClient.getQueryCache().clear()`
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * function MyComponent() {
55
+ * const { isAvailable, clearCache } = useAutoClearReactQuery();
56
+ *
57
+ * if (isAvailable && clearCache) {
58
+ * // Can clear React Query cache
59
+ * clearCache();
60
+ * }
61
+ * }
62
+ * ```
63
+ */
64
+ export function useAutoClearReactQuery() {
65
+ // Check if @tanstack/react-query is available
66
+ const useQueryClientHook = getReactQueryUseQueryClient();
67
+ const isReactQueryInstalled = useQueryClientHook !== null;
68
+
69
+ // Try to get the query client using react-query's useQueryClient
70
+ // This must be called unconditionally due to hooks rules
71
+ let queryClient = null;
72
+ let clientError = null;
73
+ if (useQueryClientHook) {
74
+ try {
75
+ // eslint-disable-next-line react-hooks/rules-of-hooks
76
+ queryClient = useQueryClientHook();
77
+ } catch (e) {
78
+ // Not inside a QueryClientProvider or other error
79
+ clientError = e instanceof Error ? e.message : "Failed to access QueryClient. Make sure your app is wrapped in a QueryClientProvider.";
80
+ }
81
+ } else {
82
+ clientError = "@tanstack/react-query is not installed";
83
+ }
84
+
85
+ // Create the clearCache function with guard to prevent double-firing
86
+ const clearCache = useCallback(() => {
87
+ // Synchronous guard - set flag immediately before any async work
88
+ if (isClearing) {
89
+ return;
90
+ }
91
+ isClearing = true;
92
+ if (queryClient) {
93
+ // Clear both query and mutation caches
94
+ // This is the same approach used in @buoy-gg/react-query
95
+ queryClient.getQueryCache().clear();
96
+ queryClient.getMutationCache().clear();
97
+ }
98
+
99
+ // Reset flag after a short delay to allow next legitimate call
100
+ setTimeout(() => {
101
+ isClearing = false;
102
+ }, 100);
103
+ }, [queryClient]);
104
+
105
+ // Memoize result
106
+ const result = useMemo(() => {
107
+ if (!isReactQueryInstalled || clientError || !queryClient) {
108
+ return {
109
+ isAvailable: false,
110
+ error: clientError,
111
+ clearCache: null
112
+ };
113
+ }
114
+ return {
115
+ isAvailable: true,
116
+ error: null,
117
+ clearCache
118
+ };
119
+ }, [isReactQueryInstalled, clientError, queryClient, clearCache]);
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Lightweight hook to just check if React Query is available
125
+ * without getting the client
126
+ */
127
+ export function useReactQueryAvailability() {
128
+ const useQueryClientHook = getReactQueryUseQueryClient();
129
+ if (!useQueryClientHook) {
130
+ return {
131
+ isInstalled: false,
132
+ isConnected: false,
133
+ error: "@tanstack/react-query is not installed"
134
+ };
135
+ }
136
+ try {
137
+ // eslint-disable-next-line react-hooks/rules-of-hooks
138
+ const client = useQueryClientHook();
139
+ return {
140
+ isInstalled: true,
141
+ isConnected: client !== null && client !== undefined,
142
+ error: null
143
+ };
144
+ } catch (e) {
145
+ return {
146
+ isInstalled: true,
147
+ isConnected: false,
148
+ error: e instanceof Error ? e.message : "Not inside a QueryClientProvider"
149
+ };
150
+ }
151
+ }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useAutoClearRedux Hook
5
+ *
6
+ * Auto-detects Redux store and provides a reset function.
7
+ * Uses the same pattern as @buoy-gg/redux's useAutoInstrumentRedux.
8
+ *
9
+ * This hook:
10
+ * 1. Dynamically requires react-redux
11
+ * 2. Uses useStore() to get the store from context
12
+ * 3. Returns a resetStore function that dispatches a reset action
13
+ *
14
+ * IMPORTANT: Unlike React Query which has a universal .clear() method,
15
+ * Redux reset requires the app's reducer to handle the reset action.
16
+ * The default action type is '@@RESET' but can be customized.
17
+ *
18
+ * Example reducer handling:
19
+ * ```typescript
20
+ * const rootReducer = (state, action) => {
21
+ * if (action.type === '@@RESET') {
22
+ * return undefined; // Returns to initial state
23
+ * }
24
+ * return appReducer(state, action);
25
+ * };
26
+ * ```
27
+ */
28
+
29
+ import { useCallback, useMemo } from "react";
30
+
31
+ // ============================================
32
+ // Types
33
+ // ============================================
34
+
35
+ // Flag to prevent double-firing (synchronous check)
36
+ let isDispatching = false;
37
+
38
+ /** The default action type dispatched to reset Redux state */
39
+ export const REDUX_RESET_ACTION_TYPE = "@@RESET";
40
+ // ============================================
41
+ // Safe Module Loading
42
+ // ============================================
43
+
44
+ /**
45
+ * Safely attempt to load react-redux and get useStore hook
46
+ */
47
+ function getReactReduxUseStore() {
48
+ try {
49
+ // Dynamic require to handle cases where react-redux isn't installed
50
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
51
+ const reactRedux = require("react-redux");
52
+ return reactRedux.useStore;
53
+ } catch {
54
+ // react-redux not available
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // ============================================
60
+ // Main Hook
61
+ // ============================================
62
+
63
+ /**
64
+ * Hook to auto-detect Redux store and provide state reset functionality.
65
+ *
66
+ * This dispatches a reset action (default: '@@RESET') to the store.
67
+ * Your app's root reducer must handle this action to actually reset state.
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * // In your component:
72
+ * function MyComponent() {
73
+ * const { isAvailable, resetStore } = useAutoClearRedux();
74
+ *
75
+ * if (isAvailable && resetStore) {
76
+ * resetStore(); // Dispatches { type: '@@RESET' }
77
+ * }
78
+ * }
79
+ *
80
+ * // In your root reducer:
81
+ * const rootReducer = (state, action) => {
82
+ * if (action.type === '@@RESET') {
83
+ * // Return undefined to reset to initial state
84
+ * // Or return a specific "logged out" state
85
+ * return undefined;
86
+ * }
87
+ * return appReducer(state, action);
88
+ * };
89
+ * ```
90
+ */
91
+ export function useAutoClearRedux(options = {}) {
92
+ const {
93
+ resetActionType = REDUX_RESET_ACTION_TYPE
94
+ } = options;
95
+
96
+ // Check if react-redux is available
97
+ const useStoreHook = getReactReduxUseStore();
98
+ const isReactReduxInstalled = useStoreHook !== null;
99
+
100
+ // Try to get the store using react-redux's useStore
101
+ // This must be called unconditionally due to hooks rules
102
+ let store = null;
103
+ let storeError = null;
104
+ if (useStoreHook) {
105
+ try {
106
+ // eslint-disable-next-line react-hooks/rules-of-hooks
107
+ store = useStoreHook();
108
+ } catch (e) {
109
+ // Not inside a Provider or other error
110
+ storeError = e instanceof Error ? e.message : "Failed to access Redux store. Make sure your app is wrapped in a Redux Provider.";
111
+ }
112
+ } else {
113
+ storeError = "react-redux is not installed";
114
+ }
115
+
116
+ // Create the resetStore function with guard to prevent double-firing
117
+ const resetStore = useCallback(() => {
118
+ // Synchronous guard - set flag immediately before any async work
119
+ if (isDispatching) {
120
+ return;
121
+ }
122
+ isDispatching = true;
123
+ if (store) {
124
+ // Dispatch the reset action
125
+ // The app's root reducer must handle this to actually reset state
126
+ store.dispatch({
127
+ type: resetActionType
128
+ });
129
+ }
130
+
131
+ // Reset flag after a short delay to allow next legitimate call
132
+ setTimeout(() => {
133
+ isDispatching = false;
134
+ }, 100);
135
+ }, [store, resetActionType]);
136
+
137
+ // Memoize result
138
+ const result = useMemo(() => {
139
+ if (!isReactReduxInstalled || storeError || !store) {
140
+ return {
141
+ isAvailable: false,
142
+ error: storeError,
143
+ resetStore: null
144
+ };
145
+ }
146
+ return {
147
+ isAvailable: true,
148
+ error: null,
149
+ resetStore
150
+ };
151
+ }, [isReactReduxInstalled, storeError, store, resetStore]);
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Lightweight hook to just check if Redux is available
157
+ * without getting the store
158
+ */
159
+ export function useReduxAvailability() {
160
+ const useStoreHook = getReactReduxUseStore();
161
+ if (!useStoreHook) {
162
+ return {
163
+ isInstalled: false,
164
+ isConnected: false,
165
+ error: "react-redux is not installed"
166
+ };
167
+ }
168
+ try {
169
+ // eslint-disable-next-line react-hooks/rules-of-hooks
170
+ const store = useStoreHook();
171
+ return {
172
+ isInstalled: true,
173
+ isConnected: store !== null && store !== undefined,
174
+ error: null
175
+ };
176
+ } catch (e) {
177
+ return {
178
+ isInstalled: true,
179
+ isConnected: false,
180
+ error: e instanceof Error ? e.message : "Not inside a Redux Provider"
181
+ };
182
+ }
183
+ }
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * useImpersonate Hook
5
+ *
6
+ * Main hook for managing impersonation state and actions.
7
+ * Provides all the controls needed to search users, start/stop
8
+ * impersonation, and manage settings.
9
+ */
10
+
11
+ import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
12
+ import { impersonateStore } from "../utils/impersonateStore";
13
+ import { impersonateListener } from "../utils/impersonateListener";
14
+ /**
15
+ * Hook for managing impersonation
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * function ImpersonatePanel() {
20
+ * const {
21
+ * isActive,
22
+ * currentUser,
23
+ * searchUsers,
24
+ * searchResults,
25
+ * startImpersonation,
26
+ * stopImpersonation,
27
+ * } = useImpersonate({
28
+ * onSearchUsers: async (query) => api.searchUsers(query),
29
+ * onClearReactQuery: () => queryClient.clear(),
30
+ * });
31
+ *
32
+ * return (
33
+ * <View>
34
+ * {isActive ? (
35
+ * <Text>Impersonating: {currentUser?.email}</Text>
36
+ * ) : (
37
+ * <Text>Not impersonating</Text>
38
+ * )}
39
+ * </View>
40
+ * );
41
+ * }
42
+ * ```
43
+ */
44
+ export function useImpersonate(options = {}) {
45
+ const [searchQuery, setSearchQuery] = useState("");
46
+ const [searchResults, setSearchResults] = useState([]);
47
+ const [isSearching, setIsSearching] = useState(false);
48
+ const [searchError, setSearchError] = useState(null);
49
+
50
+ // Subscribe to store state using useSyncExternalStore
51
+ const state = useSyncExternalStore(impersonateStore.subscribe, impersonateStore.getSnapshot, impersonateStore.getSnapshot // Server snapshot (same for SSR)
52
+ );
53
+
54
+ // Register nuke callbacks when they change
55
+ useEffect(() => {
56
+ impersonateStore.registerNukeCallbacks({
57
+ reactQuery: options.onClearReactQuery,
58
+ redux: options.onClearRedux,
59
+ asyncStorage: options.onClearAsyncStorage,
60
+ mmkv: options.onClearMMKV
61
+ });
62
+ }, [options.onClearReactQuery, options.onClearRedux, options.onClearAsyncStorage, options.onClearMMKV]);
63
+
64
+ // Set up AsyncStorage reference immediately for writes, then load persisted state
65
+ useEffect(() => {
66
+ let isMounted = true;
67
+ const setupAsyncStorage = async () => {
68
+ try {
69
+ // Dynamically import AsyncStorage
70
+ const AsyncStorage = await import("@react-native-async-storage/async-storage").then(m => m.default);
71
+ const storageAdapter = {
72
+ getItem: key => AsyncStorage.getItem(key),
73
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
74
+ };
75
+
76
+ // Set storage reference FIRST so writes work immediately
77
+ impersonateStore.setAsyncStorage(storageAdapter);
78
+
79
+ // Then load persisted state
80
+ if (isMounted) {
81
+ await impersonateStore.initializeAsync(storageAdapter);
82
+ }
83
+ } catch {
84
+ // AsyncStorage not available - using localStorage fallback (web)
85
+ }
86
+ };
87
+ setupAsyncStorage();
88
+
89
+ // Start listener
90
+ if (!impersonateListener().isListening) {
91
+ impersonateListener().startListening();
92
+ }
93
+ return () => {
94
+ isMounted = false;
95
+ };
96
+ // Don't stop listener on unmount - headers should keep being injected
97
+ }, []);
98
+
99
+ // Search users
100
+ const searchUsers = useCallback(async query => {
101
+ setSearchQuery(query);
102
+ setSearchError(null);
103
+ if (!query.trim()) {
104
+ setSearchResults([]);
105
+ return;
106
+ }
107
+ if (!options.onSearchUsers) {
108
+ setSearchError("Search not configured");
109
+ setSearchResults([]);
110
+ return;
111
+ }
112
+ setIsSearching(true);
113
+ try {
114
+ const results = await options.onSearchUsers(query);
115
+ setSearchResults(results);
116
+ } catch (error) {
117
+ setSearchError(error instanceof Error ? error.message : "Search failed");
118
+ setSearchResults([]);
119
+ } finally {
120
+ setIsSearching(false);
121
+ }
122
+ }, [options.onSearchUsers]);
123
+
124
+ // Clear search
125
+ const clearSearch = useCallback(() => {
126
+ setSearchQuery("");
127
+ setSearchResults([]);
128
+ setSearchError(null);
129
+ }, []);
130
+
131
+ // Start impersonation
132
+ const startImpersonation = useCallback(async user => {
133
+ await impersonateStore.startImpersonation(user);
134
+ }, []);
135
+
136
+ // Stop impersonation
137
+ const stopImpersonation = useCallback(async () => {
138
+ await impersonateStore.stopImpersonation();
139
+ }, []);
140
+
141
+ // Quick switch
142
+ const quickSwitch = useCallback(async user => {
143
+ await impersonateStore.quickSwitch(user);
144
+ }, []);
145
+
146
+ // Update header key
147
+ const updateHeaderKey = useCallback(async key => {
148
+ await impersonateStore.updateSettings({
149
+ headerKey: key
150
+ });
151
+ }, []);
152
+
153
+ // Update ignore patterns
154
+ const updateIgnorePatterns = useCallback(async patterns => {
155
+ await impersonateStore.updateSettings({
156
+ ignorePatterns: patterns
157
+ });
158
+ }, []);
159
+
160
+ // Update data nuke settings
161
+ const updateDataNukeSettings = useCallback(async settings => {
162
+ await impersonateStore.updateSettings({
163
+ dataNukeSettings: settings
164
+ });
165
+ }, []);
166
+
167
+ // Update show banner
168
+ const updateShowBanner = useCallback(async show => {
169
+ await impersonateStore.updateSettings({
170
+ showBanner: show
171
+ });
172
+ }, []);
173
+
174
+ // Remove from history
175
+ const removeFromHistory = useCallback(async userId => {
176
+ await impersonateStore.removeFromHistory(userId);
177
+ }, []);
178
+
179
+ // Clear history
180
+ const clearHistory = useCallback(async () => {
181
+ await impersonateStore.clearHistory();
182
+ }, []);
183
+ return {
184
+ // State
185
+ isActive: state.isActive,
186
+ currentUser: state.currentUser,
187
+ headerKey: state.headerKey,
188
+ ignorePatterns: state.ignorePatterns,
189
+ dataNukeSettings: state.dataNukeSettings,
190
+ showBanner: state.showBanner,
191
+ history: state.history,
192
+ // Search
193
+ searchQuery,
194
+ searchResults,
195
+ isSearching,
196
+ searchError,
197
+ searchUsers,
198
+ clearSearch,
199
+ // Actions
200
+ startImpersonation,
201
+ stopImpersonation,
202
+ quickSwitch,
203
+ // Settings
204
+ updateHeaderKey,
205
+ updateIgnorePatterns,
206
+ updateDataNukeSettings,
207
+ updateShowBanner,
208
+ // History
209
+ removeFromHistory,
210
+ clearHistory
211
+ };
212
+ }