@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,211 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ImpersonateBanner Component
5
+ *
6
+ * A floating banner that shows when impersonation is active.
7
+ * Features polished design with avatar and elegant animations.
8
+ * Can be positioned at top or bottom of the screen.
9
+ */
10
+
11
+ import React, { useSyncExternalStore } from "react";
12
+ import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
13
+ import { buoyColors, X, PowerToggleButton } from "@buoy-gg/shared-ui";
14
+ import { impersonateStore } from "../utils/impersonateStore";
15
+ import { UserAvatar } from "./UserAvatar";
16
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ /**
18
+ * Floating banner showing impersonation status
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function App() {
23
+ * return (
24
+ * <>
25
+ * <MainContent />
26
+ * <ImpersonateBanner
27
+ * position="top"
28
+ * offset={60}
29
+ * onPress={() => openImpersonateModal()}
30
+ * />
31
+ * </>
32
+ * );
33
+ * }
34
+ * ```
35
+ */
36
+ export function ImpersonateBanner({
37
+ onPress,
38
+ onStopPress,
39
+ position = "top",
40
+ offset = 50
41
+ }) {
42
+ const state = useSyncExternalStore(impersonateStore.subscribe, impersonateStore.getSnapshot, impersonateStore.getSnapshot);
43
+
44
+ // Don't render if not impersonating or banner is disabled
45
+ if (!state.isActive || !state.currentUser || !state.showBanner) {
46
+ return null;
47
+ }
48
+ const handlePauseResume = async () => {
49
+ if (state.isPaused) {
50
+ await impersonateStore.resumeImpersonation();
51
+ } else {
52
+ await impersonateStore.pauseImpersonation();
53
+ }
54
+ };
55
+ const displayName = state.currentUser.displayName || state.currentUser.email || state.currentUser.id;
56
+ const positionStyle = position === "top" ? {
57
+ top: offset
58
+ } : {
59
+ bottom: offset
60
+ };
61
+ const handleStop = async () => {
62
+ await impersonateStore.stopImpersonation();
63
+ };
64
+ return /*#__PURE__*/_jsx(View, {
65
+ style: [styles.container, positionStyle],
66
+ children: /*#__PURE__*/_jsxs(View, {
67
+ style: styles.banner,
68
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
69
+ style: styles.userSection,
70
+ onPress: onPress,
71
+ activeOpacity: 0.7,
72
+ children: [/*#__PURE__*/_jsx(UserAvatar, {
73
+ userId: state.currentUser.id,
74
+ name: displayName,
75
+ size: "small",
76
+ showActiveIndicator: false
77
+ }), /*#__PURE__*/_jsx(Text, {
78
+ style: [styles.userName, state.isPaused && styles.userNamePaused],
79
+ numberOfLines: 1,
80
+ children: displayName
81
+ })]
82
+ }), /*#__PURE__*/_jsx(PowerToggleButton, {
83
+ isEnabled: !state.isPaused,
84
+ onToggle: handlePauseResume,
85
+ size: "small",
86
+ accessibilityLabel: "Toggle impersonation"
87
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
88
+ style: styles.closeButton,
89
+ onPress: handleStop,
90
+ activeOpacity: 0.7,
91
+ children: /*#__PURE__*/_jsx(X, {
92
+ size: 12,
93
+ color: "rgba(255, 255, 255, 0.5)"
94
+ })
95
+ })]
96
+ })
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Minimal banner variant - just an indicator badge
102
+ */
103
+ export function ImpersonateBannerMinimal({
104
+ onPress
105
+ }) {
106
+ const state = useSyncExternalStore(impersonateStore.subscribe, impersonateStore.getSnapshot, impersonateStore.getSnapshot);
107
+ if (!state.isActive || !state.currentUser) {
108
+ return null;
109
+ }
110
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
111
+ style: styles.minimalBanner,
112
+ onPress: onPress,
113
+ activeOpacity: 0.8,
114
+ children: [/*#__PURE__*/_jsx(View, {
115
+ style: styles.minimalDot
116
+ }), /*#__PURE__*/_jsx(Text, {
117
+ style: styles.minimalText,
118
+ children: "IMP"
119
+ })]
120
+ });
121
+ }
122
+ const styles = StyleSheet.create({
123
+ container: {
124
+ position: "absolute",
125
+ left: 0,
126
+ right: 0,
127
+ alignItems: "center",
128
+ zIndex: 9999,
129
+ pointerEvents: "box-none"
130
+ },
131
+ banner: {
132
+ flexDirection: "row",
133
+ alignItems: "center",
134
+ backgroundColor: "rgba(20, 20, 20, 0.94)",
135
+ borderWidth: 1,
136
+ borderColor: buoyColors.success + "40",
137
+ borderRadius: 22,
138
+ paddingVertical: 7,
139
+ paddingLeft: 7,
140
+ paddingRight: 10,
141
+ gap: 10,
142
+ marginTop: 3,
143
+ shadowColor: "#000",
144
+ shadowOffset: {
145
+ width: 0,
146
+ height: 6
147
+ },
148
+ shadowOpacity: 0.3,
149
+ shadowRadius: 14,
150
+ elevation: 10
151
+ },
152
+ userSection: {
153
+ flexDirection: "row",
154
+ alignItems: "center",
155
+ gap: 10
156
+ },
157
+ userName: {
158
+ fontSize: 14,
159
+ fontWeight: "600",
160
+ color: "#fff",
161
+ maxWidth: 180
162
+ },
163
+ userNamePaused: {
164
+ opacity: 0.6
165
+ },
166
+ closeButton: {
167
+ width: 22,
168
+ height: 22,
169
+ borderRadius: 11,
170
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
171
+ alignItems: "center",
172
+ justifyContent: "center",
173
+ marginLeft: -4
174
+ },
175
+ // Minimal variant
176
+ minimalBanner: {
177
+ position: "absolute",
178
+ top: 60,
179
+ right: 16,
180
+ flexDirection: "row",
181
+ alignItems: "center",
182
+ backgroundColor: "rgba(26, 26, 26, 0.92)",
183
+ borderWidth: 1,
184
+ borderColor: buoyColors.success + "35",
185
+ borderRadius: 12,
186
+ paddingVertical: 5,
187
+ paddingHorizontal: 10,
188
+ gap: 5,
189
+ zIndex: 9999,
190
+ shadowColor: "#000",
191
+ shadowOffset: {
192
+ width: 0,
193
+ height: 2
194
+ },
195
+ shadowOpacity: 0.2,
196
+ shadowRadius: 6,
197
+ elevation: 4
198
+ },
199
+ minimalDot: {
200
+ width: 6,
201
+ height: 6,
202
+ borderRadius: 3,
203
+ backgroundColor: buoyColors.success
204
+ },
205
+ minimalText: {
206
+ fontSize: 10,
207
+ fontWeight: "700",
208
+ color: buoyColors.success,
209
+ letterSpacing: 0.5
210
+ }
211
+ });
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ImpersonateHistoryList Component
5
+ *
6
+ * List of recently impersonated users for quick switching.
7
+ * Uses polished UserCard component for consistent styling.
8
+ */
9
+
10
+ import React, { useCallback } from "react";
11
+ import { View, Text, TouchableOpacity, StyleSheet, FlatList } from "react-native";
12
+ import { buoyColors, Clock, Trash2, formatRelativeTime } from "@buoy-gg/shared-ui";
13
+ import { UserCard } from "./UserCard";
14
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ export function ImpersonateHistoryList({
16
+ history,
17
+ currentUserId,
18
+ onSelectUser,
19
+ onStopImpersonation,
20
+ onRemoveFromHistory,
21
+ onClearHistory
22
+ }) {
23
+ const renderHistoryItem = useCallback(({
24
+ item
25
+ }) => {
26
+ const {
27
+ user,
28
+ lastUsedAt
29
+ } = item;
30
+ const isActive = currentUserId === user.id;
31
+ return /*#__PURE__*/_jsx(UserCard, {
32
+ user: user,
33
+ isActive: isActive,
34
+ onPress: () => !isActive && onSelectUser(user),
35
+ onStop: isActive ? onStopImpersonation : undefined,
36
+ onRemove: onRemoveFromHistory ? () => onRemoveFromHistory(user.id) : undefined,
37
+ showRemoveButton: !!onRemoveFromHistory,
38
+ lastUsedTime: formatRelativeTime(new Date(lastUsedAt))
39
+ });
40
+ }, [currentUserId, onSelectUser, onStopImpersonation, onRemoveFromHistory]);
41
+ if (history.length === 0) {
42
+ return /*#__PURE__*/_jsxs(View, {
43
+ style: styles.emptyContainer,
44
+ children: [/*#__PURE__*/_jsx(View, {
45
+ style: styles.emptyIcon,
46
+ children: /*#__PURE__*/_jsx(Clock, {
47
+ size: 28,
48
+ color: buoyColors.textMuted
49
+ })
50
+ }), /*#__PURE__*/_jsx(Text, {
51
+ style: styles.emptyTitle,
52
+ children: "No History"
53
+ }), /*#__PURE__*/_jsx(Text, {
54
+ style: styles.emptyDescription,
55
+ children: "Users you impersonate will appear here for quick access"
56
+ })]
57
+ });
58
+ }
59
+ return /*#__PURE__*/_jsxs(View, {
60
+ style: styles.container,
61
+ children: [/*#__PURE__*/_jsxs(View, {
62
+ style: styles.headerRow,
63
+ children: [/*#__PURE__*/_jsx(Text, {
64
+ style: styles.headerTitle,
65
+ children: "Recent"
66
+ }), /*#__PURE__*/_jsx(View, {
67
+ style: styles.headerBadge,
68
+ children: /*#__PURE__*/_jsx(Text, {
69
+ style: styles.headerBadgeText,
70
+ children: history.length
71
+ })
72
+ }), /*#__PURE__*/_jsx(View, {
73
+ style: styles.headerSpacer
74
+ }), onClearHistory && /*#__PURE__*/_jsx(TouchableOpacity, {
75
+ style: styles.clearButton,
76
+ onPress: onClearHistory,
77
+ activeOpacity: 0.7,
78
+ children: /*#__PURE__*/_jsx(Trash2, {
79
+ size: 12,
80
+ color: buoyColors.textMuted
81
+ })
82
+ })]
83
+ }), /*#__PURE__*/_jsx(FlatList, {
84
+ data: history,
85
+ keyExtractor: item => item.user.id,
86
+ renderItem: renderHistoryItem,
87
+ ItemSeparatorComponent: () => /*#__PURE__*/_jsx(View, {
88
+ style: styles.separator
89
+ }),
90
+ contentContainerStyle: styles.listContent,
91
+ showsVerticalScrollIndicator: false
92
+ })]
93
+ });
94
+ }
95
+ const styles = StyleSheet.create({
96
+ container: {
97
+ flex: 1,
98
+ paddingHorizontal: 16
99
+ },
100
+ headerRow: {
101
+ flexDirection: "row",
102
+ alignItems: "center",
103
+ paddingVertical: 10,
104
+ gap: 8
105
+ },
106
+ headerTitle: {
107
+ fontSize: 12,
108
+ fontWeight: "600",
109
+ color: buoyColors.textMuted,
110
+ textTransform: "uppercase",
111
+ letterSpacing: 0.5
112
+ },
113
+ headerBadge: {
114
+ backgroundColor: buoyColors.hover,
115
+ paddingHorizontal: 6,
116
+ paddingVertical: 2,
117
+ borderRadius: 4
118
+ },
119
+ headerBadgeText: {
120
+ fontSize: 10,
121
+ fontWeight: "600",
122
+ color: buoyColors.textMuted
123
+ },
124
+ headerSpacer: {
125
+ flex: 1
126
+ },
127
+ clearButton: {
128
+ width: 24,
129
+ height: 24,
130
+ alignItems: "center",
131
+ justifyContent: "center",
132
+ backgroundColor: buoyColors.hover,
133
+ borderRadius: 4
134
+ },
135
+ listContent: {
136
+ paddingBottom: 20
137
+ },
138
+ separator: {
139
+ height: 8
140
+ },
141
+ emptyContainer: {
142
+ flex: 1,
143
+ alignItems: "center",
144
+ justifyContent: "center",
145
+ paddingHorizontal: 32,
146
+ paddingVertical: 48
147
+ },
148
+ emptyIcon: {
149
+ width: 56,
150
+ height: 56,
151
+ borderRadius: 28,
152
+ backgroundColor: buoyColors.hover,
153
+ alignItems: "center",
154
+ justifyContent: "center",
155
+ marginBottom: 16
156
+ },
157
+ emptyTitle: {
158
+ fontSize: 16,
159
+ fontWeight: "600",
160
+ color: buoyColors.text,
161
+ marginBottom: 6
162
+ },
163
+ emptyDescription: {
164
+ fontSize: 14,
165
+ color: buoyColors.textSecondary,
166
+ textAlign: "center"
167
+ }
168
+ });
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ImpersonateModal Component
5
+ *
6
+ * Main modal for the impersonation tool. Uses JsModal with ModalHeader
7
+ * pattern matching other Buoy dev tools (Network, Storage, etc.).
8
+ */
9
+
10
+ import React, { useState, useCallback, useRef, useEffect } from "react";
11
+ import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
12
+ import { JsModal as JsModalBase, TabSelector, ModalHeader, macOSColors, Search, X } from "@buoy-gg/shared-ui";
13
+ import { useImpersonate } from "../hooks/useImpersonate";
14
+ import { useAutoClearReactQuery } from "../hooks/useAutoClearReactQuery";
15
+ import { useAutoClearRedux } from "../hooks/useAutoClearRedux";
16
+ import { useAutoClearAsyncStorage } from "../hooks/useAutoClearAsyncStorage";
17
+ import { UserSearchView } from "./UserSearchView";
18
+ import { ImpersonateHistoryList } from "./ImpersonateHistoryList";
19
+ import { DataNukeSettings as DataNukeSettingsComponent } from "./DataNukeSettings";
20
+
21
+ // Type assertion to work around React types compatibility issue with memo components
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
+ const JsModal = JsModalBase;
25
+
26
+ // Local Tab type (matches shared-ui's TabSelector)
27
+
28
+ const ALL_TABS = [{
29
+ key: "search",
30
+ label: "Search"
31
+ }, {
32
+ key: "history",
33
+ label: "History"
34
+ }, {
35
+ key: "settings",
36
+ label: "Settings"
37
+ }];
38
+ const TABS_WITHOUT_SETTINGS = [{
39
+ key: "search",
40
+ label: "Search"
41
+ }, {
42
+ key: "history",
43
+ label: "History"
44
+ }];
45
+ export function ImpersonateModal({
46
+ visible,
47
+ onClose,
48
+ onBack,
49
+ onMinimize,
50
+ onSearchUsers,
51
+ onClearReactQuery,
52
+ onClearRedux,
53
+ onClearAsyncStorage,
54
+ onClearMMKV,
55
+ showSettingsTab = true
56
+ }) {
57
+ // Select tabs based on showSettingsTab prop
58
+ const tabs = showSettingsTab ? ALL_TABS : TABS_WITHOUT_SETTINGS;
59
+ const [activeTab, setActiveTab] = useState("search");
60
+ const [isSearchActive, setIsSearchActive] = useState(false);
61
+ const [searchText, setSearchText] = useState("");
62
+ const searchInputRef = useRef(null);
63
+
64
+ // Auto-detect React Query and get clear function if user didn't provide one
65
+ const {
66
+ clearCache: autoClearReactQuery,
67
+ isAvailable: isReactQueryDetected
68
+ } = useAutoClearReactQuery();
69
+
70
+ // Auto-detect Redux and get reset function if user didn't provide one
71
+ const {
72
+ resetStore: autoClearRedux,
73
+ isAvailable: isReduxDetected
74
+ } = useAutoClearRedux();
75
+
76
+ // Auto-detect AsyncStorage and get clear function if user didn't provide one
77
+ const {
78
+ clearStorage: autoClearAsyncStorage,
79
+ isAvailable: isAsyncStorageDetected
80
+ } = useAutoClearAsyncStorage();
81
+
82
+ // Use user-provided callbacks, or fall back to auto-detected ones
83
+ const effectiveClearReactQuery = onClearReactQuery ?? autoClearReactQuery ?? undefined;
84
+ const effectiveClearRedux = onClearRedux ?? autoClearRedux ?? undefined;
85
+ const effectiveClearAsyncStorage = onClearAsyncStorage ?? autoClearAsyncStorage ?? undefined;
86
+
87
+ // Detection status for settings display
88
+ // Shows "detected" if either user provided callback OR auto-detected
89
+ const detectionStatus = {
90
+ reactQuery: !!onClearReactQuery || isReactQueryDetected,
91
+ redux: !!onClearRedux || isReduxDetected,
92
+ asyncStorage: !!onClearAsyncStorage || isAsyncStorageDetected,
93
+ mmkv: !!onClearMMKV // TODO: implement auto-detection
94
+ };
95
+ const {
96
+ isActive,
97
+ currentUser,
98
+ headerKey,
99
+ dataNukeSettings,
100
+ showBanner,
101
+ searchQuery,
102
+ searchResults,
103
+ isSearching,
104
+ searchError,
105
+ searchUsers,
106
+ startImpersonation,
107
+ stopImpersonation,
108
+ updateHeaderKey,
109
+ updateDataNukeSettings,
110
+ updateShowBanner,
111
+ history,
112
+ removeFromHistory,
113
+ clearHistory
114
+ } = useImpersonate({
115
+ onSearchUsers,
116
+ onClearReactQuery: effectiveClearReactQuery,
117
+ onClearRedux: effectiveClearRedux,
118
+ onClearAsyncStorage: effectiveClearAsyncStorage,
119
+ onClearMMKV
120
+ });
121
+
122
+ // Focus input when search becomes active
123
+ useEffect(() => {
124
+ if (isSearchActive && searchInputRef.current) {
125
+ setTimeout(() => {
126
+ searchInputRef.current?.focus();
127
+ }, 100);
128
+ }
129
+ }, [isSearchActive]);
130
+
131
+ // Reset to search tab if settings tab is hidden while active
132
+ useEffect(() => {
133
+ if (!showSettingsTab && activeTab === "settings") {
134
+ setActiveTab("search");
135
+ }
136
+ }, [showSettingsTab, activeTab]);
137
+ const handleSearchTextChange = useCallback(text => {
138
+ setSearchText(text);
139
+ }, []);
140
+ const handleSubmitSearch = useCallback(() => {
141
+ if (searchText.trim()) {
142
+ searchUsers(searchText.trim());
143
+ }
144
+ setIsSearchActive(false);
145
+ }, [searchText, searchUsers]);
146
+ const handleSelectUser = useCallback(async user => {
147
+ await startImpersonation(user);
148
+ }, [startImpersonation]);
149
+ const handleSaveSettings = useCallback(async (newHeaderKey, settings, newShowBanner) => {
150
+ // Save header key, data nuke settings, and show banner
151
+ await updateHeaderKey(newHeaderKey);
152
+ await updateDataNukeSettings(settings);
153
+ await updateShowBanner(newShowBanner);
154
+ }, [updateHeaderKey, updateDataNukeSettings, updateShowBanner]);
155
+ const handleTabChange = useCallback(tabKey => {
156
+ setActiveTab(tabKey);
157
+ // Close search when switching tabs
158
+ setIsSearchActive(false);
159
+ }, []);
160
+
161
+ // Header content renderer (matching NetworkModal pattern)
162
+ const renderHeaderContent = () => {
163
+ return /*#__PURE__*/_jsxs(ModalHeader, {
164
+ children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
165
+ onBack: onBack
166
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
167
+ title: "",
168
+ children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
169
+ style: styles.headerSearchContainer,
170
+ children: [/*#__PURE__*/_jsx(Search, {
171
+ size: 14,
172
+ color: macOSColors.text.secondary
173
+ }), /*#__PURE__*/_jsx(TextInput, {
174
+ ref: searchInputRef,
175
+ style: styles.headerSearchInput,
176
+ placeholder: "Search by email, name, or ID...",
177
+ placeholderTextColor: macOSColors.text.muted,
178
+ value: searchText,
179
+ onChangeText: handleSearchTextChange,
180
+ onSubmitEditing: handleSubmitSearch,
181
+ onBlur: () => setIsSearchActive(false),
182
+ autoCapitalize: "none",
183
+ autoCorrect: false,
184
+ returnKeyType: "search"
185
+ }), searchText.length > 0 ? /*#__PURE__*/_jsx(TouchableOpacity, {
186
+ onPress: () => {
187
+ setSearchText("");
188
+ setIsSearchActive(false);
189
+ },
190
+ style: styles.headerSearchClear,
191
+ children: /*#__PURE__*/_jsx(X, {
192
+ size: 14,
193
+ color: macOSColors.text.secondary
194
+ })
195
+ }) : /*#__PURE__*/_jsx(TouchableOpacity, {
196
+ onPress: () => setIsSearchActive(false),
197
+ style: styles.headerSearchClear,
198
+ children: /*#__PURE__*/_jsx(X, {
199
+ size: 14,
200
+ color: macOSColors.text.muted
201
+ })
202
+ })]
203
+ }) : /*#__PURE__*/_jsx(TabSelector, {
204
+ tabs: tabs,
205
+ activeTab: activeTab,
206
+ onTabChange: handleTabChange
207
+ })
208
+ }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
209
+ children: activeTab === "search" && !isSearchActive && /*#__PURE__*/_jsx(TouchableOpacity, {
210
+ onPress: () => setIsSearchActive(true),
211
+ style: styles.headerActionButton,
212
+ children: /*#__PURE__*/_jsx(Search, {
213
+ size: 14,
214
+ color: macOSColors.text.secondary
215
+ })
216
+ })
217
+ })]
218
+ });
219
+ };
220
+ if (!visible) {
221
+ return null;
222
+ }
223
+ return /*#__PURE__*/_jsx(JsModal, {
224
+ visible: visible,
225
+ onClose: onClose,
226
+ onMinimize: onMinimize,
227
+ header: {
228
+ showToggleButton: true,
229
+ customContent: renderHeaderContent()
230
+ },
231
+ enablePersistence: true,
232
+ initialMode: "bottomSheet",
233
+ disableScrollWrapper: true,
234
+ children: /*#__PURE__*/_jsxs(View, {
235
+ style: styles.container,
236
+ children: [activeTab === "search" && /*#__PURE__*/_jsx(UserSearchView, {
237
+ currentUser: currentUser,
238
+ isActive: isActive,
239
+ searchQuery: searchQuery,
240
+ searchResults: searchResults,
241
+ isSearching: isSearching,
242
+ searchError: searchError,
243
+ onSelectUser: handleSelectUser,
244
+ onStopImpersonation: stopImpersonation,
245
+ searchAvailable: !!onSearchUsers
246
+ }), activeTab === "history" && /*#__PURE__*/_jsx(ImpersonateHistoryList, {
247
+ history: history,
248
+ currentUserId: currentUser?.id ?? null,
249
+ onSelectUser: handleSelectUser,
250
+ onStopImpersonation: stopImpersonation,
251
+ onRemoveFromHistory: removeFromHistory,
252
+ onClearHistory: clearHistory
253
+ }), activeTab === "settings" && /*#__PURE__*/_jsx(DataNukeSettingsComponent, {
254
+ headerKey: headerKey,
255
+ settings: dataNukeSettings,
256
+ showBanner: showBanner,
257
+ onSave: handleSaveSettings,
258
+ onShowBannerChange: updateShowBanner,
259
+ detectionStatus: detectionStatus
260
+ })]
261
+ })
262
+ });
263
+ }
264
+ const styles = StyleSheet.create({
265
+ container: {
266
+ flex: 1
267
+ },
268
+ headerSearchContainer: {
269
+ flexDirection: "row",
270
+ alignItems: "center",
271
+ backgroundColor: macOSColors.background.input,
272
+ borderRadius: 6,
273
+ borderWidth: 1,
274
+ borderColor: macOSColors.border.input,
275
+ paddingHorizontal: 12,
276
+ paddingVertical: 8,
277
+ marginTop: 8,
278
+ flex: 1,
279
+ marginBottom: 2
280
+ },
281
+ headerSearchInput: {
282
+ flex: 1,
283
+ color: macOSColors.text.primary,
284
+ fontSize: 13,
285
+ marginLeft: 6,
286
+ paddingVertical: 2
287
+ },
288
+ headerSearchClear: {
289
+ marginLeft: 6,
290
+ padding: 4
291
+ },
292
+ headerActionButton: {
293
+ width: 28,
294
+ height: 28,
295
+ borderRadius: 6,
296
+ backgroundColor: macOSColors.background.hover,
297
+ justifyContent: "center",
298
+ alignItems: "center"
299
+ }
300
+ });