@buoy-gg/impersonate 7.0.32 → 7.0.34

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.
@@ -39,51 +39,40 @@ function ToggleRow({
39
39
  dangerous,
40
40
  detected
41
41
  }) {
42
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
43
- style: styles.toggleRow,
44
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
45
- style: styles.toggleInfo,
46
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
47
- style: styles.toggleHeader,
48
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
49
- style: [styles.toggleLabel, dangerous && styles.dangerousLabel],
50
- children: label
51
- }), detected !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
52
- style: [styles.detectedBadge, detected ? styles.detectedBadgeActive : styles.detectedBadgeInactive],
53
- children: [detected ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Check, {
54
- size: 10,
55
- color: _sharedUi.buoyColors.success
56
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
57
- size: 10,
58
- color: _sharedUi.night.color.textTertiary
59
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
60
- style: [styles.detectedText, detected ? styles.detectedTextActive : styles.detectedTextInactive],
61
- children: detected ? "Detected" : "Not Detected"
62
- })]
63
- }), recommended && detected !== false && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
64
- style: styles.recommendedBadge,
65
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
66
- size: 10,
67
- color: _sharedUi.night.color.accent
68
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
69
- style: styles.recommendedText,
70
- children: "Recommended"
71
- })]
72
- })]
42
+ const accessory = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
43
+ children: [detected !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
44
+ style: [styles.detectedBadge, detected ? styles.detectedBadgeActive : styles.detectedBadgeInactive],
45
+ children: [detected ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Check, {
46
+ size: 10,
47
+ color: _sharedUi.buoyColors.success
48
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
49
+ size: 10,
50
+ color: _sharedUi.night.color.textTertiary
73
51
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
74
- style: styles.toggleDescription,
75
- children: description
52
+ style: [styles.detectedText, detected ? styles.detectedTextActive : styles.detectedTextInactive],
53
+ children: detected ? "Detected" : "Not Detected"
54
+ })]
55
+ }), recommended && detected !== false && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
56
+ style: styles.recommendedBadge,
57
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
58
+ size: 10,
59
+ color: _sharedUi.night.color.accent
60
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
61
+ style: styles.recommendedText,
62
+ children: "Recommended"
76
63
  })]
77
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Switch, {
78
- value: value,
79
- onValueChange: onChange,
80
- trackColor: {
81
- false: _sharedUi.night.color.border,
82
- true: dangerous ? _sharedUi.night.color.warning + "50" : _sharedUi.night.color.accent + "50"
83
- },
84
- thumbColor: value ? dangerous ? _sharedUi.night.color.warning : _sharedUi.night.color.accent : _sharedUi.night.color.knob
85
64
  })]
86
65
  });
66
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToggleRow, {
67
+ variant: "plain",
68
+ label: label,
69
+ description: description,
70
+ value: value,
71
+ onValueChange: onChange,
72
+ tone: dangerous ? "warning" : "accent",
73
+ emphasizeLabel: dangerous,
74
+ accessory: accessory
75
+ });
87
76
  }
88
77
  function DataNukeSettings({
89
78
  headerKey,
@@ -560,31 +549,6 @@ const styles = _reactNative.StyleSheet.create({
560
549
  color: _sharedUi.night.color.accent,
561
550
  fontWeight: "500"
562
551
  },
563
- toggleRow: {
564
- flexDirection: "row",
565
- alignItems: "center",
566
- justifyContent: "space-between",
567
- paddingVertical: 12,
568
- paddingHorizontal: 12
569
- },
570
- toggleInfo: {
571
- flex: 1,
572
- marginRight: 12
573
- },
574
- toggleHeader: {
575
- flexDirection: "row",
576
- alignItems: "center",
577
- gap: 8,
578
- marginBottom: 3
579
- },
580
- toggleLabel: {
581
- fontSize: 14,
582
- fontWeight: "500",
583
- color: _sharedUi.night.color.text
584
- },
585
- dangerousLabel: {
586
- color: _sharedUi.night.color.warning
587
- },
588
552
  recommendedBadge: {
589
553
  flexDirection: "row",
590
554
  alignItems: "center",
@@ -623,11 +587,6 @@ const styles = _reactNative.StyleSheet.create({
623
587
  detectedTextInactive: {
624
588
  color: _sharedUi.night.color.textTertiary
625
589
  },
626
- toggleDescription: {
627
- fontSize: 12,
628
- color: _sharedUi.night.color.textTertiary,
629
- lineHeight: 16
630
- },
631
590
  divider: {
632
591
  height: 1,
633
592
  backgroundColor: _sharedUi.night.color.separator,
@@ -59,21 +59,14 @@ function ImpersonateHistoryList({
59
59
  });
60
60
  }, [currentUserId, onSelectUser, onStopImpersonation, onRemoveFromHistory]);
61
61
  if (history.length === 0) {
62
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
63
- style: styles.emptyContainer,
64
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
65
- style: styles.emptyIcon,
66
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Clock, {
67
- size: 28,
68
- color: _sharedUi.night.color.textTertiary
69
- })
70
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
71
- style: styles.emptyTitle,
72
- children: "No History"
73
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
74
- style: styles.emptyDescription,
75
- children: "Users you impersonate will appear here for quick access"
76
- })]
62
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
63
+ testID: "buoy-impersonate-history-empty"
64
+ // disguise = "impersonating another user" — the tool's own state,
65
+ // with nothing worn yet.
66
+ ,
67
+ otto: "disguise",
68
+ title: "No History",
69
+ message: "Users you impersonate will appear here for quick access"
77
70
  });
78
71
  }
79
72
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -64,6 +64,12 @@ function ImpersonateModal({
64
64
  const tabs = showSettingsTab ? ALL_TABS : TABS_WITHOUT_SETTINGS;
65
65
  const [activeTab, setActiveTab] = (0, _react.useState)("search");
66
66
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
67
+ // True while the header's clear ✕ is being pressed. With a mouse, pressing
68
+ // the ✕ blurs the input FIRST, and the blur handler closes search — which
69
+ // unmounts the ✕ before its own press can fire, so the query was never
70
+ // cleared (desktop). Press-in lands before the blur, so the blur handler can
71
+ // tell this dismissal apart from a click elsewhere.
72
+ const clearPressRef = (0, _react.useRef)(false);
67
73
  const [searchText, setSearchText] = (0, _react.useState)("");
68
74
  const searchInputRef = (0, _react.useRef)(null);
69
75
 
@@ -171,42 +177,23 @@ function ImpersonateModal({
171
177
  onBack: onBack
172
178
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
173
179
  title: "",
174
- children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
175
- style: styles.headerSearchContainer,
176
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
177
- size: 14,
178
- color: _sharedUi.night.color.textSecondary
179
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
180
- ref: searchInputRef,
181
- testID: "buoy-impersonate-search-input",
182
- style: styles.headerSearchInput,
183
- placeholder: "Search by email, name, or ID...",
184
- placeholderTextColor: _sharedUi.night.color.placeholder,
185
- value: searchText,
186
- onChangeText: handleSearchTextChange,
187
- onSubmitEditing: handleSubmitSearch,
188
- onBlur: () => setIsSearchActive(false),
189
- autoCapitalize: "none",
190
- autoCorrect: false,
191
- returnKeyType: "search"
192
- }), searchText.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
193
- onPress: () => {
194
- setSearchText("");
195
- setIsSearchActive(false);
196
- },
197
- style: styles.headerSearchClear,
198
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
199
- size: 14,
200
- color: _sharedUi.night.color.textSecondary
201
- })
202
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
203
- onPress: () => setIsSearchActive(false),
204
- style: styles.headerSearchClear,
205
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
206
- size: 14,
207
- color: _sharedUi.night.color.textTertiary
208
- })
209
- })]
180
+ children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
181
+ value: searchText,
182
+ onChangeText: handleSearchTextChange
183
+ // ✕ clears and closes; with no text it simply closes.
184
+ ,
185
+ onCancel: () => {
186
+ setSearchText("");
187
+ setIsSearchActive(false);
188
+ },
189
+ onDismiss: () => setIsSearchActive(false),
190
+ dismissOnBlur: true,
191
+ placeholder: "Search by email, name, or ID...",
192
+ iconSize: 14,
193
+ testID: "buoy-impersonate-search-input",
194
+ inputProps: {
195
+ onSubmitEditing: handleSubmitSearch
196
+ }
210
197
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
211
198
  tabs: tabs,
212
199
  activeTab: activeTab,
@@ -321,30 +308,6 @@ const styles = _reactNative.StyleSheet.create({
321
308
  container: {
322
309
  flex: 1
323
310
  },
324
- headerSearchContainer: {
325
- flexDirection: "row",
326
- alignItems: "center",
327
- backgroundColor: _sharedUi.night.color.surfaceElevated,
328
- borderRadius: 6,
329
- borderWidth: 1,
330
- borderColor: _sharedUi.night.color.border,
331
- paddingHorizontal: 12,
332
- paddingVertical: 8,
333
- marginTop: 8,
334
- flex: 1,
335
- marginBottom: 2
336
- },
337
- headerSearchInput: {
338
- flex: 1,
339
- color: _sharedUi.night.color.text,
340
- fontSize: 13,
341
- marginLeft: 6,
342
- paddingVertical: 2
343
- },
344
- headerSearchClear: {
345
- marginLeft: 6,
346
- padding: 4
347
- },
348
311
  headerActionButton: {
349
312
  width: 28,
350
313
  height: 28,
@@ -71,14 +71,47 @@ function toWireState(state) {
71
71
  }))
72
72
  };
73
73
  }
74
+
75
+ /**
76
+ * Users this device has actually seen — search results from the app's own
77
+ * directory. `startImpersonation` refuses anything else: a live eval showed
78
+ * the model INVENTING a plausible user object when search returned nothing,
79
+ * and impersonating a fabricated user is a silent half-state (headers patched
80
+ * for a person who does not exist). History and the current user also count,
81
+ * so the desktop's re-impersonate-from-history flow keeps working.
82
+ */
83
+ const seenUserIds = new Set();
84
+ function rememberUsers(users) {
85
+ for (const u of users) {
86
+ if (u && typeof u === "object" && u.id) seenUserIds.add(String(u.id));
87
+ }
88
+ }
89
+ function isKnownUser(user) {
90
+ if (!user || typeof user !== "object" || !user.id) return false;
91
+ if (seenUserIds.has(String(user.id))) return true;
92
+ const state = _impersonateStore.impersonateStore.getState();
93
+ if (state.currentUser?.id === user.id) return true;
94
+ return state.history.some(entry => entry.user.id === user.id);
95
+ }
74
96
  function buildActions(onSearchUsers) {
75
97
  return {
76
98
  searchUsers: async params => {
77
99
  const query = String(params?.query ?? "");
78
100
  const users = await onSearchUsers(query);
101
+ if (Array.isArray(users)) rememberUsers(users);
79
102
  return Array.isArray(users) ? users.map(toWireUser) : users;
80
103
  },
81
- startImpersonation: params => _impersonateStore.impersonateStore.startImpersonation(params.user),
104
+ startImpersonation: async params => {
105
+ const user = params?.user;
106
+ if (!isKnownUser(user)) {
107
+ throw new Error("That user isn't in this app's directory. Call searchUsers and impersonate one of its results — a user object that didn't come from search impersonates nobody.");
108
+ }
109
+ await _impersonateStore.impersonateStore.startImpersonation(user);
110
+ return {
111
+ ok: true,
112
+ userId: user.id
113
+ };
114
+ },
82
115
  stopImpersonation: () => _impersonateStore.impersonateStore.stopImpersonation(),
83
116
  pauseImpersonation: () => _impersonateStore.impersonateStore.pauseImpersonation(),
84
117
  resumeImpersonation: () => _impersonateStore.impersonateStore.resumeImpersonation(),
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import React, { useState, useEffect, useMemo } from "react";
11
- import { View, Text, TextInput, StyleSheet, Switch, ScrollView, TouchableOpacity } from "react-native";
12
- import { CollapsibleSection, buoyColors, night, Settings, AlertTriangle, Info, Zap, RefreshCw, Check, X, Copy, Eye, copyToClipboard } from "@buoy-gg/shared-ui";
11
+ import { View, Text, TextInput, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
12
+ import { CollapsibleSection, buoyColors, night, Settings, AlertTriangle, Info, Zap, RefreshCw, Check, X, Copy, Eye, copyToClipboard, ToggleRow as SharedToggleRow } from "@buoy-gg/shared-ui";
13
13
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
14
14
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
15
15
  const DEFAULT_HEADER_KEY = "x-impersonate-user-id";
@@ -34,51 +34,40 @@ function ToggleRow({
34
34
  dangerous,
35
35
  detected
36
36
  }) {
37
- return /*#__PURE__*/_jsxs(View, {
38
- style: styles.toggleRow,
39
- children: [/*#__PURE__*/_jsxs(View, {
40
- style: styles.toggleInfo,
41
- children: [/*#__PURE__*/_jsxs(View, {
42
- style: styles.toggleHeader,
43
- children: [/*#__PURE__*/_jsx(Text, {
44
- style: [styles.toggleLabel, dangerous && styles.dangerousLabel],
45
- children: label
46
- }), detected !== undefined && /*#__PURE__*/_jsxs(View, {
47
- style: [styles.detectedBadge, detected ? styles.detectedBadgeActive : styles.detectedBadgeInactive],
48
- children: [detected ? /*#__PURE__*/_jsx(Check, {
49
- size: 10,
50
- color: buoyColors.success
51
- }) : /*#__PURE__*/_jsx(X, {
52
- size: 10,
53
- color: night.color.textTertiary
54
- }), /*#__PURE__*/_jsx(Text, {
55
- style: [styles.detectedText, detected ? styles.detectedTextActive : styles.detectedTextInactive],
56
- children: detected ? "Detected" : "Not Detected"
57
- })]
58
- }), recommended && detected !== false && /*#__PURE__*/_jsxs(View, {
59
- style: styles.recommendedBadge,
60
- children: [/*#__PURE__*/_jsx(Zap, {
61
- size: 10,
62
- color: night.color.accent
63
- }), /*#__PURE__*/_jsx(Text, {
64
- style: styles.recommendedText,
65
- children: "Recommended"
66
- })]
67
- })]
37
+ const accessory = /*#__PURE__*/_jsxs(_Fragment, {
38
+ children: [detected !== undefined && /*#__PURE__*/_jsxs(View, {
39
+ style: [styles.detectedBadge, detected ? styles.detectedBadgeActive : styles.detectedBadgeInactive],
40
+ children: [detected ? /*#__PURE__*/_jsx(Check, {
41
+ size: 10,
42
+ color: buoyColors.success
43
+ }) : /*#__PURE__*/_jsx(X, {
44
+ size: 10,
45
+ color: night.color.textTertiary
68
46
  }), /*#__PURE__*/_jsx(Text, {
69
- style: styles.toggleDescription,
70
- children: description
47
+ style: [styles.detectedText, detected ? styles.detectedTextActive : styles.detectedTextInactive],
48
+ children: detected ? "Detected" : "Not Detected"
49
+ })]
50
+ }), recommended && detected !== false && /*#__PURE__*/_jsxs(View, {
51
+ style: styles.recommendedBadge,
52
+ children: [/*#__PURE__*/_jsx(Zap, {
53
+ size: 10,
54
+ color: night.color.accent
55
+ }), /*#__PURE__*/_jsx(Text, {
56
+ style: styles.recommendedText,
57
+ children: "Recommended"
71
58
  })]
72
- }), /*#__PURE__*/_jsx(Switch, {
73
- value: value,
74
- onValueChange: onChange,
75
- trackColor: {
76
- false: night.color.border,
77
- true: dangerous ? night.color.warning + "50" : night.color.accent + "50"
78
- },
79
- thumbColor: value ? dangerous ? night.color.warning : night.color.accent : night.color.knob
80
59
  })]
81
60
  });
61
+ return /*#__PURE__*/_jsx(SharedToggleRow, {
62
+ variant: "plain",
63
+ label: label,
64
+ description: description,
65
+ value: value,
66
+ onValueChange: onChange,
67
+ tone: dangerous ? "warning" : "accent",
68
+ emphasizeLabel: dangerous,
69
+ accessory: accessory
70
+ });
82
71
  }
83
72
  export function DataNukeSettings({
84
73
  headerKey,
@@ -555,31 +544,6 @@ const styles = StyleSheet.create({
555
544
  color: night.color.accent,
556
545
  fontWeight: "500"
557
546
  },
558
- toggleRow: {
559
- flexDirection: "row",
560
- alignItems: "center",
561
- justifyContent: "space-between",
562
- paddingVertical: 12,
563
- paddingHorizontal: 12
564
- },
565
- toggleInfo: {
566
- flex: 1,
567
- marginRight: 12
568
- },
569
- toggleHeader: {
570
- flexDirection: "row",
571
- alignItems: "center",
572
- gap: 8,
573
- marginBottom: 3
574
- },
575
- toggleLabel: {
576
- fontSize: 14,
577
- fontWeight: "500",
578
- color: night.color.text
579
- },
580
- dangerousLabel: {
581
- color: night.color.warning
582
- },
583
547
  recommendedBadge: {
584
548
  flexDirection: "row",
585
549
  alignItems: "center",
@@ -618,11 +582,6 @@ const styles = StyleSheet.create({
618
582
  detectedTextInactive: {
619
583
  color: night.color.textTertiary
620
584
  },
621
- toggleDescription: {
622
- fontSize: 12,
623
- color: night.color.textTertiary,
624
- lineHeight: 16
625
- },
626
585
  divider: {
627
586
  height: 1,
628
587
  backgroundColor: night.color.separator,
@@ -9,7 +9,7 @@
9
9
 
10
10
  import React, { useCallback } from "react";
11
11
  import { View, Text, TouchableOpacity, StyleSheet, FlatList } from "react-native";
12
- import { night, Clock, Trash2, useRelativeTime } from "@buoy-gg/shared-ui";
12
+ import { night, Trash2, useRelativeTime, ToolEmptyState } from "@buoy-gg/shared-ui";
13
13
  import { UserCard } from "./UserCard";
14
14
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  /** Wrapper to use the useRelativeTime hook per history item */
@@ -54,21 +54,14 @@ export function ImpersonateHistoryList({
54
54
  });
55
55
  }, [currentUserId, onSelectUser, onStopImpersonation, onRemoveFromHistory]);
56
56
  if (history.length === 0) {
57
- return /*#__PURE__*/_jsxs(View, {
58
- style: styles.emptyContainer,
59
- children: [/*#__PURE__*/_jsx(View, {
60
- style: styles.emptyIcon,
61
- children: /*#__PURE__*/_jsx(Clock, {
62
- size: 28,
63
- color: night.color.textTertiary
64
- })
65
- }), /*#__PURE__*/_jsx(Text, {
66
- style: styles.emptyTitle,
67
- children: "No History"
68
- }), /*#__PURE__*/_jsx(Text, {
69
- style: styles.emptyDescription,
70
- children: "Users you impersonate will appear here for quick access"
71
- })]
57
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
58
+ testID: "buoy-impersonate-history-empty"
59
+ // disguise = "impersonating another user" — the tool's own state,
60
+ // with nothing worn yet.
61
+ ,
62
+ otto: "disguise",
63
+ title: "No History",
64
+ message: "Users you impersonate will appear here for quick access"
72
65
  });
73
66
  }
74
67
  return /*#__PURE__*/_jsxs(View, {
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
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, buoyColors, night, Search, X, TickProvider, windowDragProps } from "@buoy-gg/shared-ui";
11
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
12
+ import { JsModal as JsModalBase, TabSelector, ModalHeader, buoyColors, night, Search, TickProvider, windowDragProps } from "@buoy-gg/shared-ui";
13
13
  import { useImpersonate } from "../hooks/useImpersonate";
14
14
  import { useAutoClearReactQuery } from "../hooks/useAutoClearReactQuery";
15
15
  import { useAutoClearRedux } from "../hooks/useAutoClearRedux";
@@ -59,6 +59,12 @@ export function ImpersonateModal({
59
59
  const tabs = showSettingsTab ? ALL_TABS : TABS_WITHOUT_SETTINGS;
60
60
  const [activeTab, setActiveTab] = useState("search");
61
61
  const [isSearchActive, setIsSearchActive] = useState(false);
62
+ // True while the header's clear ✕ is being pressed. With a mouse, pressing
63
+ // the ✕ blurs the input FIRST, and the blur handler closes search — which
64
+ // unmounts the ✕ before its own press can fire, so the query was never
65
+ // cleared (desktop). Press-in lands before the blur, so the blur handler can
66
+ // tell this dismissal apart from a click elsewhere.
67
+ const clearPressRef = useRef(false);
62
68
  const [searchText, setSearchText] = useState("");
63
69
  const searchInputRef = useRef(null);
64
70
 
@@ -166,42 +172,23 @@ export function ImpersonateModal({
166
172
  onBack: onBack
167
173
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
168
174
  title: "",
169
- children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
170
- style: styles.headerSearchContainer,
171
- children: [/*#__PURE__*/_jsx(Search, {
172
- size: 14,
173
- color: night.color.textSecondary
174
- }), /*#__PURE__*/_jsx(TextInput, {
175
- ref: searchInputRef,
176
- testID: "buoy-impersonate-search-input",
177
- style: styles.headerSearchInput,
178
- placeholder: "Search by email, name, or ID...",
179
- placeholderTextColor: night.color.placeholder,
180
- value: searchText,
181
- onChangeText: handleSearchTextChange,
182
- onSubmitEditing: handleSubmitSearch,
183
- onBlur: () => setIsSearchActive(false),
184
- autoCapitalize: "none",
185
- autoCorrect: false,
186
- returnKeyType: "search"
187
- }), searchText.length > 0 ? /*#__PURE__*/_jsx(TouchableOpacity, {
188
- onPress: () => {
189
- setSearchText("");
190
- setIsSearchActive(false);
191
- },
192
- style: styles.headerSearchClear,
193
- children: /*#__PURE__*/_jsx(X, {
194
- size: 14,
195
- color: night.color.textSecondary
196
- })
197
- }) : /*#__PURE__*/_jsx(TouchableOpacity, {
198
- onPress: () => setIsSearchActive(false),
199
- style: styles.headerSearchClear,
200
- children: /*#__PURE__*/_jsx(X, {
201
- size: 14,
202
- color: night.color.textTertiary
203
- })
204
- })]
175
+ children: isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
176
+ value: searchText,
177
+ onChangeText: handleSearchTextChange
178
+ // ✕ clears and closes; with no text it simply closes.
179
+ ,
180
+ onCancel: () => {
181
+ setSearchText("");
182
+ setIsSearchActive(false);
183
+ },
184
+ onDismiss: () => setIsSearchActive(false),
185
+ dismissOnBlur: true,
186
+ placeholder: "Search by email, name, or ID...",
187
+ iconSize: 14,
188
+ testID: "buoy-impersonate-search-input",
189
+ inputProps: {
190
+ onSubmitEditing: handleSubmitSearch
191
+ }
205
192
  }) : /*#__PURE__*/_jsx(TabSelector, {
206
193
  tabs: tabs,
207
194
  activeTab: activeTab,
@@ -316,30 +303,6 @@ const styles = StyleSheet.create({
316
303
  container: {
317
304
  flex: 1
318
305
  },
319
- headerSearchContainer: {
320
- flexDirection: "row",
321
- alignItems: "center",
322
- backgroundColor: night.color.surfaceElevated,
323
- borderRadius: 6,
324
- borderWidth: 1,
325
- borderColor: night.color.border,
326
- paddingHorizontal: 12,
327
- paddingVertical: 8,
328
- marginTop: 8,
329
- flex: 1,
330
- marginBottom: 2
331
- },
332
- headerSearchInput: {
333
- flex: 1,
334
- color: night.color.text,
335
- fontSize: 13,
336
- marginLeft: 6,
337
- paddingVertical: 2
338
- },
339
- headerSearchClear: {
340
- marginLeft: 6,
341
- padding: 4
342
- },
343
306
  headerActionButton: {
344
307
  width: 28,
345
308
  height: 28,
@@ -63,14 +63,47 @@ function toWireState(state) {
63
63
  }))
64
64
  };
65
65
  }
66
+
67
+ /**
68
+ * Users this device has actually seen — search results from the app's own
69
+ * directory. `startImpersonation` refuses anything else: a live eval showed
70
+ * the model INVENTING a plausible user object when search returned nothing,
71
+ * and impersonating a fabricated user is a silent half-state (headers patched
72
+ * for a person who does not exist). History and the current user also count,
73
+ * so the desktop's re-impersonate-from-history flow keeps working.
74
+ */
75
+ const seenUserIds = new Set();
76
+ function rememberUsers(users) {
77
+ for (const u of users) {
78
+ if (u && typeof u === "object" && u.id) seenUserIds.add(String(u.id));
79
+ }
80
+ }
81
+ function isKnownUser(user) {
82
+ if (!user || typeof user !== "object" || !user.id) return false;
83
+ if (seenUserIds.has(String(user.id))) return true;
84
+ const state = impersonateStore.getState();
85
+ if (state.currentUser?.id === user.id) return true;
86
+ return state.history.some(entry => entry.user.id === user.id);
87
+ }
66
88
  function buildActions(onSearchUsers) {
67
89
  return {
68
90
  searchUsers: async params => {
69
91
  const query = String(params?.query ?? "");
70
92
  const users = await onSearchUsers(query);
93
+ if (Array.isArray(users)) rememberUsers(users);
71
94
  return Array.isArray(users) ? users.map(toWireUser) : users;
72
95
  },
73
- startImpersonation: params => impersonateStore.startImpersonation(params.user),
96
+ startImpersonation: async params => {
97
+ const user = params?.user;
98
+ if (!isKnownUser(user)) {
99
+ throw new Error("That user isn't in this app's directory. Call searchUsers and impersonate one of its results — a user object that didn't come from search impersonates nobody.");
100
+ }
101
+ await impersonateStore.startImpersonation(user);
102
+ return {
103
+ ok: true,
104
+ userId: user.id
105
+ };
106
+ },
74
107
  stopImpersonation: () => impersonateStore.stopImpersonation(),
75
108
  pauseImpersonation: () => impersonateStore.pauseImpersonation(),
76
109
  resumeImpersonation: () => impersonateStore.resumeImpersonation(),
@@ -1 +1 @@
1
- {"version":3,"file":"DataNukeSettings.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/DataNukeSettings.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AA0B5D,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAWzE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,8CAA8C;IAC9C,KAAK,EAAE,OAAO,CAAC;IACf,qDAAqD;IACrD,YAAY,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,0CAA0C;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kCAAkC;IAClC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IACzF,2EAA2E;IAC3E,kBAAkB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAgFD,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,QAAQ,EACR,UAAU,EACV,MAAM,EACN,kBAAkB,EAClB,eAAe,GAChB,EAAE,qBAAqB,qBAkVvB"}
1
+ {"version":3,"file":"DataNukeSettings.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/DataNukeSettings.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AA2B5D,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAWzE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,8CAA8C;IAC9C,KAAK,EAAE,OAAO,CAAC;IACf,qDAAqD;IACrD,YAAY,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,0CAA0C;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kCAAkC;IAClC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IACzF,2EAA2E;IAC3E,kBAAkB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAgED,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,QAAQ,EACR,UAAU,EACV,MAAM,EACN,kBAAkB,EAClB,eAAe,GAChB,EAAE,qBAAqB,qBAkVvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ImpersonateHistoryList.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateHistoryList.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAsB,MAAM,OAAO,CAAC;AAe3C,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAgCnD,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iEAAiE;IACjE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uDAAuD;IACvD,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,wBAAgB,sBAAsB,CAAC,EACrC,OAAO,EACP,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,GACf,EAAE,2BAA2B,qBAgE7B"}
1
+ {"version":3,"file":"ImpersonateHistoryList.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateHistoryList.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAsB,MAAM,OAAO,CAAC;AAgB3C,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAgCnD,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,iEAAiE;IACjE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uDAAuD;IACvD,YAAY,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,wBAAgB,sBAAsB,CAAC,EACrC,OAAO,EACP,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,GACf,EAAE,2BAA2B,qBA+D7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ImpersonateModal.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateModal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAmBxE,OAAO,KAAK,EAAE,qBAAqB,EAA0B,MAAM,UAAU,CAAC;AAgC9E,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAsB,EACtB,MAAc,GACf,EAAE,qBAAqB,4BA2PvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC,qBAGrE"}
1
+ {"version":3,"file":"ImpersonateModal.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateModal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAmBxE,OAAO,KAAK,EAAE,qBAAqB,EAA0B,MAAM,UAAU,CAAC;AAgC9E,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAsB,EACtB,MAAc,GACf,EAAE,qBAAqB,4BA6OvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC,qBAGrE"}
@@ -34,7 +34,10 @@ export declare function createImpersonateSyncAdapter({ onSearchUsers, }: CreateI
34
34
  subscribe: (onChange: () => void) => () => void;
35
35
  actions: {
36
36
  searchUsers: (params: unknown) => Promise<User[]>;
37
- startImpersonation: (params?: unknown) => Promise<void>;
37
+ startImpersonation: (params?: unknown) => Promise<{
38
+ ok: boolean;
39
+ userId: string;
40
+ }>;
38
41
  stopImpersonation: () => Promise<void>;
39
42
  pauseImpersonation: () => Promise<void>;
40
43
  resumeImpersonation: () => Promise<void>;
@@ -55,7 +58,10 @@ export declare const impersonateSyncAdapter: {
55
58
  subscribe: (onChange: () => void) => () => void;
56
59
  actions: {
57
60
  searchUsers: (params: unknown) => Promise<User[]>;
58
- startImpersonation: (params?: unknown) => Promise<void>;
61
+ startImpersonation: (params?: unknown) => Promise<{
62
+ ok: boolean;
63
+ userId: string;
64
+ }>;
59
65
  stopImpersonation: () => Promise<void>;
60
66
  pauseImpersonation: () => Promise<void>;
61
67
  resumeImpersonation: () => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"impersonateSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/impersonateSyncAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAO7D,8EAA8E;AAC9E,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,IAAI,CAEN;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAW,CAAC;AAClD,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAQxD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUrE;AA2DD,UAAU,mCAAmC;IAC3C;;;OAGG;IACH,aAAa,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,aAAa,GACd,EAAE,mCAAmC;;uBAGjB,kBAAkB;0BACb,MAAM,IAAI;;8BAlDJ,OAAO;sCAOL,OAAO;;;;kCAKX,OAAO;qCAQJ,OAAO;;;EAiCvC;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;uBAEhB,kBAAkB;0BACb,MAAM,IAAI;;8BAhEF,OAAO;sCAOL,OAAO;;;;kCAKX,OAAO;qCAQJ,OAAO;;;CAqDvC,CAAC"}
1
+ {"version":3,"file":"impersonateSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/impersonateSyncAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAO7D,8EAA8E;AAC9E,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,IAAI,CAEN;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAW,CAAC;AAClD,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAQxD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUrE;AA4FD,UAAU,mCAAmC;IAC3C;;;OAGG;IACH,aAAa,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,aAAa,GACd,EAAE,mCAAmC;;uBAGjB,kBAAkB;0BACb,MAAM,IAAI;;8BA3DJ,OAAO;sCAQC,OAAO;;;;;;;kCAajB,OAAO;qCAQJ,OAAO;;;EAiCvC;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;uBAEhB,kBAAkB;0BACb,MAAM,IAAI;;8BAzEF,OAAO;sCAQC,OAAO;;;;;;;kCAajB,OAAO;qCAQJ,OAAO;;;CAqDvC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/impersonate",
3
- "version": "7.0.32",
3
+ "version": "7.0.34",
4
4
  "description": "Test your React Native app as any user, role, or flag combo — without logging out or rebuilding. Part of Buoy devtools.",
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": "^7.0.32"
29
+ "@buoy-gg/shared-ui": "^7.0.34"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "*",