yaver-feedback-react-native 0.8.1 → 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/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
@@ -317,7 +326,7 @@ export class P2PClient {
317
326
  async reloadApp(
318
327
  mode: 'dev' | 'bundle' = 'bundle',
319
328
  opts?: { projectName?: string; bundleId?: string; projectPath?: string },
320
- ): Promise<{ ok: boolean }> {
329
+ ): Promise<ReloadAck> {
321
330
  // Default path: always rebuild a fresh Hermes bundle.
322
331
  //
323
332
  // Rationale: the SDK's common caller is a phone user who's not
@@ -338,7 +347,19 @@ export class P2PClient {
338
347
  headers: { Authorization: `Bearer ${this.authToken}` },
339
348
  });
340
349
  if (primary.ok) {
341
- return primary.json().catch(() => ({ ok: true }));
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
+ };
342
363
  }
343
364
  // Dev mode failed — fall through to bundle rebuild below rather
344
365
  // than surfacing the raw error, so the user never has to know
@@ -370,7 +391,19 @@ export class P2PClient {
370
391
  const text = await res.text().catch(() => '');
371
392
  throw new Error(friendlyReloadError(res.status, text));
372
393
  }
373
- return res.json().catch(() => ({ ok: true }));
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
+ };
374
407
  }
375
408
 
376
409
  /**
@@ -12,7 +12,13 @@ import {
12
12
  View,
13
13
  } from 'react-native';
14
14
  import { YaverFeedback } from './YaverFeedback';
15
- import { getQuickIconDisabled, setQuickIconDisabled } from './preferences';
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
@@ -123,6 +129,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
123
129
  const didDrag = useRef(false);
124
130
 
125
131
  const [userDisabled, setUserDisabled] = useState<boolean | null>(null);
132
+ const [colorPreset, setColorPreset] = useState<keyof typeof QUICK_ICON_COLOR_PRESETS | null>(null);
126
133
  const [shakenThisSession, setShakenThisSession] = useState(false);
127
134
  const [menuOpen, setMenuOpen] = useState(false);
128
135
  const [hostSuppressed] = useState<boolean>(() => isRunningInsideYaverHost());
@@ -135,6 +142,9 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
135
142
  getQuickIconDisabled().then((v) => {
136
143
  if (alive) setUserDisabled(v);
137
144
  });
145
+ getQuickIconColorPreset().then((v) => {
146
+ if (alive) setColorPreset(v);
147
+ });
138
148
  return () => {
139
149
  alive = false;
140
150
  };
@@ -169,9 +179,18 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
169
179
  setMenuOpen(false);
170
180
  },
171
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
+ );
172
190
  return () => {
173
191
  showSub.remove();
174
192
  hideSub.remove();
193
+ colorSub.remove();
175
194
  };
176
195
  }, []);
177
196
 
@@ -229,6 +248,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
229
248
  if (mode === 'after-shake' && !shakenThisSession) return null;
230
249
  if (!YaverFeedback.isEnabled()) return null;
231
250
 
251
+ const presetColors = colorPreset ? QUICK_ICON_COLOR_PRESETS[colorPreset] : null;
232
252
  const visualSize = size;
233
253
  const radius = visualSize / 2;
234
254
 
@@ -271,9 +291,9 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
271
291
  width: visualSize,
272
292
  height: visualSize,
273
293
  borderRadius: radius,
274
- backgroundColor,
275
- borderColor,
276
- shadowColor,
294
+ backgroundColor: presetColors?.backgroundColor ?? backgroundColor,
295
+ borderColor: presetColors?.borderColor ?? borderColor,
296
+ shadowColor: presetColors?.shadowColor ?? shadowColor,
277
297
  opacity: pressed ? 0.85 : 1,
278
298
  },
279
299
  ]}
@@ -282,7 +302,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
282
302
  style={[
283
303
  styles.iconLabel,
284
304
  {
285
- color: foregroundColor,
305
+ color: presetColors?.foregroundColor ?? foregroundColor,
286
306
  fontSize: Math.round(visualSize * 0.5),
287
307
  },
288
308
  ]}
@@ -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
@@ -362,6 +366,15 @@ export class YaverFeedback {
362
366
  await YaverFeedback.discoverAgent();
363
367
  }
364
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
+
365
378
  /**
366
379
  * Sign out: clear cached token + device, tear down the P2P client. The
367
380
  * SDK stays enabled; the next feedback trigger will re-prompt for login.
@@ -859,6 +872,22 @@ export class YaverFeedback {
859
872
  return getQuickIconDisabled();
860
873
  }
861
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
+
862
891
  /** Clear the persisted "user hid the icon" flag. */
863
892
  static async resetQuickIconPreference(): Promise<void> {
864
893
  await YaverFeedback.setQuickIconVisible(true);
@@ -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/index.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * yaver-feedback-react-native — Visual feedback SDK for Yaver.
3
3
  *
4
- * Shake-to-report surface with four core actions:
4
+ * Shake-to-report surface with three launch actions:
5
5
  * 1. Hot Reload — instant JS reload
6
6
  * 2. Vibing — open a vibing session on the agent
7
- * 3. Screenshot / Upload — capture the screen under the modal or
8
- * upload existing media
9
- * 4. Screen Recording — start, then stop + upload
7
+ * 3. Screenshot & Fix — capture the current screen and trigger
8
+ * the fix loop
10
9
  *
11
10
  * The small quick-access icon stays hidden until the first shake by
12
11
  * default on mobile, then remains available unless the user hides it.
@@ -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
+ }