yaver-feedback-react-native 0.8.0 → 0.8.2
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/README.md +40 -6
- package/dist/AuthOverlay.js +1 -1
- package/dist/FeedbackModal.js +475 -265
- package/dist/P2PClient.d.ts +9 -3
- package/dist/P2PClient.js +30 -2
- package/dist/QuickActionIcon.d.ts +10 -2
- package/dist/QuickActionIcon.js +49 -12
- package/dist/YaverFeedback.d.ts +5 -0
- package/dist/YaverFeedback.js +24 -0
- package/dist/__tests__/P2PClient.test.js +30 -0
- package/dist/__tests__/YaverFeedback.test.js +39 -0
- package/dist/capture.d.ts +11 -0
- package/dist/capture.js +59 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.js +9 -7
- package/dist/preferences.d.ts +11 -0
- package/dist/preferences.js +82 -0
- package/dist/types.d.ts +29 -4
- package/dist/upload.d.ts +1 -0
- package/dist/upload.js +8 -0
- package/package.json +11 -5
- package/src/AuthOverlay.tsx +1 -0
- package/src/FeedbackModal.tsx +561 -319
- package/src/P2PClient.ts +44 -3
- package/src/QuickActionIcon.tsx +76 -13
- package/src/YaverFeedback.ts +31 -0
- package/src/__tests__/P2PClient.test.ts +40 -0
- package/src/__tests__/YaverFeedback.test.ts +42 -0
- package/src/capture.ts +74 -0
- package/src/index.ts +8 -6
- package/src/preferences.ts +96 -0
- package/src/types.ts +29 -4
- package/src/upload.ts +9 -0
package/src/P2PClient.ts
CHANGED
|
@@ -7,6 +7,15 @@ export interface FeedbackEvent {
|
|
|
7
7
|
data: any;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface ReloadAck {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
mode: 'dev' | 'bundle';
|
|
13
|
+
acknowledged: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
nativeChangesDetected?: boolean;
|
|
16
|
+
changeClass?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
12
21
|
* agent can map the reload request to a MobileProject in its scan
|
|
@@ -179,6 +188,14 @@ export class P2PClient {
|
|
|
179
188
|
} as any);
|
|
180
189
|
}
|
|
181
190
|
|
|
191
|
+
if (bundle.audio) {
|
|
192
|
+
formData.append('audio', {
|
|
193
|
+
uri: Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
|
|
194
|
+
type: 'audio/m4a',
|
|
195
|
+
name: 'voice_note.m4a',
|
|
196
|
+
} as any);
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
const response = await fetch(`${this.baseUrl}/feedback`, {
|
|
183
200
|
method: 'POST',
|
|
184
201
|
headers: {
|
|
@@ -309,7 +326,7 @@ export class P2PClient {
|
|
|
309
326
|
async reloadApp(
|
|
310
327
|
mode: 'dev' | 'bundle' = 'bundle',
|
|
311
328
|
opts?: { projectName?: string; bundleId?: string; projectPath?: string },
|
|
312
|
-
): Promise<
|
|
329
|
+
): Promise<ReloadAck> {
|
|
313
330
|
// Default path: always rebuild a fresh Hermes bundle.
|
|
314
331
|
//
|
|
315
332
|
// Rationale: the SDK's common caller is a phone user who's not
|
|
@@ -330,7 +347,19 @@ export class P2PClient {
|
|
|
330
347
|
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
331
348
|
});
|
|
332
349
|
if (primary.ok) {
|
|
333
|
-
|
|
350
|
+
const payload = await primary.json().catch(() => ({} as Record<string, unknown>));
|
|
351
|
+
const nativeChangesDetected = payload.nativeChangesDetected === true;
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
mode: 'dev',
|
|
355
|
+
acknowledged: true,
|
|
356
|
+
nativeChangesDetected,
|
|
357
|
+
changeClass:
|
|
358
|
+
typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
|
|
359
|
+
message: nativeChangesDetected
|
|
360
|
+
? 'Reload accepted, but native changes need a rebuild.'
|
|
361
|
+
: 'Hot reload request accepted.',
|
|
362
|
+
};
|
|
334
363
|
}
|
|
335
364
|
// Dev mode failed — fall through to bundle rebuild below rather
|
|
336
365
|
// than surfacing the raw error, so the user never has to know
|
|
@@ -362,7 +391,19 @@ export class P2PClient {
|
|
|
362
391
|
const text = await res.text().catch(() => '');
|
|
363
392
|
throw new Error(friendlyReloadError(res.status, text));
|
|
364
393
|
}
|
|
365
|
-
|
|
394
|
+
const payload = await res.json().catch(() => ({} as Record<string, unknown>));
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
mode: 'bundle',
|
|
398
|
+
acknowledged: true,
|
|
399
|
+
changeClass:
|
|
400
|
+
typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
|
|
401
|
+
nativeChangesDetected: payload.nativeChangesDetected === true,
|
|
402
|
+
message:
|
|
403
|
+
typeof payload.message === 'string' && payload.message.trim()
|
|
404
|
+
? payload.message
|
|
405
|
+
: 'Reload request acknowledged. Agent is rebuilding the bundle.',
|
|
406
|
+
};
|
|
366
407
|
}
|
|
367
408
|
|
|
368
409
|
/**
|
package/src/QuickActionIcon.tsx
CHANGED
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
View,
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { YaverFeedback } from './YaverFeedback';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getQuickIconColorPreset,
|
|
17
|
+
getQuickIconDisabled,
|
|
18
|
+
QUICK_ICON_COLOR_PRESETS,
|
|
19
|
+
setQuickIconColorPreset,
|
|
20
|
+
setQuickIconDisabled,
|
|
21
|
+
} from './preferences';
|
|
16
22
|
|
|
17
23
|
// Mirror the suppression rule used by YaverFeedback + ShakeDetector:
|
|
18
24
|
// when loaded through Yaver's super-host Hermes bundle, the host owns
|
|
@@ -26,12 +32,23 @@ function isRunningInsideYaverHost(): boolean {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
const DEFAULT_SIZE = 44;
|
|
29
|
-
const
|
|
35
|
+
const DEFAULT_BACKGROUND_COLOR = '#ff6b2c';
|
|
36
|
+
const DEFAULT_LABEL_COLOR = '#111111';
|
|
37
|
+
const DEFAULT_BORDER_COLOR = 'rgba(255,255,255,0.92)';
|
|
38
|
+
const DEFAULT_SHADOW_COLOR = '#000000';
|
|
30
39
|
const LONG_PRESS_MS = 550;
|
|
31
40
|
|
|
32
41
|
export interface QuickActionIconProps {
|
|
33
|
-
/**
|
|
42
|
+
/** Deprecated alias for `backgroundColor`. */
|
|
34
43
|
color?: string;
|
|
44
|
+
/** Override the background from FeedbackConfig.quickIconBackgroundColor. */
|
|
45
|
+
backgroundColor?: string;
|
|
46
|
+
/** Override the label color from FeedbackConfig.quickIconForegroundColor. */
|
|
47
|
+
foregroundColor?: string;
|
|
48
|
+
/** Override the border color from FeedbackConfig.quickIconBorderColor. */
|
|
49
|
+
borderColor?: string;
|
|
50
|
+
/** Override the shadow color from FeedbackConfig.quickIconShadowColor. */
|
|
51
|
+
shadowColor?: string;
|
|
35
52
|
/** Override the initial position from FeedbackConfig.quickIconInitialPosition. */
|
|
36
53
|
initialPosition?: { x: number; y: number };
|
|
37
54
|
/** Override the icon diameter. Default 44. */
|
|
@@ -52,7 +69,7 @@ export interface QuickActionIconProps {
|
|
|
52
69
|
* hidden the user can still shake to open feedback.
|
|
53
70
|
*
|
|
54
71
|
* Visibility is controlled by `FeedbackConfig.quickIcon`:
|
|
55
|
-
* - `'auto'` (default) → `'
|
|
72
|
+
* - `'auto'` (default) → `'after-shake'` on iOS/Android, `'off'` on web.
|
|
56
73
|
* - `'always'` → visible from first render.
|
|
57
74
|
* - `'after-shake'` → hidden until `yaverFeedback:firstShake` fires.
|
|
58
75
|
* - `'off'` → never rendered.
|
|
@@ -62,6 +79,10 @@ export interface QuickActionIconProps {
|
|
|
62
79
|
*/
|
|
63
80
|
export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
64
81
|
color: colorProp,
|
|
82
|
+
backgroundColor: backgroundColorProp,
|
|
83
|
+
foregroundColor: foregroundColorProp,
|
|
84
|
+
borderColor: borderColorProp,
|
|
85
|
+
shadowColor: shadowColorProp,
|
|
65
86
|
initialPosition: initialPositionProp,
|
|
66
87
|
size = DEFAULT_SIZE,
|
|
67
88
|
}) => {
|
|
@@ -70,12 +91,29 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
70
91
|
const mode: 'always' | 'after-shake' | 'off' = (() => {
|
|
71
92
|
const raw = config?.quickIcon ?? 'auto';
|
|
72
93
|
if (raw === 'auto') {
|
|
73
|
-
return Platform.OS === 'web' ? 'off' : '
|
|
94
|
+
return Platform.OS === 'web' ? 'off' : 'after-shake';
|
|
74
95
|
}
|
|
75
96
|
return raw;
|
|
76
97
|
})();
|
|
77
98
|
|
|
78
|
-
const
|
|
99
|
+
const backgroundColor =
|
|
100
|
+
backgroundColorProp ??
|
|
101
|
+
colorProp ??
|
|
102
|
+
config?.quickIconBackgroundColor ??
|
|
103
|
+
config?.quickIconColor ??
|
|
104
|
+
DEFAULT_BACKGROUND_COLOR;
|
|
105
|
+
const foregroundColor =
|
|
106
|
+
foregroundColorProp ??
|
|
107
|
+
config?.quickIconForegroundColor ??
|
|
108
|
+
DEFAULT_LABEL_COLOR;
|
|
109
|
+
const borderColor =
|
|
110
|
+
borderColorProp ??
|
|
111
|
+
config?.quickIconBorderColor ??
|
|
112
|
+
DEFAULT_BORDER_COLOR;
|
|
113
|
+
const shadowColor =
|
|
114
|
+
shadowColorProp ??
|
|
115
|
+
config?.quickIconShadowColor ??
|
|
116
|
+
DEFAULT_SHADOW_COLOR;
|
|
79
117
|
|
|
80
118
|
const { width, height } = Dimensions.get('window');
|
|
81
119
|
const defaultStart =
|
|
@@ -91,6 +129,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
91
129
|
const didDrag = useRef(false);
|
|
92
130
|
|
|
93
131
|
const [userDisabled, setUserDisabled] = useState<boolean | null>(null);
|
|
132
|
+
const [colorPreset, setColorPreset] = useState<keyof typeof QUICK_ICON_COLOR_PRESETS | null>(null);
|
|
94
133
|
const [shakenThisSession, setShakenThisSession] = useState(false);
|
|
95
134
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
96
135
|
const [hostSuppressed] = useState<boolean>(() => isRunningInsideYaverHost());
|
|
@@ -103,6 +142,9 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
103
142
|
getQuickIconDisabled().then((v) => {
|
|
104
143
|
if (alive) setUserDisabled(v);
|
|
105
144
|
});
|
|
145
|
+
getQuickIconColorPreset().then((v) => {
|
|
146
|
+
if (alive) setColorPreset(v);
|
|
147
|
+
});
|
|
106
148
|
return () => {
|
|
107
149
|
alive = false;
|
|
108
150
|
};
|
|
@@ -137,9 +179,18 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
137
179
|
setMenuOpen(false);
|
|
138
180
|
},
|
|
139
181
|
);
|
|
182
|
+
const colorSub = DeviceEventEmitter.addListener(
|
|
183
|
+
'yaverFeedback:quickIconColorChange',
|
|
184
|
+
(next: { preset?: keyof typeof QUICK_ICON_COLOR_PRESETS | null }) => {
|
|
185
|
+
const preset = next?.preset ?? null;
|
|
186
|
+
setColorPreset(preset);
|
|
187
|
+
void setQuickIconColorPreset(preset);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
140
190
|
return () => {
|
|
141
191
|
showSub.remove();
|
|
142
192
|
hideSub.remove();
|
|
193
|
+
colorSub.remove();
|
|
143
194
|
};
|
|
144
195
|
}, []);
|
|
145
196
|
|
|
@@ -197,6 +248,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
197
248
|
if (mode === 'after-shake' && !shakenThisSession) return null;
|
|
198
249
|
if (!YaverFeedback.isEnabled()) return null;
|
|
199
250
|
|
|
251
|
+
const presetColors = colorPreset ? QUICK_ICON_COLOR_PRESETS[colorPreset] : null;
|
|
200
252
|
const visualSize = size;
|
|
201
253
|
const radius = visualSize / 2;
|
|
202
254
|
|
|
@@ -239,12 +291,24 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
239
291
|
width: visualSize,
|
|
240
292
|
height: visualSize,
|
|
241
293
|
borderRadius: radius,
|
|
242
|
-
backgroundColor:
|
|
294
|
+
backgroundColor: presetColors?.backgroundColor ?? backgroundColor,
|
|
295
|
+
borderColor: presetColors?.borderColor ?? borderColor,
|
|
296
|
+
shadowColor: presetColors?.shadowColor ?? shadowColor,
|
|
243
297
|
opacity: pressed ? 0.85 : 1,
|
|
244
298
|
},
|
|
245
299
|
]}
|
|
246
300
|
>
|
|
247
|
-
<Text
|
|
301
|
+
<Text
|
|
302
|
+
style={[
|
|
303
|
+
styles.iconLabel,
|
|
304
|
+
{
|
|
305
|
+
color: presetColors?.foregroundColor ?? foregroundColor,
|
|
306
|
+
fontSize: Math.round(visualSize * 0.5),
|
|
307
|
+
},
|
|
308
|
+
]}
|
|
309
|
+
>
|
|
310
|
+
y
|
|
311
|
+
</Text>
|
|
248
312
|
</Pressable>
|
|
249
313
|
{menuOpen ? (
|
|
250
314
|
<View style={styles.menu}>
|
|
@@ -286,14 +350,13 @@ const styles = StyleSheet.create({
|
|
|
286
350
|
icon: {
|
|
287
351
|
alignItems: 'center',
|
|
288
352
|
justifyContent: 'center',
|
|
289
|
-
shadowColor: '#000',
|
|
290
353
|
shadowOffset: { width: 0, height: 2 },
|
|
291
|
-
shadowOpacity: 0.
|
|
292
|
-
shadowRadius:
|
|
293
|
-
elevation:
|
|
354
|
+
shadowOpacity: 0.34,
|
|
355
|
+
shadowRadius: 6,
|
|
356
|
+
elevation: 7,
|
|
357
|
+
borderWidth: 2,
|
|
294
358
|
},
|
|
295
359
|
iconLabel: {
|
|
296
|
-
color: '#ffffff',
|
|
297
360
|
fontWeight: '700',
|
|
298
361
|
includeFontPadding: false,
|
|
299
362
|
},
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -9,13 +9,17 @@ import {
|
|
|
9
9
|
setStrictNativeAuth,
|
|
10
10
|
getToken,
|
|
11
11
|
getSelectedDeviceId,
|
|
12
|
+
listReachableDevices,
|
|
12
13
|
clearToken,
|
|
13
14
|
clearSelectedDeviceId,
|
|
14
15
|
DEFAULT_CONVEX_SITE_URL,
|
|
15
16
|
} from './auth';
|
|
16
17
|
import {
|
|
17
18
|
getQuickIconDisabled,
|
|
19
|
+
getQuickIconColorPreset,
|
|
18
20
|
setQuickIconDisabled,
|
|
21
|
+
setQuickIconColorPreset,
|
|
22
|
+
QuickIconColorPreset,
|
|
19
23
|
} from './preferences';
|
|
20
24
|
|
|
21
25
|
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
@@ -92,6 +96,7 @@ export class YaverFeedback {
|
|
|
92
96
|
autoLogin: true,
|
|
93
97
|
...cfg,
|
|
94
98
|
};
|
|
99
|
+
firstShakeFired = false;
|
|
95
100
|
|
|
96
101
|
// Route the in-SDK login screen to prod yaver.io by default; callers may
|
|
97
102
|
// override for staging via authConvexSiteUrl / authWebBaseUrl.
|
|
@@ -361,6 +366,15 @@ export class YaverFeedback {
|
|
|
361
366
|
await YaverFeedback.discoverAgent();
|
|
362
367
|
}
|
|
363
368
|
|
|
369
|
+
/** Resolve the currently selected remote machine from the authenticated device list. */
|
|
370
|
+
static async getSelectedRemoteDevice() {
|
|
371
|
+
if (!config?.authToken || !config.preferredDeviceId) return null;
|
|
372
|
+
const preferredDeviceId = config.preferredDeviceId;
|
|
373
|
+
const devices = await listReachableDevices(config.authToken);
|
|
374
|
+
const all = [...devices.owned, ...devices.shared];
|
|
375
|
+
return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
|
|
376
|
+
}
|
|
377
|
+
|
|
364
378
|
/**
|
|
365
379
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
366
380
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
@@ -858,6 +872,22 @@ export class YaverFeedback {
|
|
|
858
872
|
return getQuickIconDisabled();
|
|
859
873
|
}
|
|
860
874
|
|
|
875
|
+
static async setQuickIconColorPreset(
|
|
876
|
+
preset: QuickIconColorPreset | null,
|
|
877
|
+
): Promise<void> {
|
|
878
|
+
await setQuickIconColorPreset(preset);
|
|
879
|
+
try {
|
|
880
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
881
|
+
DeviceEventEmitter.emit('yaverFeedback:quickIconColorChange', { preset });
|
|
882
|
+
} catch {
|
|
883
|
+
// emitter unavailable — preference is still persisted
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
static async getQuickIconColorPreset(): Promise<QuickIconColorPreset | null> {
|
|
888
|
+
return getQuickIconColorPreset();
|
|
889
|
+
}
|
|
890
|
+
|
|
861
891
|
/** Clear the persisted "user hid the icon" flag. */
|
|
862
892
|
static async resetQuickIconPreference(): Promise<void> {
|
|
863
893
|
await YaverFeedback.setQuickIconVisible(true);
|
|
@@ -869,6 +899,7 @@ export class YaverFeedback {
|
|
|
869
899
|
shakeDetector.stop();
|
|
870
900
|
shakeDetector = null;
|
|
871
901
|
}
|
|
902
|
+
firstShakeFired = false;
|
|
872
903
|
enabled = false;
|
|
873
904
|
config = null;
|
|
874
905
|
p2pClient = null;
|
|
@@ -215,4 +215,44 @@ describe('P2PClient', () => {
|
|
|
215
215
|
expect(result).toEqual(builds);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
describe('reloadApp()', () => {
|
|
220
|
+
it('returns an acknowledgement for dev reloads', async () => {
|
|
221
|
+
mockFetch.mockResolvedValue({
|
|
222
|
+
ok: true,
|
|
223
|
+
json: () => Promise.resolve({ ok: true, changeClass: 'js_only' }),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const client = new P2PClient('http://localhost:18080', 'tok');
|
|
227
|
+
const result = await client.reloadApp('dev');
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual(
|
|
230
|
+
expect.objectContaining({
|
|
231
|
+
ok: true,
|
|
232
|
+
mode: 'dev',
|
|
233
|
+
acknowledged: true,
|
|
234
|
+
message: 'Hot reload request accepted.',
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns an acknowledgement for bundle reloads', async () => {
|
|
240
|
+
mockFetch.mockResolvedValue({
|
|
241
|
+
ok: true,
|
|
242
|
+
json: () => Promise.resolve({ ok: true }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const client = new P2PClient('http://localhost:18080', 'tok');
|
|
246
|
+
const result = await client.reloadApp('bundle');
|
|
247
|
+
|
|
248
|
+
expect(result).toEqual(
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
ok: true,
|
|
251
|
+
mode: 'bundle',
|
|
252
|
+
acknowledged: true,
|
|
253
|
+
message: 'Reload request acknowledged. Agent is rebuilding the bundle.',
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
218
258
|
});
|
|
@@ -17,6 +17,34 @@ jest.mock('../Discovery', () => ({
|
|
|
17
17
|
},
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
jest.mock('../auth', () => ({
|
|
21
|
+
configureAuthEndpoints: jest.fn(),
|
|
22
|
+
setStrictNativeAuth: jest.fn(),
|
|
23
|
+
getToken: jest.fn(async () => null),
|
|
24
|
+
getSelectedDeviceId: jest.fn(async () => null),
|
|
25
|
+
clearToken: jest.fn(async () => {}),
|
|
26
|
+
clearSelectedDeviceId: jest.fn(async () => {}),
|
|
27
|
+
listReachableDevices: jest.fn(async () => ({
|
|
28
|
+
owned: [
|
|
29
|
+
{
|
|
30
|
+
deviceId: 'device-1',
|
|
31
|
+
name: 'Dev Mac',
|
|
32
|
+
platform: 'darwin',
|
|
33
|
+
isOnline: true,
|
|
34
|
+
needsAuth: false,
|
|
35
|
+
runnerDown: false,
|
|
36
|
+
lastHeartbeat: Date.now(),
|
|
37
|
+
isGuest: false,
|
|
38
|
+
accessScope: 'owner',
|
|
39
|
+
quicHost: '127.0.0.1',
|
|
40
|
+
quicPort: 18080,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
shared: [],
|
|
44
|
+
})),
|
|
45
|
+
DEFAULT_CONVEX_SITE_URL: 'https://example.convex.site',
|
|
46
|
+
}));
|
|
47
|
+
|
|
20
48
|
// Reset module-level state between tests by re-requiring
|
|
21
49
|
beforeEach(() => {
|
|
22
50
|
// YaverFeedback uses module-level variables (config, enabled, p2pClient).
|
|
@@ -126,6 +154,20 @@ describe('YaverFeedback', () => {
|
|
|
126
154
|
});
|
|
127
155
|
});
|
|
128
156
|
|
|
157
|
+
describe('getSelectedRemoteDevice()', () => {
|
|
158
|
+
it('returns the selected device from the reachable device list', async () => {
|
|
159
|
+
YaverFeedback.init({
|
|
160
|
+
authToken: 'tok',
|
|
161
|
+
preferredDeviceId: 'device-1',
|
|
162
|
+
enabled: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const device = await YaverFeedback.getSelectedRemoteDevice();
|
|
166
|
+
expect(device?.deviceId).toBe('device-1');
|
|
167
|
+
expect(device?.name).toBe('Dev Mac');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
129
171
|
describe('startReport()', () => {
|
|
130
172
|
it('does nothing when not enabled', async () => {
|
|
131
173
|
YaverFeedback.init({ authToken: 'tok', enabled: false });
|
package/src/capture.ts
CHANGED
|
@@ -38,6 +38,80 @@ export async function captureScreenshot(): Promise<string> {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export interface PickedFeedbackFile {
|
|
42
|
+
path: string;
|
|
43
|
+
name: string;
|
|
44
|
+
mimeType?: string;
|
|
45
|
+
kind: 'image' | 'video' | 'audio' | 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function classifyPickedFile(name: string, mimeType?: string): PickedFeedbackFile['kind'] {
|
|
49
|
+
const lowerName = name.toLowerCase();
|
|
50
|
+
const lowerMime = (mimeType ?? '').toLowerCase();
|
|
51
|
+
if (
|
|
52
|
+
lowerMime.startsWith('image/') ||
|
|
53
|
+
lowerName.endsWith('.png') ||
|
|
54
|
+
lowerName.endsWith('.jpg') ||
|
|
55
|
+
lowerName.endsWith('.jpeg') ||
|
|
56
|
+
lowerName.endsWith('.webp')
|
|
57
|
+
) {
|
|
58
|
+
return 'image';
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
lowerMime.startsWith('video/') ||
|
|
62
|
+
lowerName.endsWith('.mp4') ||
|
|
63
|
+
lowerName.endsWith('.mov') ||
|
|
64
|
+
lowerName.endsWith('.m4v')
|
|
65
|
+
) {
|
|
66
|
+
return 'video';
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
lowerMime.startsWith('audio/') ||
|
|
70
|
+
lowerName.endsWith('.m4a') ||
|
|
71
|
+
lowerName.endsWith('.aac') ||
|
|
72
|
+
lowerName.endsWith('.wav') ||
|
|
73
|
+
lowerName.endsWith('.mp3')
|
|
74
|
+
) {
|
|
75
|
+
return 'audio';
|
|
76
|
+
}
|
|
77
|
+
return 'unknown';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pick an existing media file from the device. Requires
|
|
82
|
+
* `expo-document-picker` to be installed.
|
|
83
|
+
*/
|
|
84
|
+
export async function pickFeedbackFile(): Promise<PickedFeedbackFile> {
|
|
85
|
+
try {
|
|
86
|
+
const picker = require('expo-document-picker');
|
|
87
|
+
const result = await picker.getDocumentAsync({
|
|
88
|
+
copyToCacheDirectory: true,
|
|
89
|
+
multiple: false,
|
|
90
|
+
type: ['image/*', 'video/*', 'audio/*'],
|
|
91
|
+
});
|
|
92
|
+
if (result?.canceled) {
|
|
93
|
+
throw new Error('File selection canceled.');
|
|
94
|
+
}
|
|
95
|
+
const asset = result?.assets?.[0];
|
|
96
|
+
if (!asset?.uri) {
|
|
97
|
+
throw new Error('No file selected.');
|
|
98
|
+
}
|
|
99
|
+
const name = asset.name || asset.uri.split('/').pop() || 'attachment';
|
|
100
|
+
const mimeType = asset.mimeType as string | undefined;
|
|
101
|
+
return {
|
|
102
|
+
path: asset.uri,
|
|
103
|
+
name,
|
|
104
|
+
mimeType,
|
|
105
|
+
kind: classifyPickedFile(name, mimeType),
|
|
106
|
+
};
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'[YaverFeedback] File upload needs `expo-document-picker` as an optional peer dependency. ' +
|
|
110
|
+
String(err),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
41
115
|
let videoRecorderModule: any = null;
|
|
42
116
|
let videoRecordingActive = false;
|
|
43
117
|
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* yaver-feedback-react-native — Visual feedback SDK for Yaver.
|
|
3
3
|
*
|
|
4
|
-
* Shake-to-report surface with
|
|
4
|
+
* Shake-to-report surface with three launch actions:
|
|
5
5
|
* 1. Hot Reload — instant JS reload
|
|
6
|
-
* 2.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* 2. Vibing — open a vibing session on the agent
|
|
7
|
+
* 3. Screenshot & Fix — capture the current screen and trigger
|
|
8
|
+
* the fix loop
|
|
9
|
+
*
|
|
10
|
+
* The small quick-access icon stays hidden until the first shake by
|
|
11
|
+
* default on mobile, then remains available unless the user hides it.
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* ```tsx
|
|
@@ -83,6 +84,7 @@ export type {
|
|
|
83
84
|
} from './auth';
|
|
84
85
|
export {
|
|
85
86
|
captureScreenshot,
|
|
87
|
+
pickFeedbackFile,
|
|
86
88
|
startVideoRecording,
|
|
87
89
|
stopVideoRecording,
|
|
88
90
|
isVideoRecording,
|
package/src/preferences.ts
CHANGED
|
@@ -25,6 +25,69 @@ try {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const QUICK_ICON_DISABLED_KEY = 'yaver_feedback_quickicon_disabled';
|
|
28
|
+
const QUICK_ICON_COLOR_KEY = 'yaver_feedback_quickicon_color';
|
|
29
|
+
|
|
30
|
+
export type QuickIconColorPreset =
|
|
31
|
+
| 'orange'
|
|
32
|
+
| 'lime'
|
|
33
|
+
| 'cyan'
|
|
34
|
+
| 'pink'
|
|
35
|
+
| 'yellow'
|
|
36
|
+
| 'slate';
|
|
37
|
+
|
|
38
|
+
export const QUICK_ICON_COLOR_PRESETS: Record<
|
|
39
|
+
QuickIconColorPreset,
|
|
40
|
+
{
|
|
41
|
+
label: string;
|
|
42
|
+
backgroundColor: string;
|
|
43
|
+
foregroundColor: string;
|
|
44
|
+
borderColor: string;
|
|
45
|
+
shadowColor: string;
|
|
46
|
+
}
|
|
47
|
+
> = {
|
|
48
|
+
orange: {
|
|
49
|
+
label: 'Orange',
|
|
50
|
+
backgroundColor: '#ff6b2c',
|
|
51
|
+
foregroundColor: '#111111',
|
|
52
|
+
borderColor: 'rgba(255,255,255,0.92)',
|
|
53
|
+
shadowColor: '#000000',
|
|
54
|
+
},
|
|
55
|
+
lime: {
|
|
56
|
+
label: 'Lime',
|
|
57
|
+
backgroundColor: '#a3e635',
|
|
58
|
+
foregroundColor: '#111111',
|
|
59
|
+
borderColor: 'rgba(255,255,255,0.85)',
|
|
60
|
+
shadowColor: '#365314',
|
|
61
|
+
},
|
|
62
|
+
cyan: {
|
|
63
|
+
label: 'Cyan',
|
|
64
|
+
backgroundColor: '#22d3ee',
|
|
65
|
+
foregroundColor: '#082f49',
|
|
66
|
+
borderColor: 'rgba(255,255,255,0.82)',
|
|
67
|
+
shadowColor: '#083344',
|
|
68
|
+
},
|
|
69
|
+
pink: {
|
|
70
|
+
label: 'Pink',
|
|
71
|
+
backgroundColor: '#fb7185',
|
|
72
|
+
foregroundColor: '#fff1f2',
|
|
73
|
+
borderColor: 'rgba(255,255,255,0.78)',
|
|
74
|
+
shadowColor: '#4c0519',
|
|
75
|
+
},
|
|
76
|
+
yellow: {
|
|
77
|
+
label: 'Yellow',
|
|
78
|
+
backgroundColor: '#facc15',
|
|
79
|
+
foregroundColor: '#1c1917',
|
|
80
|
+
borderColor: 'rgba(255,255,255,0.88)',
|
|
81
|
+
shadowColor: '#713f12',
|
|
82
|
+
},
|
|
83
|
+
slate: {
|
|
84
|
+
label: 'Slate',
|
|
85
|
+
backgroundColor: '#475569',
|
|
86
|
+
foregroundColor: '#f8fafc',
|
|
87
|
+
borderColor: 'rgba(255,255,255,0.68)',
|
|
88
|
+
shadowColor: '#020617',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
28
91
|
|
|
29
92
|
/** True if the user has long-pressed the icon and chosen "Hide". */
|
|
30
93
|
export async function getQuickIconDisabled(): Promise<boolean> {
|
|
@@ -53,3 +116,36 @@ export async function setQuickIconDisabled(disabled: boolean): Promise<void> {
|
|
|
53
116
|
export async function clearQuickIconDisabled(): Promise<void> {
|
|
54
117
|
await setQuickIconDisabled(false);
|
|
55
118
|
}
|
|
119
|
+
|
|
120
|
+
export async function getQuickIconColorPreset(): Promise<QuickIconColorPreset | null> {
|
|
121
|
+
if (!AsyncStorage) return null;
|
|
122
|
+
try {
|
|
123
|
+
const v = await AsyncStorage.getItem(QUICK_ICON_COLOR_KEY);
|
|
124
|
+
if (!v) return null;
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(QUICK_ICON_COLOR_PRESETS, v)) {
|
|
126
|
+
return v as QuickIconColorPreset;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function setQuickIconColorPreset(
|
|
135
|
+
preset: QuickIconColorPreset | null,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
if (!AsyncStorage) return;
|
|
138
|
+
try {
|
|
139
|
+
if (!preset) {
|
|
140
|
+
await AsyncStorage.removeItem(QUICK_ICON_COLOR_KEY);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await AsyncStorage.setItem(QUICK_ICON_COLOR_KEY, preset);
|
|
144
|
+
} catch {
|
|
145
|
+
// best-effort
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function clearQuickIconColorPreset(): Promise<void> {
|
|
150
|
+
await setQuickIconColorPreset(null);
|
|
151
|
+
}
|