yaver-feedback-react-native 0.5.3 → 0.5.4

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.
Files changed (50) hide show
  1. package/README.md +8 -0
  2. package/dist/AuthOverlay.d.ts +16 -0
  3. package/dist/AuthOverlay.js +104 -0
  4. package/dist/BlackBox.d.ts +154 -0
  5. package/dist/BlackBox.js +395 -0
  6. package/dist/ConnectionScreen.d.ts +13 -0
  7. package/dist/ConnectionScreen.js +373 -0
  8. package/dist/Discovery.d.ts +59 -0
  9. package/dist/Discovery.js +293 -0
  10. package/dist/FeedbackModal.d.ts +11 -0
  11. package/dist/FeedbackModal.js +623 -0
  12. package/dist/FixReport.d.ts +23 -0
  13. package/dist/FixReport.js +282 -0
  14. package/dist/FloatingButton.d.ts +71 -0
  15. package/dist/FloatingButton.js +778 -0
  16. package/dist/LoginScreen.d.ts +14 -0
  17. package/dist/LoginScreen.js +317 -0
  18. package/dist/MachinePickerScreen.d.ts +19 -0
  19. package/dist/MachinePickerScreen.js +175 -0
  20. package/dist/P2PClient.d.ts +136 -0
  21. package/dist/P2PClient.js +357 -0
  22. package/dist/ShakeDetector.d.ts +39 -0
  23. package/dist/ShakeDetector.js +111 -0
  24. package/dist/YaverFeedback.d.ts +198 -0
  25. package/dist/YaverFeedback.js +679 -0
  26. package/dist/YaverUpdates.d.ts +78 -0
  27. package/dist/YaverUpdates.js +272 -0
  28. package/dist/__tests__/Discovery.test.d.ts +1 -0
  29. package/dist/__tests__/Discovery.test.js +164 -0
  30. package/dist/__tests__/P2PClient.test.d.ts +1 -0
  31. package/dist/__tests__/P2PClient.test.js +169 -0
  32. package/dist/__tests__/SDKToken.test.d.ts +1 -0
  33. package/dist/__tests__/SDKToken.test.js +215 -0
  34. package/dist/__tests__/YaverFeedback.test.d.ts +1 -0
  35. package/dist/__tests__/YaverFeedback.test.js +161 -0
  36. package/dist/__tests__/types.test.d.ts +1 -0
  37. package/dist/__tests__/types.test.js +219 -0
  38. package/dist/auth.d.ts +105 -0
  39. package/dist/auth.js +282 -0
  40. package/dist/capture.d.ts +27 -0
  41. package/dist/capture.js +74 -0
  42. package/dist/expo.d.ts +15 -0
  43. package/dist/expo.js +62 -0
  44. package/dist/index.d.ts +48 -0
  45. package/dist/index.js +80 -0
  46. package/dist/types.d.ts +282 -0
  47. package/dist/types.js +2 -0
  48. package/dist/upload.d.ts +13 -0
  49. package/dist/upload.js +59 -0
  50. package/package.json +6 -3
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.P2PClient = void 0;
4
+ const react_native_1 = require("react-native");
5
+ /**
6
+ * Lightweight P2P HTTP client for communicating with a Yaver agent.
7
+ *
8
+ * Reuses the same endpoint patterns as the main upload module but adds
9
+ * support for streaming feedback, listing builds, and triggering builds.
10
+ */
11
+ class P2PClient {
12
+ constructor(baseUrl, authToken) {
13
+ this.baseUrl = baseUrl.replace(/\/$/, '');
14
+ this.authToken = authToken;
15
+ }
16
+ /** Update the base URL (e.g. after re-discovery). */
17
+ setBaseUrl(url) {
18
+ this.baseUrl = url.replace(/\/$/, '');
19
+ }
20
+ /** Update the auth token. */
21
+ setAuthToken(token) {
22
+ this.authToken = token;
23
+ }
24
+ /** Health check — returns true if the agent is reachable. */
25
+ async health() {
26
+ try {
27
+ const controller = new AbortController();
28
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
29
+ const response = await fetch(`${this.baseUrl}/health`, {
30
+ method: 'GET',
31
+ signal: controller.signal,
32
+ });
33
+ clearTimeout(timeoutId);
34
+ return response.ok;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /** Get agent info (hostname, version, platform). */
41
+ async info() {
42
+ const response = await this.request('GET', '/health');
43
+ const data = await response.json();
44
+ return {
45
+ hostname: data.hostname ?? data.name ?? 'Unknown',
46
+ version: data.version ?? 'unknown',
47
+ platform: data.platform ?? 'unknown',
48
+ };
49
+ }
50
+ /**
51
+ * Upload a feedback bundle via multipart POST.
52
+ * @returns The feedback report ID from the agent.
53
+ */
54
+ async uploadFeedback(bundle) {
55
+ const formData = new FormData();
56
+ formData.append('metadata', JSON.stringify(bundle.metadata));
57
+ for (let i = 0; i < bundle.screenshots.length; i++) {
58
+ const path = bundle.screenshots[i];
59
+ formData.append(`screenshot_${i}`, {
60
+ uri: react_native_1.Platform.OS === 'android' ? `file://${path}` : path,
61
+ type: 'image/png',
62
+ name: `screenshot_${i}.png`,
63
+ });
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
+ if (bundle.video) {
73
+ formData.append('video', {
74
+ uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.video}` : bundle.video,
75
+ type: 'video/mp4',
76
+ name: 'screen_recording.mp4',
77
+ });
78
+ }
79
+ const response = await fetch(`${this.baseUrl}/feedback`, {
80
+ method: 'POST',
81
+ headers: {
82
+ Authorization: `Bearer ${this.authToken}`,
83
+ },
84
+ body: formData,
85
+ });
86
+ if (!response.ok) {
87
+ const text = await response.text().catch(() => '');
88
+ throw new Error(`[P2PClient] Upload failed (${response.status}): ${text}`);
89
+ }
90
+ const result = await response.json();
91
+ return result.id ?? result.reportId ?? 'unknown';
92
+ }
93
+ /**
94
+ * Stream feedback events to the agent in live mode.
95
+ * Sends each event as a JSON POST to `/feedback/stream`.
96
+ */
97
+ async streamFeedback(events) {
98
+ for await (const event of events) {
99
+ const response = await fetch(`${this.baseUrl}/feedback/stream`, {
100
+ method: 'POST',
101
+ headers: {
102
+ Authorization: `Bearer ${this.authToken}`,
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify(event),
106
+ });
107
+ if (!response.ok) {
108
+ const text = await response.text().catch(() => '');
109
+ throw new Error(`[P2PClient] Stream event failed (${response.status}): ${text}`);
110
+ }
111
+ }
112
+ }
113
+ /** List available builds from the agent. */
114
+ async listBuilds() {
115
+ const response = await this.request('GET', '/builds');
116
+ const data = await response.json();
117
+ return data.builds ?? data ?? [];
118
+ }
119
+ /** Start a build for the given platform. */
120
+ async startBuild(platform) {
121
+ const response = await fetch(`${this.baseUrl}/builds`, {
122
+ method: 'POST',
123
+ headers: {
124
+ Authorization: `Bearer ${this.authToken}`,
125
+ 'Content-Type': 'application/json',
126
+ },
127
+ body: JSON.stringify({ platform }),
128
+ });
129
+ if (!response.ok) {
130
+ const text = await response.text().catch(() => '');
131
+ throw new Error(`[P2PClient] Start build failed (${response.status}): ${text}`);
132
+ }
133
+ return response.json();
134
+ }
135
+ /**
136
+ * Get voice capability info from the agent.
137
+ * voiceInputEnabled is always true — mobile can always record and send audio.
138
+ * s2sProvider/sttProvider indicate whether transcription is available.
139
+ */
140
+ async voiceStatus() {
141
+ const response = await this.request('GET', '/voice/status');
142
+ const data = await response.json();
143
+ return {
144
+ voiceInputEnabled: data.voiceInputEnabled ?? true,
145
+ s2sProvider: data.s2sProvider ?? undefined,
146
+ s2sReady: data.s2sReady ?? false,
147
+ sttProvider: data.sttProvider ?? undefined,
148
+ sttReady: data.sttReady ?? false,
149
+ };
150
+ }
151
+ /**
152
+ * Send voice audio to the agent for transcription.
153
+ * Works with any configured STT or S2S provider on the agent.
154
+ * If no provider is configured, audio is saved for manual review.
155
+ * @returns Transcribed text (or empty string if no provider available).
156
+ */
157
+ async transcribeVoice(audioUri) {
158
+ const formData = new FormData();
159
+ formData.append('audio', {
160
+ uri: react_native_1.Platform.OS === 'android' ? `file://${audioUri}` : audioUri,
161
+ type: 'audio/wav',
162
+ name: 'voice_input.wav',
163
+ });
164
+ const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
165
+ method: 'POST',
166
+ headers: {
167
+ Authorization: `Bearer ${this.authToken}`,
168
+ },
169
+ body: formData,
170
+ });
171
+ if (!response.ok) {
172
+ const text = await response.text().catch(() => '');
173
+ throw new Error(`[P2PClient] Voice transcribe failed (${response.status}): ${text}`);
174
+ }
175
+ const result = await response.json();
176
+ return {
177
+ text: result.text ?? '',
178
+ provider: result.provider ?? 'none',
179
+ audioFile: result.audioFile,
180
+ };
181
+ }
182
+ /**
183
+ * Trigger a reload of the third-party app.
184
+ * In dev mode, this tells the dev server to hot-reload.
185
+ * In bundle mode, this rebuilds the native bundle and pushes it.
186
+ * The reload command is also broadcast to all connected SDK devices
187
+ * via the BlackBox command channel.
188
+ * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
189
+ */
190
+ async reloadApp(mode = 'dev') {
191
+ const response = await fetch(`${this.baseUrl}/dev/reload-app`, {
192
+ method: 'POST',
193
+ headers: {
194
+ Authorization: `Bearer ${this.authToken}`,
195
+ 'Content-Type': 'application/json',
196
+ },
197
+ body: JSON.stringify({ mode }),
198
+ });
199
+ if (!response.ok) {
200
+ const text = await response.text().catch(() => '');
201
+ throw new Error(`[P2PClient] Reload app failed (${response.status}): ${text}`);
202
+ }
203
+ return response.json();
204
+ }
205
+ /** Get the download URL for a build artifact. */
206
+ getArtifactUrl(buildId) {
207
+ return `${this.baseUrl}/builds/${buildId}/artifact`;
208
+ }
209
+ /**
210
+ * Start an autonomous test session.
211
+ * The agent reads the codebase for context, then navigates the app
212
+ * on the connected device/emulator, catches exceptions via BlackBox,
213
+ * writes fixes, and hot reloads — all without committing.
214
+ */
215
+ async startTestSession() {
216
+ const response = await fetch(`${this.baseUrl}/test-app/start`, {
217
+ method: 'POST',
218
+ headers: {
219
+ Authorization: `Bearer ${this.authToken}`,
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({ source: 'feedback-sdk' }),
223
+ });
224
+ if (!response.ok) {
225
+ const text = await response.text().catch(() => '');
226
+ throw new Error(`[P2PClient] Start test session failed (${response.status}): ${text}`);
227
+ }
228
+ return response.json();
229
+ }
230
+ /** Stop a running test session. */
231
+ async stopTestSession() {
232
+ await fetch(`${this.baseUrl}/test-app/stop`, {
233
+ method: 'POST',
234
+ headers: { Authorization: `Bearer ${this.authToken}` },
235
+ });
236
+ }
237
+ /** Get the current test session status and list of fixes. */
238
+ async getTestSession() {
239
+ const response = await this.request('GET', '/test-app/status');
240
+ return response.json();
241
+ }
242
+ /**
243
+ * Rotate the SDK token. The old token stays valid for 5 minutes (grace period).
244
+ * After rotation, the client automatically uses the new token.
245
+ * @returns The new token and its expiry time.
246
+ */
247
+ async rotateToken() {
248
+ const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
249
+ method: 'POST',
250
+ headers: {
251
+ Authorization: `Bearer ${this.authToken}`,
252
+ 'Content-Type': 'application/json',
253
+ },
254
+ });
255
+ if (!response.ok) {
256
+ const text = await response.text().catch(() => '');
257
+ throw new Error(`[P2PClient] Token rotation failed (${response.status}): ${text}`);
258
+ }
259
+ const result = await response.json();
260
+ // Auto-update to new token
261
+ this.authToken = result.token;
262
+ return { token: result.token, expiresAt: result.expiresAt };
263
+ }
264
+ // ─── Feature flags (F1) ──────────────────────────────────────────
265
+ /**
266
+ * Evaluate every flag for a userId. Hits /flags/eval which uses
267
+ * SHA256 bucketing against rolloutPercent — stable per user per
268
+ * flag. Results are the dev's source of truth; the SDK caches
269
+ * for 30s in getFlagsCached().
270
+ */
271
+ async flagsEvaluate(userId = 'anonymous') {
272
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
273
+ if (!res.ok)
274
+ return {};
275
+ const data = await res.json();
276
+ return data.flags ?? {};
277
+ }
278
+ /** Evaluate a single flag by key — shortcut when you only need one. */
279
+ async flagsEvaluateOne(key, userId = 'anonymous') {
280
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
281
+ if (!res.ok)
282
+ return undefined;
283
+ const data = await res.json();
284
+ return data.value;
285
+ }
286
+ // ─── Releases (R1) ───────────────────────────────────────────────
287
+ /**
288
+ * Ask what bundle this device should run. Returns the latest
289
+ * release in the channel plus a rollout gate. The mobile app
290
+ * uses this on cold start to decide whether to download a new
291
+ * bundle from /releases/bundle.
292
+ */
293
+ async releasesLatest(channel = 'production', deviceId) {
294
+ const params = new URLSearchParams({ channel });
295
+ if (deviceId)
296
+ params.set('device', deviceId);
297
+ const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
298
+ headers: { Authorization: `Bearer ${this.authToken}` },
299
+ });
300
+ if (!res.ok)
301
+ return null;
302
+ return res.json();
303
+ }
304
+ /** Download a specific bundle as raw bytes. */
305
+ async releasesDownload(channel, semver) {
306
+ const params = new URLSearchParams({ channel, semver });
307
+ const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
308
+ headers: { Authorization: `Bearer ${this.authToken}` },
309
+ });
310
+ if (!res.ok)
311
+ return null;
312
+ return res.arrayBuffer();
313
+ }
314
+ // ─── Analytics ingest (A1 — direct POST path) ───────────────────
315
+ /**
316
+ * Fire-and-forget track event. Most callers should use
317
+ * `BlackBox.track()` which fans through the streaming channel;
318
+ * this method is the fallback for surfaces without a live SSE.
319
+ */
320
+ async analyticsIngest(name, props, opts) {
321
+ try {
322
+ const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
323
+ method: 'POST',
324
+ headers: {
325
+ Authorization: `Bearer ${this.authToken}`,
326
+ 'Content-Type': 'application/json',
327
+ },
328
+ body: JSON.stringify({
329
+ name,
330
+ props,
331
+ deviceId: opts?.deviceId,
332
+ route: opts?.route,
333
+ timestamp: opts?.timestamp ?? Date.now(),
334
+ }),
335
+ });
336
+ return res.ok;
337
+ }
338
+ catch {
339
+ return false;
340
+ }
341
+ }
342
+ /** Internal helper for authenticated GET/POST requests. */
343
+ async request(method, path) {
344
+ const response = await fetch(`${this.baseUrl}${path}`, {
345
+ method,
346
+ headers: {
347
+ Authorization: `Bearer ${this.authToken}`,
348
+ },
349
+ });
350
+ if (!response.ok) {
351
+ const text = await response.text().catch(() => '');
352
+ throw new Error(`[P2PClient] ${method} ${path} failed (${response.status}): ${text}`);
353
+ }
354
+ return response;
355
+ }
356
+ }
357
+ exports.P2PClient = P2PClient;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Detects device shake gestures.
3
+ *
4
+ * Two detection paths run in parallel so shake works in both Debug and
5
+ * Release / TestFlight builds:
6
+ *
7
+ * 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
8
+ * on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
9
+ * on iOS, which is why TestFlight builds never saw a shake prior to
10
+ * SDK 0.5.1.
11
+ * 2. Accelerometer-based fallback via `expo-sensors` (optional peer
12
+ * dep). When the host app has `expo-sensors` installed we subscribe
13
+ * to the accelerometer and fire on a burst of above-threshold peaks.
14
+ * This path works identically in Debug, Release, and TestFlight.
15
+ *
16
+ * Both paths share a 1-second debounce so a single shake never fires the
17
+ * callback twice.
18
+ */
19
+ export declare class ShakeDetector {
20
+ private devMenuSub;
21
+ private accelSub;
22
+ private lastShakeTime;
23
+ private peakTimestamps;
24
+ start(onShake: () => void): void;
25
+ stop(): void;
26
+ private fire;
27
+ /**
28
+ * Dev-menu / platform-native event listener. On iOS this is the
29
+ * `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
30
+ * the `ShakeEvent` name a handful of third-party shake libraries emit.
31
+ */
32
+ private subscribeDevMenu;
33
+ /**
34
+ * Accelerometer-based detection. Uses `expo-sensors` when available —
35
+ * the host app's own dependency, not the SDK's, so apps that don't
36
+ * want the extra native surface get the Dev-menu path only.
37
+ */
38
+ private subscribeAccelerometer;
39
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShakeDetector = void 0;
4
+ const react_native_1 = require("react-native");
5
+ const SHAKE_TIMEOUT_MS = 1000; // minimum time between shakes
6
+ const ACCEL_THRESHOLD_G = 1.8; // peak g-force that qualifies as a shake event
7
+ const ACCEL_MIN_HITS = 3; // peaks within the window before we fire
8
+ const ACCEL_WINDOW_MS = 800; // rolling window
9
+ const ACCEL_SAMPLE_INTERVAL_MS = 80; // ≈12Hz — cheap but catches shakes
10
+ /**
11
+ * Detects device shake gestures.
12
+ *
13
+ * Two detection paths run in parallel so shake works in both Debug and
14
+ * Release / TestFlight builds:
15
+ *
16
+ * 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
17
+ * on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
18
+ * on iOS, which is why TestFlight builds never saw a shake prior to
19
+ * SDK 0.5.1.
20
+ * 2. Accelerometer-based fallback via `expo-sensors` (optional peer
21
+ * dep). When the host app has `expo-sensors` installed we subscribe
22
+ * to the accelerometer and fire on a burst of above-threshold peaks.
23
+ * This path works identically in Debug, Release, and TestFlight.
24
+ *
25
+ * Both paths share a 1-second debounce so a single shake never fires the
26
+ * callback twice.
27
+ */
28
+ class ShakeDetector {
29
+ constructor() {
30
+ this.devMenuSub = null;
31
+ this.accelSub = null;
32
+ this.lastShakeTime = 0;
33
+ this.peakTimestamps = [];
34
+ }
35
+ start(onShake) {
36
+ this.stop();
37
+ this.subscribeDevMenu(onShake);
38
+ this.subscribeAccelerometer(onShake);
39
+ }
40
+ stop() {
41
+ if (this.devMenuSub) {
42
+ this.devMenuSub.remove();
43
+ this.devMenuSub = null;
44
+ }
45
+ if (this.accelSub) {
46
+ this.accelSub.remove();
47
+ this.accelSub = null;
48
+ }
49
+ this.peakTimestamps = [];
50
+ }
51
+ fire(onShake) {
52
+ const now = Date.now();
53
+ if (now - this.lastShakeTime <= SHAKE_TIMEOUT_MS)
54
+ return;
55
+ this.lastShakeTime = now;
56
+ this.peakTimestamps = [];
57
+ onShake();
58
+ }
59
+ /**
60
+ * Dev-menu / platform-native event listener. On iOS this is the
61
+ * `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
62
+ * the `ShakeEvent` name a handful of third-party shake libraries emit.
63
+ */
64
+ subscribeDevMenu(onShake) {
65
+ const eventName = react_native_1.Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
66
+ this.devMenuSub = react_native_1.DeviceEventEmitter.addListener(eventName, () => {
67
+ this.fire(onShake);
68
+ });
69
+ }
70
+ /**
71
+ * Accelerometer-based detection. Uses `expo-sensors` when available —
72
+ * the host app's own dependency, not the SDK's, so apps that don't
73
+ * want the extra native surface get the Dev-menu path only.
74
+ */
75
+ subscribeAccelerometer(onShake) {
76
+ let Accelerometer = null;
77
+ try {
78
+ // Optional peer dep — if it isn't installed, we simply skip this path.
79
+ Accelerometer = require('expo-sensors').Accelerometer;
80
+ }
81
+ catch {
82
+ return;
83
+ }
84
+ if (!Accelerometer)
85
+ return;
86
+ try {
87
+ Accelerometer.setUpdateInterval(ACCEL_SAMPLE_INTERVAL_MS);
88
+ }
89
+ catch {
90
+ // Some platforms reject zero-value intervals; fall through with defaults
91
+ }
92
+ this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
93
+ // Magnitude of acceleration vector (in g). Subtract 1 so a stationary
94
+ // device reports ~0 rather than the 1g of gravity.
95
+ const mag = Math.sqrt(x * x + y * y + z * z);
96
+ if (mag - 1 < ACCEL_THRESHOLD_G - 1)
97
+ return;
98
+ const now = Date.now();
99
+ this.peakTimestamps.push(now);
100
+ // Drop peaks that fell out of the rolling window.
101
+ while (this.peakTimestamps.length > 0 &&
102
+ now - this.peakTimestamps[0] > ACCEL_WINDOW_MS) {
103
+ this.peakTimestamps.shift();
104
+ }
105
+ if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
106
+ this.fire(onShake);
107
+ }
108
+ });
109
+ }
110
+ }
111
+ exports.ShakeDetector = ShakeDetector;