yaver-feedback-react-native 0.6.1 → 0.7.1

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.
@@ -79,7 +79,11 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
79
79
  };
80
80
  const renderDevice = (device) => {
81
81
  const selected = device.deviceId === currentDeviceId;
82
- const stale = Date.now() - device.lastHeartbeat > 60000;
82
+ // Match the Yaver mobile app: HEARTBEAT_STALE_MS is 90 s. Using
83
+ // 60 s here flashed yellow on a single missed agent beat even
84
+ // though the Mac was up.
85
+ const stale = device.lastHeartbeat > 0 &&
86
+ Date.now() - device.lastHeartbeat > 90000;
83
87
  const healthColor = !device.isOnline
84
88
  ? '#ef4444'
85
89
  : device.needsAuth || device.runnerDown || stale
@@ -68,6 +68,30 @@ export declare class P2PClient {
68
68
  reloadApp(mode?: 'dev' | 'bundle'): Promise<{
69
69
  ok: boolean;
70
70
  }>;
71
+ /**
72
+ * Open a vibing session on the connected agent. Vibing is the Yaver
73
+ * interactive coding-agent flow — `/vibing/execute` creates a task with
74
+ * the project context plus the user's prompt. Returns the task id the
75
+ * caller can poll via `/tasks/{id}` if needed.
76
+ *
77
+ * Requires an owner/CLI/paired token — the `/vibing*` routes do not
78
+ * currently accept SDK-minted tokens. Power users typically drive
79
+ * vibing from Claude Code / the Yaver mobile app; this method is a
80
+ * convenience for the SDK's one-tap bug-report-to-vibing path.
81
+ */
82
+ vibing(prompt: string, projectPath?: string): Promise<{
83
+ taskId: string;
84
+ }>;
85
+ /**
86
+ * After uploading a feedback bundle with `uploadFeedback`, call this
87
+ * with the returned report id to create a fix task on the agent. The
88
+ * task includes the feedback's screenshots, errors, and (when available)
89
+ * the BlackBox context for the originating device.
90
+ */
91
+ triggerFix(feedbackId: string): Promise<{
92
+ taskId: string;
93
+ prompt: string;
94
+ }>;
71
95
  /** Get the download URL for a build artifact. */
72
96
  getArtifactUrl(buildId: string): string;
73
97
  /**
package/dist/P2PClient.js CHANGED
@@ -62,13 +62,6 @@ class P2PClient {
62
62
  name: `screenshot_${i}.png`,
63
63
  });
64
64
  }
65
- if (bundle.audio) {
66
- formData.append('audio', {
67
- uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
68
- type: 'audio/m4a',
69
- name: 'voice_note.m4a',
70
- });
71
- }
72
65
  if (bundle.video) {
73
66
  formData.append('video', {
74
67
  uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.video}` : bundle.video,
@@ -188,17 +181,79 @@ class P2PClient {
188
181
  * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
189
182
  */
190
183
  async reloadApp(mode = 'dev') {
191
- const response = await fetch(`${this.baseUrl}/dev/reload-app`, {
184
+ // Primary path: /dev/reload — same endpoint the Yaver mobile app uses.
185
+ // Triggers Metro/Expo HMR synchronously and emits an SSE `reload` event
186
+ // on /dev/events that the FeedbackModal can subscribe to for progress.
187
+ //
188
+ // Only fall back to /dev/reload-app (the BlackBox-SSE-broadcast path)
189
+ // when the primary path reports "no dev server running" — that mode is
190
+ // really for the mobile app remotely kicking a third-party app, not
191
+ // for the app kicking itself.
192
+ const primary = await fetch(`${this.baseUrl}/dev/reload`, {
193
+ method: 'POST',
194
+ headers: { Authorization: `Bearer ${this.authToken}` },
195
+ });
196
+ if (primary.ok) {
197
+ return primary.json().catch(() => ({ ok: true }));
198
+ }
199
+ if (primary.status >= 500 || primary.status === 404 || mode === 'bundle') {
200
+ const fallback = await fetch(`${this.baseUrl}/dev/reload-app`, {
201
+ method: 'POST',
202
+ headers: {
203
+ Authorization: `Bearer ${this.authToken}`,
204
+ 'Content-Type': 'application/json',
205
+ },
206
+ body: JSON.stringify({ mode }),
207
+ });
208
+ if (!fallback.ok) {
209
+ const text = await fallback.text().catch(() => '');
210
+ throw new Error(`[P2PClient] Reload failed (${fallback.status}): ${text}`);
211
+ }
212
+ return fallback.json().catch(() => ({ ok: true }));
213
+ }
214
+ const text = await primary.text().catch(() => '');
215
+ throw new Error(`[P2PClient] Reload failed (${primary.status}): ${text}`);
216
+ }
217
+ /**
218
+ * Open a vibing session on the connected agent. Vibing is the Yaver
219
+ * interactive coding-agent flow — `/vibing/execute` creates a task with
220
+ * the project context plus the user's prompt. Returns the task id the
221
+ * caller can poll via `/tasks/{id}` if needed.
222
+ *
223
+ * Requires an owner/CLI/paired token — the `/vibing*` routes do not
224
+ * currently accept SDK-minted tokens. Power users typically drive
225
+ * vibing from Claude Code / the Yaver mobile app; this method is a
226
+ * convenience for the SDK's one-tap bug-report-to-vibing path.
227
+ */
228
+ async vibing(prompt, projectPath) {
229
+ const response = await fetch(`${this.baseUrl}/vibing/execute`, {
192
230
  method: 'POST',
193
231
  headers: {
194
232
  Authorization: `Bearer ${this.authToken}`,
195
233
  'Content-Type': 'application/json',
196
234
  },
197
- body: JSON.stringify({ mode }),
235
+ body: JSON.stringify({ prompt, projectPath: projectPath ?? '' }),
236
+ });
237
+ if (!response.ok) {
238
+ const text = await response.text().catch(() => '');
239
+ throw new Error(`[P2PClient] Vibing failed (${response.status}): ${text}`);
240
+ }
241
+ return response.json();
242
+ }
243
+ /**
244
+ * After uploading a feedback bundle with `uploadFeedback`, call this
245
+ * with the returned report id to create a fix task on the agent. The
246
+ * task includes the feedback's screenshots, errors, and (when available)
247
+ * the BlackBox context for the originating device.
248
+ */
249
+ async triggerFix(feedbackId) {
250
+ const response = await fetch(`${this.baseUrl}/feedback/${encodeURIComponent(feedbackId)}/fix`, {
251
+ method: 'POST',
252
+ headers: { Authorization: `Bearer ${this.authToken}` },
198
253
  });
199
254
  if (!response.ok) {
200
255
  const text = await response.text().catch(() => '');
201
- throw new Error(`[P2PClient] Reload app failed (${response.status}): ${text}`);
256
+ throw new Error(`[P2PClient] Fix trigger failed (${response.status}): ${text}`);
202
257
  }
203
258
  return response.json();
204
259
  }
@@ -19,6 +19,16 @@ export declare class YaverFeedback {
19
19
  * Sets config.agentUrl and creates P2PClient on success.
20
20
  */
21
21
  static discoverAgent(): Promise<void>;
22
+ /**
23
+ * Force a fresh Convex lookup for the agent URL — ignoring any
24
+ * cached URL. Callers use this after a P2P request fails
25
+ * (connection refused / timeout) because the most common cause is
26
+ * the Mac's LAN IP rotating. Convex has the fresh one, so we
27
+ * re-query and probe `[quicHost, ...localIps]` in parallel.
28
+ *
29
+ * Returns true when a new URL was adopted.
30
+ */
31
+ static reconnect(): Promise<boolean>;
22
32
  /**
23
33
  * Pull a cached session token + selected device from AsyncStorage (populated
24
34
  * by the in-SDK login + machine-picker screens). When present the SDK can
@@ -116,10 +126,6 @@ export declare class YaverFeedback {
116
126
  * Available after init if agentUrl is set, or after first successful discovery.
117
127
  */
118
128
  static getP2PClient(): P2PClient | null;
119
- /** Returns the current feedback mode. */
120
- static getFeedbackMode(): 'live' | 'narrated' | 'batch';
121
- /** Returns the agent commentary level (0-10). */
122
- static getCommentaryLevel(): number;
123
129
  /**
124
130
  * Record a business event. Routes through BlackBox so the agent
125
131
  * persists it to the analytics ledger (no dashboards — export
@@ -65,8 +65,6 @@ class YaverFeedback {
65
65
  config = {
66
66
  trigger: 'shake',
67
67
  maxRecordingDuration: 120,
68
- feedbackMode: 'batch',
69
- agentCommentaryLevel: 0,
70
68
  autoLogin: true,
71
69
  ...cfg,
72
70
  };
@@ -190,6 +188,36 @@ class YaverFeedback {
190
188
  // Discovery failed — FloatingButton will show disconnected, user can retry
191
189
  }
192
190
  }
191
+ /**
192
+ * Force a fresh Convex lookup for the agent URL — ignoring any
193
+ * cached URL. Callers use this after a P2P request fails
194
+ * (connection refused / timeout) because the most common cause is
195
+ * the Mac's LAN IP rotating. Convex has the fresh one, so we
196
+ * re-query and probe `[quicHost, ...localIps]` in parallel.
197
+ *
198
+ * Returns true when a new URL was adopted.
199
+ */
200
+ static async reconnect() {
201
+ if (!config || !enabled)
202
+ return false;
203
+ if (!config.authToken || !config.convexUrl)
204
+ return false;
205
+ try {
206
+ const result = await Discovery_1.YaverDiscovery.refreshFromConvex({
207
+ convexUrl: config.convexUrl,
208
+ authToken: config.authToken,
209
+ preferredDeviceId: config.preferredDeviceId,
210
+ });
211
+ if (!result)
212
+ return false;
213
+ config.agentUrl = result.url;
214
+ p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
215
+ return true;
216
+ }
217
+ catch {
218
+ return false;
219
+ }
220
+ }
193
221
  /**
194
222
  * Pull a cached session token + selected device from AsyncStorage (populated
195
223
  * by the in-SDK login + machine-picker screens). When present the SDK can
@@ -464,14 +492,6 @@ class YaverFeedback {
464
492
  static getP2PClient() {
465
493
  return p2pClient;
466
494
  }
467
- /** Returns the current feedback mode. */
468
- static getFeedbackMode() {
469
- return config?.feedbackMode ?? 'batch';
470
- }
471
- /** Returns the agent commentary level (0-10). */
472
- static getCommentaryLevel() {
473
- return config?.agentCommentaryLevel ?? 0;
474
- }
475
495
  // ─── One-stop SaaS replacement methods ─────────────────────────
476
496
  //
477
497
  // These are the three solo-dev SaaS-replacement entry points
@@ -36,22 +36,18 @@ describe('YaverFeedback', () => {
36
36
  expect(cfg.agentUrl).toBe('http://localhost:18080');
37
37
  expect(cfg.trigger).toBe('shake');
38
38
  expect(cfg.maxRecordingDuration).toBe(120);
39
- expect(cfg.feedbackMode).toBe('batch');
40
- expect(cfg.agentCommentaryLevel).toBe(0);
41
39
  });
42
40
  it('respects user-provided values over defaults', () => {
43
41
  YaverFeedback_1.YaverFeedback.init({
44
42
  authToken: 'tok',
45
43
  trigger: 'floating-button',
46
44
  maxRecordingDuration: 60,
47
- feedbackMode: 'live',
48
- agentCommentaryLevel: 7,
45
+ strictNativeAuth: true,
49
46
  });
50
47
  const cfg = YaverFeedback_1.YaverFeedback.getConfig();
51
48
  expect(cfg.trigger).toBe('floating-button');
52
49
  expect(cfg.maxRecordingDuration).toBe(60);
53
- expect(cfg.feedbackMode).toBe('live');
54
- expect(cfg.agentCommentaryLevel).toBe(7);
50
+ expect(cfg.strictNativeAuth).toBe(true);
55
51
  });
56
52
  it('with enabled=false sets enabled to false', () => {
57
53
  YaverFeedback_1.YaverFeedback.init({
@@ -110,35 +106,6 @@ describe('YaverFeedback', () => {
110
106
  expect(cfg.agentUrl).toBe('http://10.0.0.1:18080');
111
107
  });
112
108
  });
113
- describe('getFeedbackMode()', () => {
114
- it('defaults to batch when no config', () => {
115
- // After any init, feedbackMode defaults to 'batch'
116
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
117
- expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('batch');
118
- });
119
- it('returns configured mode', () => {
120
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'narrated' });
121
- expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('narrated');
122
- });
123
- it('returns live when configured', () => {
124
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', feedbackMode: 'live' });
125
- expect(YaverFeedback_1.YaverFeedback.getFeedbackMode()).toBe('live');
126
- });
127
- });
128
- describe('getCommentaryLevel()', () => {
129
- it('defaults to 0', () => {
130
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok' });
131
- expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(0);
132
- });
133
- it('returns configured level', () => {
134
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 5 });
135
- expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(5);
136
- });
137
- it('returns max level when set to 10', () => {
138
- YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', agentCommentaryLevel: 10 });
139
- expect(YaverFeedback_1.YaverFeedback.getCommentaryLevel()).toBe(10);
140
- });
141
- });
142
109
  describe('startReport()', () => {
143
110
  it('does nothing when not enabled', async () => {
144
111
  YaverFeedback_1.YaverFeedback.init({ authToken: 'tok', enabled: false });
@@ -11,8 +11,6 @@ describe('React Native SDK types', () => {
11
11
  expect(config.trigger).toBeUndefined();
12
12
  expect(config.enabled).toBeUndefined();
13
13
  expect(config.maxRecordingDuration).toBeUndefined();
14
- expect(config.feedbackMode).toBeUndefined();
15
- expect(config.agentCommentaryLevel).toBeUndefined();
16
14
  });
17
15
  it('can be constructed with all optional fields', () => {
18
16
  const config = {
@@ -21,12 +19,10 @@ describe('React Native SDK types', () => {
21
19
  trigger: 'shake',
22
20
  enabled: true,
23
21
  maxRecordingDuration: 60,
24
- feedbackMode: 'live',
25
- agentCommentaryLevel: 7,
22
+ strictNativeAuth: true,
26
23
  };
27
24
  expect(config.trigger).toBe('shake');
28
- expect(config.feedbackMode).toBe('live');
29
- expect(config.agentCommentaryLevel).toBe(7);
25
+ expect(config.strictNativeAuth).toBe(true);
30
26
  });
31
27
  it('accepts all trigger types', () => {
32
28
  const triggers = ['shake', 'floating-button', 'manual'];
@@ -35,13 +31,6 @@ describe('React Native SDK types', () => {
35
31
  expect(config.trigger).toBe(trigger);
36
32
  });
37
33
  });
38
- it('accepts all feedback modes', () => {
39
- const modes = ['live', 'narrated', 'batch'];
40
- modes.forEach((mode) => {
41
- const config = { authToken: 'tok', feedbackMode: mode };
42
- expect(config.feedbackMode).toBe(mode);
43
- });
44
- });
45
34
  });
46
35
  describe('FeedbackBundle', () => {
47
36
  it('can be constructed with required fields', () => {
@@ -67,9 +56,8 @@ describe('React Native SDK types', () => {
67
56
  expect(bundle.metadata.device.platform).toBe('ios');
68
57
  expect(bundle.screenshots).toEqual([]);
69
58
  expect(bundle.video).toBeUndefined();
70
- expect(bundle.audio).toBeUndefined();
71
59
  });
72
- it('can include optional video, audio, and screenshots', () => {
60
+ it('can include optional video + screenshots', () => {
73
61
  const bundle = {
74
62
  metadata: {
75
63
  timestamp: '2026-03-24T12:00:00Z',
@@ -84,11 +72,9 @@ describe('React Native SDK types', () => {
84
72
  userNote: 'This button does not work',
85
73
  },
86
74
  video: '/tmp/recording.mp4',
87
- audio: '/tmp/voice.m4a',
88
75
  screenshots: ['/tmp/ss1.png', '/tmp/ss2.png'],
89
76
  };
90
77
  expect(bundle.video).toBe('/tmp/recording.mp4');
91
- expect(bundle.audio).toBe('/tmp/voice.m4a');
92
78
  expect(bundle.screenshots).toHaveLength(2);
93
79
  expect(bundle.metadata.userNote).toBe('This button does not work');
94
80
  });
@@ -187,24 +173,6 @@ describe('React Native SDK types', () => {
187
173
  });
188
174
  });
189
175
  });
190
- describe('AgentCommentary', () => {
191
- it('has correct structure', () => {
192
- const commentary = {
193
- id: 'cmt-1',
194
- timestamp: '2026-03-24T12:00:00Z',
195
- message: 'I see a layout issue on the login screen',
196
- type: 'observation',
197
- };
198
- expect(commentary.type).toBe('observation');
199
- });
200
- it('accepts all commentary types', () => {
201
- const types = ['observation', 'suggestion', 'question', 'action'];
202
- types.forEach((type) => {
203
- const c = { id: '1', timestamp: 'now', message: 'test', type };
204
- expect(c.type).toBe(type);
205
- });
206
- });
207
- });
208
176
  describe('FeedbackStreamEvent', () => {
209
177
  it('has correct structure', () => {
210
178
  const event = {
package/dist/auth.d.ts CHANGED
@@ -105,7 +105,17 @@ export interface RemoteDevice {
105
105
  accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
106
106
  quicHost: string;
107
107
  quicPort: number;
108
+ /** Agent HTTP port — preferred over quicPort when present. */
109
+ httpPort?: number;
108
110
  publicKey?: string;
111
+ /** Hardware identifier — used for dedup across re-pair events. */
112
+ hwid?: string;
113
+ /**
114
+ * Every LAN IP the agent reported in its last heartbeat. Useful on
115
+ * multi-homed hosts — probing all of them in parallel is the same
116
+ * trick the Yaver mobile app uses to "just work" on the same Wi-Fi.
117
+ */
118
+ localIps?: string[];
109
119
  }
110
120
  export interface DeviceList {
111
121
  owned: RemoteDevice[];
@@ -114,5 +124,9 @@ export interface DeviceList {
114
124
  /**
115
125
  * Fetch the set of remote dev machines this user can reach. Splits into
116
126
  * owned (user is the host) vs shared (host invited them as a guest).
127
+ *
128
+ * Collapses duplicate rows before splitting — Convex can return multiple
129
+ * rows per physical machine after a re-pair or hostname change, and the
130
+ * raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
117
131
  */
118
132
  export declare function listReachableDevices(token: string): Promise<DeviceList>;
package/dist/auth.js CHANGED
@@ -346,6 +346,10 @@ async function loginWithEmail(email, password) {
346
346
  /**
347
347
  * Fetch the set of remote dev machines this user can reach. Splits into
348
348
  * owned (user is the host) vs shared (host invited them as a guest).
349
+ *
350
+ * Collapses duplicate rows before splitting — Convex can return multiple
351
+ * rows per physical machine after a re-pair or hostname change, and the
352
+ * raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
349
353
  */
350
354
  async function listReachableDevices(token) {
351
355
  try {
@@ -355,10 +359,39 @@ async function listReachableDevices(token) {
355
359
  if (!res.ok)
356
360
  return { owned: [], shared: [] };
357
361
  const data = await res.json();
358
- const all = (data.devices ?? []);
362
+ const raw = (data.devices ?? []);
363
+ // Normalise Convex field names → SDK's RemoteDevice shape. The
364
+ // backend returns `localIps`, sometimes the mobile-side mapping
365
+ // surfaces `lanIps` — accept either so the field survives.
366
+ const normalised = raw.map((d) => ({
367
+ deviceId: d.deviceId ?? d.id,
368
+ name: d.name ?? '',
369
+ platform: d.platform ?? d.os ?? '',
370
+ isOnline: !!d.isOnline,
371
+ needsAuth: !!d.needsAuth,
372
+ runnerDown: !!d.runnerDown,
373
+ lastHeartbeat: d.lastHeartbeat ?? 0,
374
+ isGuest: !!d.isGuest,
375
+ hostName: d.hostName,
376
+ hostEmail: d.hostEmail,
377
+ accessScope: d.accessScope ?? 'owner',
378
+ quicHost: d.quicHost ?? d.host ?? '',
379
+ quicPort: d.quicPort ?? 0,
380
+ httpPort: d.httpPort ?? d.quicPort,
381
+ publicKey: d.publicKey,
382
+ hwid: d.hardwareId ?? d.hwid,
383
+ localIps: Array.isArray(d.localIps)
384
+ ? d.localIps
385
+ : Array.isArray(d.lanIps)
386
+ ? d.lanIps
387
+ : undefined,
388
+ }));
389
+ // Lazy require so Jest + tree-shakers don't choke on a circular import.
390
+ const { collapseRemoteDevices } = require('./deviceDedup');
391
+ const deduped = collapseRemoteDevices(normalised);
359
392
  return {
360
- owned: all.filter((d) => !d.isGuest),
361
- shared: all.filter((d) => d.isGuest),
393
+ owned: deduped.filter((d) => !d.isGuest),
394
+ shared: deduped.filter((d) => d.isGuest),
362
395
  };
363
396
  }
364
397
  catch {
package/dist/capture.d.ts CHANGED
@@ -1,27 +1,40 @@
1
1
  /**
2
- * Screen capture and audio recording helpers.
2
+ * Screen capture helpers screenshot + video recording.
3
3
  *
4
- * Screenshot capture requires `react-native-view-shot` as a peer dependency.
5
- * Audio recording requires `react-native-audio-recorder-player` or a
6
- * similar library the implementation below uses a minimal approach
7
- * that works when one of those is available.
4
+ * Peer deps (all optional loaded lazily):
5
+ * - `react-native-view-shot` screenshot
6
+ * - `react-native-record-screen` video recording (iOS ReplayKit /
7
+ * Android MediaProjection)
8
+ *
9
+ * Each helper surfaces a clear error if the module is missing so a host
10
+ * app knows exactly which peer dep to add. Audio-note / voice-command
11
+ * recording was removed in 0.7.0 — see FeedbackModal for the new
12
+ * 5-button surface.
8
13
  */
9
14
  /**
10
15
  * Capture the current screen as a PNG image.
11
16
  * Requires `react-native-view-shot` to be installed.
12
- * @returns File path of the captured screenshot.
17
+ *
18
+ * Note: the feedback modal should hide itself *before* calling this so the
19
+ * screenshot contains the underlying app state (the actual bug), not the
20
+ * modal. See `FeedbackModal.handleScreenshotForFix`.
13
21
  */
14
22
  export declare function captureScreenshot(): Promise<string>;
15
23
  /**
16
- * Start recording an audio voice note.
17
- * Requires `react-native-audio-recorder-player` to be installed.
24
+ * Start a screen-recording session. Requires
25
+ * `react-native-record-screen` as a peer dep.
26
+ *
27
+ * The user must grant the iOS ReplayKit / Android MediaProjection
28
+ * permission the first time; the prompt is shown by the native module,
29
+ * not the SDK.
18
30
  */
19
- export declare function startAudioRecording(): Promise<void>;
31
+ export declare function startVideoRecording(): Promise<void>;
20
32
  /**
21
- * Stop the current audio recording.
22
- * @returns Object with the file path and duration in seconds.
33
+ * Stop the current video recording and return the on-device file path.
23
34
  */
24
- export declare function stopAudioRecording(): Promise<{
35
+ export declare function stopVideoRecording(): Promise<{
25
36
  path: string;
26
37
  duration: number;
27
38
  }>;
39
+ /** Whether a video recording is currently active. */
40
+ export declare function isVideoRecording(): boolean;
package/dist/capture.js CHANGED
@@ -1,21 +1,29 @@
1
1
  "use strict";
2
2
  /**
3
- * Screen capture and audio recording helpers.
3
+ * Screen capture helpers screenshot + video recording.
4
4
  *
5
- * Screenshot capture requires `react-native-view-shot` as a peer dependency.
6
- * Audio recording requires `react-native-audio-recorder-player` or a
7
- * similar library the implementation below uses a minimal approach
8
- * that works when one of those is available.
5
+ * Peer deps (all optional loaded lazily):
6
+ * - `react-native-view-shot` screenshot
7
+ * - `react-native-record-screen` video recording (iOS ReplayKit /
8
+ * Android MediaProjection)
9
+ *
10
+ * Each helper surfaces a clear error if the module is missing so a host
11
+ * app knows exactly which peer dep to add. Audio-note / voice-command
12
+ * recording was removed in 0.7.0 — see FeedbackModal for the new
13
+ * 5-button surface.
9
14
  */
10
15
  Object.defineProperty(exports, "__esModule", { value: true });
11
16
  exports.captureScreenshot = captureScreenshot;
12
- exports.startAudioRecording = startAudioRecording;
13
- exports.stopAudioRecording = stopAudioRecording;
14
- let audioRecorderModule = null;
17
+ exports.startVideoRecording = startVideoRecording;
18
+ exports.stopVideoRecording = stopVideoRecording;
19
+ exports.isVideoRecording = isVideoRecording;
15
20
  /**
16
21
  * Capture the current screen as a PNG image.
17
22
  * Requires `react-native-view-shot` to be installed.
18
- * @returns File path of the captured screenshot.
23
+ *
24
+ * Note: the feedback modal should hide itself *before* calling this so the
25
+ * screenshot contains the underlying app state (the actual bug), not the
26
+ * modal. See `FeedbackModal.handleScreenshotForFix`.
19
27
  */
20
28
  async function captureScreenshot() {
21
29
  try {
@@ -27,48 +35,79 @@ async function captureScreenshot() {
27
35
  return uri;
28
36
  }
29
37
  catch (err) {
30
- throw new Error('[YaverFeedback] Screenshot capture failed. Make sure react-native-view-shot is installed. ' +
38
+ throw new Error('[YaverFeedback] Screenshot capture failed. Install react-native-view-shot as a peer dep. ' +
31
39
  String(err));
32
40
  }
33
41
  }
42
+ let videoRecorderModule = null;
43
+ let videoRecordingActive = false;
34
44
  /**
35
- * Start recording an audio voice note.
36
- * Requires `react-native-audio-recorder-player` to be installed.
45
+ * Start a screen-recording session. Requires
46
+ * `react-native-record-screen` as a peer dep.
47
+ *
48
+ * The user must grant the iOS ReplayKit / Android MediaProjection
49
+ * permission the first time; the prompt is shown by the native module,
50
+ * not the SDK.
37
51
  */
38
- async function startAudioRecording() {
52
+ async function startVideoRecording() {
53
+ if (videoRecordingActive) {
54
+ throw new Error('[YaverFeedback] A video recording is already in progress.');
55
+ }
39
56
  try {
40
- const AudioRecorderPlayer = require('react-native-audio-recorder-player').default;
41
- audioRecorderModule = new AudioRecorderPlayer();
42
- await audioRecorderModule.startRecorder();
57
+ videoRecorderModule = require('react-native-record-screen').default ??
58
+ require('react-native-record-screen');
59
+ if (typeof videoRecorderModule.startRecording !== 'function') {
60
+ throw new Error('react-native-record-screen missing startRecording()');
61
+ }
62
+ const result = await videoRecorderModule.startRecording({
63
+ mic: false,
64
+ width: 720,
65
+ bitrate: 1024 * 1000,
66
+ });
67
+ if (result && result.status && result.status !== 'success') {
68
+ throw new Error(`startRecording returned ${result.status}`);
69
+ }
70
+ videoRecordingActive = true;
43
71
  }
44
72
  catch (err) {
45
- audioRecorderModule = null;
46
- throw new Error('[YaverFeedback] Audio recording failed to start. Make sure react-native-audio-recorder-player is installed. ' +
73
+ videoRecorderModule = null;
74
+ videoRecordingActive = false;
75
+ throw new Error('[YaverFeedback] Could not start screen recording. Install react-native-record-screen. ' +
47
76
  String(err));
48
77
  }
49
78
  }
50
79
  /**
51
- * Stop the current audio recording.
52
- * @returns Object with the file path and duration in seconds.
80
+ * Stop the current video recording and return the on-device file path.
53
81
  */
54
- async function stopAudioRecording() {
55
- if (!audioRecorderModule) {
56
- throw new Error('[YaverFeedback] No audio recording in progress.');
82
+ async function stopVideoRecording() {
83
+ if (!videoRecordingActive || !videoRecorderModule) {
84
+ throw new Error('[YaverFeedback] No video recording in progress.');
57
85
  }
58
86
  try {
59
- const result = await audioRecorderModule.stopRecorder();
60
- const recorder = audioRecorderModule;
61
- audioRecorderModule = null;
62
- // result is the file path on most implementations
63
- const path = typeof result === 'string' ? result : result?.uri ?? '';
64
- // Duration tracking — recorder-player provides currentPosition in ms
65
- const durationMs = typeof recorder.currentPosition === 'number'
66
- ? recorder.currentPosition
67
- : 0;
87
+ const res = await videoRecorderModule.stopRecording();
88
+ videoRecordingActive = false;
89
+ const path = typeof res === 'string'
90
+ ? res
91
+ : res?.result?.outputURL ??
92
+ res?.outputURL ??
93
+ res?.uri ??
94
+ '';
95
+ const durationMs = typeof res?.result?.duration === 'number'
96
+ ? res.result.duration
97
+ : typeof res?.duration === 'number'
98
+ ? res.duration
99
+ : 0;
100
+ if (!path) {
101
+ throw new Error('stopRecording() returned no file path');
102
+ }
68
103
  return { path, duration: durationMs / 1000 };
69
104
  }
70
105
  catch (err) {
71
- audioRecorderModule = null;
72
- throw new Error('[YaverFeedback] Failed to stop audio recording. ' + String(err));
106
+ videoRecordingActive = false;
107
+ throw new Error('[YaverFeedback] Failed to stop screen recording. ' + String(err));
73
108
  }
74
109
  }
110
+ /** Whether a video recording is currently active. */
111
+ function isVideoRecording() {
112
+ return videoRecordingActive;
113
+ }