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.
- package/dist/FeedbackModal.js +157 -12
- package/dist/P2PClient.d.ts +5 -1
- package/dist/P2PClient.js +15 -2
- package/dist/QuickActionIcon.d.ts +35 -0
- package/dist/QuickActionIcon.js +299 -0
- package/dist/YaverFeedback.d.ts +26 -0
- package/dist/YaverFeedback.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -1
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +57 -0
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/src/FeedbackModal.tsx +199 -19
- package/src/P2PClient.ts +18 -2
- package/src/QuickActionIcon.tsx +332 -0
- package/src/YaverFeedback.ts +67 -0
- package/src/index.ts +7 -0
- package/src/preferences.ts +55 -0
- package/src/types.ts +28 -0
package/dist/FeedbackModal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
'current screen.' +
|
|
287
|
-
|
|
288
|
-
await client.vibing(prompt);
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
});
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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({
|
|
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
|
+
});
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -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
|
}
|