yaver-feedback-react-native 0.7.16 → 0.8.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.
@@ -40,6 +40,7 @@ const YaverFeedback_1 = require("./YaverFeedback");
40
40
  const capture_1 = require("./capture");
41
41
  const upload_1 = require("./upload");
42
42
  const AuthOverlay_1 = require("./AuthOverlay");
43
+ const QuickActionIcon_1 = require("./QuickActionIcon");
43
44
  const FeedbackModal = () => {
44
45
  const [visible, setVisible] = (0, react_1.useState)(false);
45
46
  const [action, setAction] = (0, react_1.useState)('idle');
@@ -52,6 +53,19 @@ const FeedbackModal = () => {
52
53
  // hidden button instead of a runtime error.
53
54
  const voiceSupported = (0, react_1.useRef)((0, capture_1.isVoiceCaptureSupported)()).current;
54
55
  const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
56
+ // Tracks whether the user has hidden the QuickActionIcon via its
57
+ // long-press menu. Shake is always available, so the feedback modal
58
+ // is our guaranteed UI for bringing the icon back — we surface a
59
+ // small "Show quick icon" row when this is true.
60
+ const [quickIconHidden, setQuickIconHidden] = (0, react_1.useState)(false);
61
+ // Vibing-input mode: same expand-on-tap pattern as email login.
62
+ // Tap "Vibing" once → the button reveals an input + Send; that lets
63
+ // the user say WHAT they want to vibe on instead of firing a canned
64
+ // "pick something for me" prompt (which in 0.7.13 pointed Claude at
65
+ // the wrong project because the matcher grepped the prompt itself).
66
+ const [showVibeInput, setShowVibeInput] = (0, react_1.useState)(false);
67
+ const [vibePrompt, setVibePrompt] = (0, react_1.useState)('');
68
+ const [lastVibeTaskId, setLastVibeTaskId] = (0, react_1.useState)(null);
55
69
  const mountedRef = (0, react_1.useRef)(true);
56
70
  (0, react_1.useEffect)(() => {
57
71
  mountedRef.current = true;
@@ -61,6 +75,15 @@ const FeedbackModal = () => {
61
75
  setError(null);
62
76
  setToast(null);
63
77
  setAction('idle');
78
+ // Re-read the "user hid the quick icon" flag on every open so
79
+ // the re-enable row reflects the latest preference (the user
80
+ // might have hidden or shown it between opens).
81
+ YaverFeedback_1.YaverFeedback.isQuickIconHidden()
82
+ .then((v) => {
83
+ if (mountedRef.current)
84
+ setQuickIconHidden(v);
85
+ })
86
+ .catch(() => { });
64
87
  }
65
88
  });
66
89
  // Agent streams build / compile progress through the BlackBox
@@ -263,7 +286,22 @@ const FeedbackModal = () => {
263
286
  }
264
287
  }, [closeSoon]);
265
288
  // ─── 3. Vibing ─────────────────────────────────────────────────────
266
- const handleVibing = (0, react_1.useCallback)(async () => {
289
+ // First tap expands the input; second submit fires the actual
290
+ // /vibing/execute. Mirrors the Yaver mobile app's Vibing tab —
291
+ // user types what they want, hits Send, sees the task id back. If
292
+ // left blank, we default to "pick the next small improvement"
293
+ // so a one-tap workflow still works for lazy days.
294
+ const handleVibingButton = (0, react_1.useCallback)(() => {
295
+ if (!showVibeInput) {
296
+ setShowVibeInput(true);
297
+ return;
298
+ }
299
+ // collapse if tapped again with empty input
300
+ if (!vibePrompt.trim()) {
301
+ setShowVibeInput(false);
302
+ }
303
+ }, [showVibeInput, vibePrompt]);
304
+ const handleVibingSubmit = (0, react_1.useCallback)(async () => {
267
305
  const client = YaverFeedback_1.YaverFeedback.getP2PClient();
268
306
  if (!client) {
269
307
  setError('Not connected to the agent yet.');
@@ -280,14 +318,16 @@ const FeedbackModal = () => {
280
318
  .map((e) => `- ${e.message}`)
281
319
  .join('\n')
282
320
  : '';
283
- const prompt = 'The user opened the feedback modal on their phone and tapped Vibing. ' +
284
- 'Investigate whatever they are likely to be asking about — pick the ' +
285
- 'next small improvement or fix based on recent activity and the ' +
286
- 'current screen.' +
287
- errNote;
288
- await client.vibing(prompt);
289
- setToast('Vibing task created');
290
- closeSoon(1200);
321
+ const userPrompt = vibePrompt.trim();
322
+ const prompt = userPrompt
323
+ ? userPrompt + errNote
324
+ : 'Pick the next small improvement or fix for this app based on recent activity and the current screen.' +
325
+ errNote;
326
+ const result = await client.vibing(prompt);
327
+ setLastVibeTaskId(result.taskId);
328
+ setToast(`Vibing task ${result.taskId.slice(0, 8)} created`);
329
+ setVibePrompt('');
330
+ setShowVibeInput(false);
291
331
  }
292
332
  catch (err) {
293
333
  setError(err instanceof Error ? err.message : String(err));
@@ -296,7 +336,7 @@ const FeedbackModal = () => {
296
336
  if (mountedRef.current)
297
337
  setAction('idle');
298
338
  }
299
- }, [closeSoon]);
339
+ }, [vibePrompt]);
300
340
  // ─── 4. Toggle screen recording ────────────────────────────────────
301
341
  const handleToggleRecording = (0, react_1.useCallback)(async () => {
302
342
  setError(null);
@@ -471,6 +511,7 @@ const FeedbackModal = () => {
471
511
  const busy = action !== 'idle';
472
512
  return (<>
473
513
  <AuthOverlay_1.AuthOverlay />
514
+ <QuickActionIcon_1.QuickActionIcon />
474
515
  {visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
475
516
  <react_native_1.Pressable style={styles.overlay} onPress={handleClose}>
476
517
  <react_native_1.Pressable style={styles.modal} onPress={(e) => e.stopPropagation()}>
@@ -489,8 +530,30 @@ const FeedbackModal = () => {
489
530
  ? 'Capturing…'
490
531
  : 'Screenshot & Fix'} tint="#22c55e" onPress={handleScreenshotAndFix} disabled={busy} busy={action === 'capturing'}/>
491
532
 
492
- {/* 3. Vibing */}
493
- <ActionRow label={action === 'vibing' ? 'Starting…' : 'Vibing'} tint="#818cf8" onPress={handleVibing} disabled={busy} busy={action === 'vibing'}/>
533
+ {/* 3. Vibing — expands to an input box on first tap
534
+ so the user says WHAT they want to vibe on, just
535
+ like the Yaver mobile app's Vibing tab. Second
536
+ tap (Send) fires /vibing/execute with the typed
537
+ prompt + resolved bundle id so the agent routes
538
+ to the right repo. */}
539
+ {!showVibeInput ? (<ActionRow label={action === 'vibing' ? 'Starting…' : 'Vibing'} tint="#818cf8" onPress={handleVibingButton} disabled={busy} busy={action === 'vibing'}/>) : (<react_native_1.View style={styles.vibeInputRow}>
540
+ <react_native_1.TextInput style={styles.vibeInput} placeholder="What do you want to vibe on?" placeholderTextColor="#666" value={vibePrompt} onChangeText={setVibePrompt} multiline autoFocus editable={action !== 'vibing'} blurOnSubmit={false}/>
541
+ <react_native_1.View style={styles.vibeInputButtons}>
542
+ <react_native_1.Pressable onPress={() => { setShowVibeInput(false); setVibePrompt(''); }} style={({ pressed }) => [styles.vibeCancelBtn, pressed && styles.buttonPressed]} disabled={action === 'vibing'}>
543
+ <react_native_1.Text style={styles.vibeCancelBtnText}>Cancel</react_native_1.Text>
544
+ </react_native_1.Pressable>
545
+ <react_native_1.Pressable onPress={handleVibingSubmit} style={({ pressed }) => [
546
+ styles.vibeSendBtn,
547
+ pressed && styles.buttonPressed,
548
+ action === 'vibing' && { opacity: 0.6 },
549
+ ]} disabled={action === 'vibing'}>
550
+ {action === 'vibing' ? (<react_native_1.ActivityIndicator color="#fff"/>) : (<react_native_1.Text style={styles.vibeSendBtnText}>Send</react_native_1.Text>)}
551
+ </react_native_1.Pressable>
552
+ </react_native_1.View>
553
+ </react_native_1.View>)}
554
+ {lastVibeTaskId && action !== 'vibing' && (<react_native_1.Text style={styles.vibeTaskLine} numberOfLines={1}>
555
+ Last vibing task: {lastVibeTaskId.slice(0, 12)}…
556
+ </react_native_1.Text>)}
494
557
 
495
558
  {/* Voice note — only rendered when expo-av is installed.
496
559
  Tap to start, tap again to stop → transcribes via
@@ -519,6 +582,27 @@ const FeedbackModal = () => {
519
582
  </react_native_1.View>)}
520
583
  {toast && <react_native_1.Text style={styles.toast}>{toast}</react_native_1.Text>}
521
584
  {error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
585
+
586
+ {/* Quick-icon toggle. The user's three ways to control
587
+ the floating icon are: (1) long-press the icon →
588
+ Hide, (2) tap this row to toggle it on/off, (3) shake
589
+ → this modal → tap this row. Shake is the unkillable
590
+ back-door when the icon is hidden and the dev hasn't
591
+ exposed their own settings UI. */}
592
+ <react_native_1.Pressable onPress={async () => {
593
+ const next = !quickIconHidden;
594
+ setQuickIconHidden(next);
595
+ await YaverFeedback_1.YaverFeedback.setQuickIconVisible(!next);
596
+ }} style={({ pressed }) => [
597
+ styles.quickIconToggle,
598
+ pressed && { opacity: 0.7 },
599
+ ]} accessibilityRole="button" accessibilityLabel={quickIconHidden ? 'Show quick icon' : 'Hide quick icon'}>
600
+ <react_native_1.Text style={styles.quickIconToggleText}>
601
+ {quickIconHidden
602
+ ? '◯ Show quick-access icon'
603
+ : '● Hide quick-access icon'}
604
+ </react_native_1.Text>
605
+ </react_native_1.Pressable>
522
606
  </react_native_1.Pressable>
523
607
  </react_native_1.Pressable>
524
608
  </react_native_1.Modal>)}
@@ -537,6 +621,56 @@ const ActionRow = ({ label, tint, onPress, disabled, busy, }) => (<react_native_
537
621
  {busy ? (<react_native_1.ActivityIndicator color={tint} size="small"/>) : (<react_native_1.Text style={[styles.actionText, { color: tint }]}>{label}</react_native_1.Text>)}
538
622
  </react_native_1.Pressable>);
539
623
  const styles = react_native_1.StyleSheet.create({
624
+ vibeInputRow: {
625
+ backgroundColor: 'rgba(129,140,248,0.08)',
626
+ borderColor: 'rgba(129,140,248,0.4)',
627
+ borderWidth: 1,
628
+ borderRadius: 12,
629
+ padding: 12,
630
+ gap: 10,
631
+ },
632
+ vibeInput: {
633
+ color: '#fff',
634
+ fontSize: 15,
635
+ minHeight: 64,
636
+ textAlignVertical: 'top',
637
+ padding: 0,
638
+ },
639
+ vibeInputButtons: {
640
+ flexDirection: 'row',
641
+ justifyContent: 'flex-end',
642
+ gap: 10,
643
+ },
644
+ vibeCancelBtn: {
645
+ paddingHorizontal: 14,
646
+ paddingVertical: 8,
647
+ borderRadius: 8,
648
+ backgroundColor: 'transparent',
649
+ },
650
+ vibeCancelBtnText: {
651
+ color: '#999',
652
+ fontSize: 14,
653
+ fontWeight: '600',
654
+ },
655
+ vibeSendBtn: {
656
+ paddingHorizontal: 16,
657
+ paddingVertical: 8,
658
+ borderRadius: 8,
659
+ backgroundColor: '#818cf8',
660
+ minWidth: 72,
661
+ alignItems: 'center',
662
+ },
663
+ vibeSendBtnText: {
664
+ color: '#fff',
665
+ fontSize: 14,
666
+ fontWeight: '700',
667
+ },
668
+ vibeTaskLine: {
669
+ color: '#818cf8',
670
+ fontSize: 12,
671
+ marginTop: -4,
672
+ fontFamily: react_native_1.Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' }),
673
+ },
540
674
  overlay: {
541
675
  flex: 1,
542
676
  backgroundColor: 'rgba(0,0,0,0.55)',
@@ -613,4 +747,15 @@ const styles = react_native_1.StyleSheet.create({
613
747
  textAlign: 'center',
614
748
  marginTop: 4,
615
749
  },
750
+ quickIconToggle: {
751
+ marginTop: 4,
752
+ alignSelf: 'center',
753
+ paddingVertical: 6,
754
+ paddingHorizontal: 12,
755
+ },
756
+ quickIconToggleText: {
757
+ color: '#9ca3af',
758
+ fontSize: 12,
759
+ fontWeight: '500',
760
+ },
616
761
  });
@@ -83,7 +83,11 @@ export declare class P2PClient {
83
83
  * vibing from Claude Code / the Yaver mobile app; this method is a
84
84
  * convenience for the SDK's one-tap bug-report-to-vibing path.
85
85
  */
86
- vibing(prompt: string, projectPath?: string): Promise<{
86
+ vibing(prompt: string, opts?: {
87
+ projectName?: string;
88
+ bundleId?: string;
89
+ projectPath?: string;
90
+ }): Promise<{
87
91
  taskId: string;
88
92
  }>;
89
93
  /**
package/dist/P2PClient.js CHANGED
@@ -327,14 +327,27 @@ class P2PClient {
327
327
  * vibing from Claude Code / the Yaver mobile app; this method is a
328
328
  * convenience for the SDK's one-tap bug-report-to-vibing path.
329
329
  */
330
- async vibing(prompt, projectPath) {
330
+ async vibing(prompt, opts) {
331
+ // Resolve app identity exactly the same way we do for
332
+ // reloadApp — bundle ID from expo-constants or native config.
333
+ // Without this, the agent falls back to "grep the prompt for a
334
+ // word that looks like a project name," which is catastrophically
335
+ // wrong: the prompt 'tapped Vibing' matched 'in' → picked mprint
336
+ // → Claude vibed on the wrong repo. Passing the bundle/name lets
337
+ // the agent go straight to findMobileProjectByName / bundleId.
338
+ const identity = resolveAppIdentity(opts);
331
339
  const response = await fetch(`${this.baseUrl}/vibing/execute`, {
332
340
  method: 'POST',
333
341
  headers: {
334
342
  Authorization: `Bearer ${this.authToken}`,
335
343
  'Content-Type': 'application/json',
336
344
  },
337
- body: JSON.stringify({ prompt, projectPath: projectPath ?? '' }),
345
+ body: JSON.stringify({
346
+ prompt,
347
+ projectPath: identity.projectPath ?? opts?.projectPath ?? '',
348
+ projectName: identity.projectName,
349
+ bundleId: identity.bundleId,
350
+ }),
338
351
  });
339
352
  if (!response.ok) {
340
353
  const text = await response.text().catch(() => '');
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ export interface QuickActionIconProps {
3
+ /** Override the color from FeedbackConfig.quickIconColor. */
4
+ color?: string;
5
+ /** Override the initial position from FeedbackConfig.quickIconInitialPosition. */
6
+ initialPosition?: {
7
+ x: number;
8
+ y: number;
9
+ };
10
+ /** Override the icon diameter. Default 44. */
11
+ size?: number;
12
+ }
13
+ /**
14
+ * Small tap-to-open icon for the Yaver Feedback SDK.
15
+ *
16
+ * Default UX:
17
+ * - **Tap** opens the feedback modal (same as shake).
18
+ * - **Long-press** (~550ms) opens a menu with "Open feedback" and
19
+ * "Hide icon". Hiding is persisted to AsyncStorage so the user's
20
+ * decision survives app relaunches.
21
+ * - **Drag** repositions the icon.
22
+ *
23
+ * Shake always keeps working independently — even when the icon is
24
+ * hidden the user can still shake to open feedback.
25
+ *
26
+ * Visibility is controlled by `FeedbackConfig.quickIcon`:
27
+ * - `'auto'` (default) → `'always'` on iOS/Android, `'off'` on web.
28
+ * - `'always'` → visible from first render.
29
+ * - `'after-shake'` → hidden until `yaverFeedback:firstShake` fires.
30
+ * - `'off'` → never rendered.
31
+ *
32
+ * Suppressed entirely when the SDK is loaded inside Yaver's super-host
33
+ * (the Yaver mobile app owns the shake gesture + overlay in that case).
34
+ */
35
+ export declare const QuickActionIcon: React.FC<QuickActionIconProps>;
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.QuickActionIcon = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const YaverFeedback_1 = require("./YaverFeedback");
40
+ const preferences_1 = require("./preferences");
41
+ // Mirror the suppression rule used by YaverFeedback + ShakeDetector:
42
+ // when loaded through Yaver's super-host Hermes bundle, the host owns
43
+ // shake + reload UX. We must not render a second action surface.
44
+ function isRunningInsideYaverHost() {
45
+ try {
46
+ return !!react_native_1.NativeModules?.YaverInfo;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ const DEFAULT_SIZE = 44;
53
+ const DEFAULT_COLOR = '#6366f1';
54
+ const LONG_PRESS_MS = 550;
55
+ /**
56
+ * Small tap-to-open icon for the Yaver Feedback SDK.
57
+ *
58
+ * Default UX:
59
+ * - **Tap** opens the feedback modal (same as shake).
60
+ * - **Long-press** (~550ms) opens a menu with "Open feedback" and
61
+ * "Hide icon". Hiding is persisted to AsyncStorage so the user's
62
+ * decision survives app relaunches.
63
+ * - **Drag** repositions the icon.
64
+ *
65
+ * Shake always keeps working independently — even when the icon is
66
+ * hidden the user can still shake to open feedback.
67
+ *
68
+ * Visibility is controlled by `FeedbackConfig.quickIcon`:
69
+ * - `'auto'` (default) → `'always'` on iOS/Android, `'off'` on web.
70
+ * - `'always'` → visible from first render.
71
+ * - `'after-shake'` → hidden until `yaverFeedback:firstShake` fires.
72
+ * - `'off'` → never rendered.
73
+ *
74
+ * Suppressed entirely when the SDK is loaded inside Yaver's super-host
75
+ * (the Yaver mobile app owns the shake gesture + overlay in that case).
76
+ */
77
+ const QuickActionIcon = ({ color: colorProp, initialPosition: initialPositionProp, size = DEFAULT_SIZE, }) => {
78
+ const config = YaverFeedback_1.YaverFeedback.getConfig();
79
+ const mode = (() => {
80
+ const raw = config?.quickIcon ?? 'auto';
81
+ if (raw === 'auto') {
82
+ return react_native_1.Platform.OS === 'web' ? 'off' : 'always';
83
+ }
84
+ return raw;
85
+ })();
86
+ const color = colorProp ?? config?.quickIconColor ?? DEFAULT_COLOR;
87
+ const { width, height } = react_native_1.Dimensions.get('window');
88
+ const defaultStart = initialPositionProp ??
89
+ config?.quickIconInitialPosition ?? {
90
+ x: Math.max(width - size - 14, 0),
91
+ y: Math.max(Math.floor(height * 0.35), 80),
92
+ };
93
+ const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY(defaultStart)).current;
94
+ const lastPos = (0, react_1.useRef)(defaultStart);
95
+ const dragStart = (0, react_1.useRef)(null);
96
+ const didDrag = (0, react_1.useRef)(false);
97
+ const [userDisabled, setUserDisabled] = (0, react_1.useState)(null);
98
+ const [shakenThisSession, setShakenThisSession] = (0, react_1.useState)(false);
99
+ const [menuOpen, setMenuOpen] = (0, react_1.useState)(false);
100
+ const [hostSuppressed] = (0, react_1.useState)(() => isRunningInsideYaverHost());
101
+ // Load the persisted disable flag once on mount. Until it resolves we
102
+ // render nothing — a one-frame flash of the icon before hiding would
103
+ // be worse than a tiny delayed appearance.
104
+ (0, react_1.useEffect)(() => {
105
+ let alive = true;
106
+ (0, preferences_1.getQuickIconDisabled)().then((v) => {
107
+ if (alive)
108
+ setUserDisabled(v);
109
+ });
110
+ return () => {
111
+ alive = false;
112
+ };
113
+ }, []);
114
+ // `after-shake` mode waits for the first shake before revealing
115
+ // itself. YaverFeedback emits this event from its shake callback.
116
+ (0, react_1.useEffect)(() => {
117
+ const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:firstShake', () => setShakenThisSession(true));
118
+ return () => sub.remove();
119
+ }, []);
120
+ // Programmatic control: host apps can call
121
+ // `YaverFeedback.setQuickIconVisible(true)` to re-surface the icon
122
+ // after the user hid it (e.g. from a settings screen).
123
+ (0, react_1.useEffect)(() => {
124
+ const showSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:quickIconShow', () => {
125
+ setUserDisabled(false);
126
+ void (0, preferences_1.setQuickIconDisabled)(false);
127
+ });
128
+ const hideSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:quickIconHide', () => {
129
+ setUserDisabled(true);
130
+ void (0, preferences_1.setQuickIconDisabled)(true);
131
+ setMenuOpen(false);
132
+ });
133
+ return () => {
134
+ showSub.remove();
135
+ hideSub.remove();
136
+ };
137
+ }, []);
138
+ const panResponder = (0, react_1.useRef)(react_native_1.PanResponder.create({
139
+ onStartShouldSetPanResponder: () => true,
140
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 3 || Math.abs(g.dy) > 3,
141
+ onPanResponderGrant: () => {
142
+ didDrag.current = false;
143
+ dragStart.current = { ...lastPos.current };
144
+ pan.setOffset({ x: lastPos.current.x, y: lastPos.current.y });
145
+ pan.setValue({ x: 0, y: 0 });
146
+ },
147
+ onPanResponderMove: (_, g) => {
148
+ if (Math.abs(g.dx) > 3 || Math.abs(g.dy) > 3) {
149
+ didDrag.current = true;
150
+ }
151
+ react_native_1.Animated.event([null, { dx: pan.x, dy: pan.y }], {
152
+ useNativeDriver: false,
153
+ })(_, g);
154
+ },
155
+ onPanResponderRelease: (_, g) => {
156
+ pan.flattenOffset();
157
+ const start = dragStart.current ?? lastPos.current;
158
+ const maxX = Math.max(width - size, 0);
159
+ const maxY = Math.max(height - size, 0);
160
+ const nextX = Math.max(0, Math.min(maxX, start.x + g.dx));
161
+ const nextY = Math.max(0, Math.min(maxY, start.y + g.dy));
162
+ lastPos.current = { x: nextX, y: nextY };
163
+ react_native_1.Animated.spring(pan, {
164
+ toValue: { x: nextX, y: nextY },
165
+ useNativeDriver: false,
166
+ friction: 7,
167
+ }).start();
168
+ },
169
+ })).current;
170
+ const openFeedback = (0, react_1.useCallback)(() => {
171
+ setMenuOpen(false);
172
+ void YaverFeedback_1.YaverFeedback.startReport();
173
+ }, []);
174
+ const hideForever = (0, react_1.useCallback)(() => {
175
+ setMenuOpen(false);
176
+ setUserDisabled(true);
177
+ void (0, preferences_1.setQuickIconDisabled)(true);
178
+ }, []);
179
+ if (hostSuppressed)
180
+ return null;
181
+ if (mode === 'off')
182
+ return null;
183
+ if (userDisabled === null)
184
+ return null;
185
+ if (userDisabled)
186
+ return null;
187
+ if (mode === 'after-shake' && !shakenThisSession)
188
+ return null;
189
+ if (!YaverFeedback_1.YaverFeedback.isEnabled())
190
+ return null;
191
+ const visualSize = size;
192
+ const radius = visualSize / 2;
193
+ return (<react_native_1.Animated.View pointerEvents="box-none" style={[
194
+ react_native_1.StyleSheet.absoluteFill,
195
+ { zIndex: 9998 },
196
+ ]}>
197
+ <react_native_1.Animated.View {...panResponder.panHandlers} style={[
198
+ styles.container,
199
+ {
200
+ transform: [{ translateX: pan.x }, { translateY: pan.y }],
201
+ },
202
+ ]}>
203
+ <react_native_1.Pressable onPress={() => {
204
+ if (didDrag.current) {
205
+ didDrag.current = false;
206
+ return;
207
+ }
208
+ openFeedback();
209
+ }} onLongPress={() => {
210
+ if (didDrag.current)
211
+ return;
212
+ setMenuOpen((m) => !m);
213
+ }} delayLongPress={LONG_PRESS_MS} hitSlop={6} accessibilityRole="button" accessibilityLabel="Open Yaver feedback" style={({ pressed }) => [
214
+ styles.icon,
215
+ {
216
+ width: visualSize,
217
+ height: visualSize,
218
+ borderRadius: radius,
219
+ backgroundColor: color,
220
+ opacity: pressed ? 0.85 : 1,
221
+ },
222
+ ]}>
223
+ <react_native_1.Text style={[styles.iconLabel, { fontSize: Math.round(visualSize * 0.5) }]}>y</react_native_1.Text>
224
+ </react_native_1.Pressable>
225
+ {menuOpen ? (<react_native_1.View style={styles.menu}>
226
+ <react_native_1.Pressable onPress={openFeedback} style={({ pressed }) => [
227
+ styles.menuItem,
228
+ pressed && styles.menuItemPressed,
229
+ ]}>
230
+ <react_native_1.Text style={styles.menuItemText}>Open feedback</react_native_1.Text>
231
+ </react_native_1.Pressable>
232
+ <react_native_1.View style={styles.menuDivider}/>
233
+ <react_native_1.Pressable onPress={hideForever} style={({ pressed }) => [
234
+ styles.menuItem,
235
+ pressed && styles.menuItemPressed,
236
+ ]}>
237
+ <react_native_1.Text style={[styles.menuItemText, styles.menuItemDanger]}>
238
+ Hide icon
239
+ </react_native_1.Text>
240
+ </react_native_1.Pressable>
241
+ </react_native_1.View>) : null}
242
+ </react_native_1.Animated.View>
243
+ </react_native_1.Animated.View>);
244
+ };
245
+ exports.QuickActionIcon = QuickActionIcon;
246
+ const styles = react_native_1.StyleSheet.create({
247
+ container: {
248
+ position: 'absolute',
249
+ top: 0,
250
+ left: 0,
251
+ alignItems: 'flex-start',
252
+ },
253
+ icon: {
254
+ alignItems: 'center',
255
+ justifyContent: 'center',
256
+ shadowColor: '#000',
257
+ shadowOffset: { width: 0, height: 2 },
258
+ shadowOpacity: 0.25,
259
+ shadowRadius: 4,
260
+ elevation: 4,
261
+ },
262
+ iconLabel: {
263
+ color: '#ffffff',
264
+ fontWeight: '700',
265
+ includeFontPadding: false,
266
+ },
267
+ menu: {
268
+ marginTop: 6,
269
+ minWidth: 150,
270
+ backgroundColor: '#1f1f23',
271
+ borderRadius: 10,
272
+ paddingVertical: 4,
273
+ shadowColor: '#000',
274
+ shadowOffset: { width: 0, height: 2 },
275
+ shadowOpacity: 0.3,
276
+ shadowRadius: 6,
277
+ elevation: 6,
278
+ },
279
+ menuItem: {
280
+ paddingHorizontal: 14,
281
+ paddingVertical: 10,
282
+ },
283
+ menuItemPressed: {
284
+ backgroundColor: '#2a2a30',
285
+ },
286
+ menuItemText: {
287
+ color: '#f4f4f5',
288
+ fontSize: 14,
289
+ fontWeight: '500',
290
+ },
291
+ menuItemDanger: {
292
+ color: '#f97316',
293
+ },
294
+ menuDivider: {
295
+ height: react_native_1.StyleSheet.hairlineWidth,
296
+ backgroundColor: '#3f3f46',
297
+ marginHorizontal: 8,
298
+ },
299
+ });
@@ -199,6 +199,32 @@ export declare class YaverFeedback {
199
199
  * 3. DevSettings.reload() (Metro dev server fallback)
200
200
  */
201
201
  private static loadBundleAndReload;
202
+ /**
203
+ * Internal: fired from every shake path (dev-menu + accelerometer)
204
+ * before the feedback modal opens. Emits `yaverFeedback:firstShake`
205
+ * exactly once per process so QuickActionIcon's `'after-shake'` mode
206
+ * can surface itself on first shake and stay visible for the rest of
207
+ * the session.
208
+ */
209
+ static notifyShake(): void;
210
+ /**
211
+ * Show / hide the QuickActionIcon programmatically and persist the
212
+ * choice across launches. Host apps can call this from a settings
213
+ * screen so the user has a second way to re-enable the icon after
214
+ * hiding it via the icon's own long-press menu — shake is always the
215
+ * third back-door because it never depends on a visible control.
216
+ */
217
+ static setQuickIconVisible(visible: boolean): Promise<void>;
218
+ /**
219
+ * Returns `true` when the user has chosen to hide the QuickActionIcon
220
+ * (via its long-press menu or `setQuickIconVisible(false)`).
221
+ * FeedbackModal uses this to surface a one-tap "Show quick icon"
222
+ * control so the user can bring the icon back without having to know
223
+ * about the programmatic API.
224
+ */
225
+ static isQuickIconHidden(): Promise<boolean>;
226
+ /** Clear the persisted "user hid the icon" flag. */
227
+ static resetQuickIconPreference(): Promise<void>;
202
228
  /** Tear down the SDK (stop shake detector, clear state). */
203
229
  static destroy(): void;
204
230
  }