@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.
- package/LICENSE +58 -0
- package/lib/commonjs/impersonate/components/DataNukeSettings.js +715 -0
- package/lib/commonjs/impersonate/components/ImpersonateBanner.js +217 -0
- package/lib/commonjs/impersonate/components/ImpersonateHistoryList.js +173 -0
- package/lib/commonjs/impersonate/components/ImpersonateModal.js +304 -0
- package/lib/commonjs/impersonate/components/ImpersonateStatusBar.js +130 -0
- package/lib/commonjs/impersonate/components/UserAvatar.js +146 -0
- package/lib/commonjs/impersonate/components/UserCard.js +200 -0
- package/lib/commonjs/impersonate/components/UserSearchView.js +227 -0
- package/lib/commonjs/impersonate/components/index.js +85 -0
- package/lib/commonjs/impersonate/hooks/index.js +64 -0
- package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +144 -0
- package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +155 -0
- package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +188 -0
- package/lib/commonjs/impersonate/hooks/useImpersonate.js +215 -0
- package/lib/commonjs/impersonate/hooks/useImpersonateHistory.js +56 -0
- package/lib/commonjs/impersonate/index.js +49 -0
- package/lib/commonjs/impersonate/types/index.js +16 -0
- package/lib/commonjs/impersonate/types/types.js +1 -0
- package/lib/commonjs/impersonate/utils/impersonateListener.js +280 -0
- package/lib/commonjs/impersonate/utils/impersonateStore.js +607 -0
- package/lib/commonjs/impersonate/utils/index.js +49 -0
- package/lib/commonjs/index.js +118 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +214 -0
- package/lib/module/impersonate/components/DataNukeSettings.js +710 -0
- package/lib/module/impersonate/components/ImpersonateBanner.js +211 -0
- package/lib/module/impersonate/components/ImpersonateHistoryList.js +168 -0
- package/lib/module/impersonate/components/ImpersonateModal.js +300 -0
- package/lib/module/impersonate/components/ImpersonateStatusBar.js +125 -0
- package/lib/module/impersonate/components/UserAvatar.js +140 -0
- package/lib/module/impersonate/components/UserCard.js +195 -0
- package/lib/module/impersonate/components/UserSearchView.js +222 -0
- package/lib/module/impersonate/components/index.js +11 -0
- package/lib/module/impersonate/hooks/index.js +7 -0
- package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +140 -0
- package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +151 -0
- package/lib/module/impersonate/hooks/useAutoClearRedux.js +183 -0
- package/lib/module/impersonate/hooks/useImpersonate.js +212 -0
- package/lib/module/impersonate/hooks/useImpersonateHistory.js +52 -0
- package/lib/module/impersonate/index.js +13 -0
- package/lib/module/impersonate/types/index.js +3 -0
- package/lib/module/impersonate/types/types.js +1 -0
- package/lib/module/impersonate/utils/impersonateListener.js +271 -0
- package/lib/module/impersonate/utils/impersonateStore.js +604 -0
- package/lib/module/impersonate/utils/index.js +4 -0
- package/lib/module/index.js +103 -0
- package/lib/module/preset.js +209 -0
- package/lib/typescript/impersonate/components/DataNukeSettings.d.ts +37 -0
- package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts +40 -0
- package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts +24 -0
- package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/ImpersonateModal.d.ts +10 -0
- package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts +15 -0
- package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/UserAvatar.d.ts +32 -0
- package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/UserCard.d.ts +28 -0
- package/lib/typescript/impersonate/components/UserCard.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/UserSearchView.d.ts +31 -0
- package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +1 -0
- package/lib/typescript/impersonate/components/index.d.ts +16 -0
- package/lib/typescript/impersonate/components/index.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/index.d.ts +11 -0
- package/lib/typescript/impersonate/hooks/index.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts +48 -0
- package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts +48 -0
- package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts +78 -0
- package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/useImpersonate.d.ts +76 -0
- package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +1 -0
- package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts +43 -0
- package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +1 -0
- package/lib/typescript/impersonate/index.d.ts +5 -0
- package/lib/typescript/impersonate/index.d.ts.map +1 -0
- package/lib/typescript/impersonate/types/index.d.ts +2 -0
- package/lib/typescript/impersonate/types/index.d.ts.map +1 -0
- package/lib/typescript/impersonate/types/types.d.ts +177 -0
- package/lib/typescript/impersonate/types/types.d.ts.map +1 -0
- package/lib/typescript/impersonate/utils/impersonateListener.d.ts +115 -0
- package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +1 -0
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts +151 -0
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -0
- package/lib/typescript/impersonate/utils/index.d.ts +3 -0
- package/lib/typescript/impersonate/utils/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +80 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +71 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- 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
|
+
}
|