yaver-feedback-react-native 0.8.13 → 0.9.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.
Files changed (76) hide show
  1. package/app.plugin.js +126 -9
  2. package/dist/BlackBox.js +9 -1
  3. package/dist/DeployPanel.js +65 -0
  4. package/dist/Discovery.js +13 -2
  5. package/dist/FeedbackModal.js +477 -66
  6. package/dist/MachinePickerScreen.js +14 -5
  7. package/dist/P2PClient.d.ts +94 -1
  8. package/dist/P2PClient.js +281 -2
  9. package/dist/ShakeDetector.js +2 -0
  10. package/dist/VibeChatScreen.d.ts +6 -1
  11. package/dist/VibeChatScreen.js +204 -1
  12. package/dist/YaverFeedback.d.ts +98 -0
  13. package/dist/YaverFeedback.js +509 -46
  14. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  15. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  16. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  17. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  18. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  19. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  20. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  21. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  22. package/dist/__tests__/P2PClient.test.js +2 -2
  23. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  24. package/dist/__tests__/ReportIdentity.test.js +168 -0
  25. package/dist/__tests__/SDKToken.test.js +1 -1
  26. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  27. package/dist/__tests__/ShakeToggle.test.js +121 -0
  28. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  29. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  30. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  31. package/dist/__tests__/reloadActions.test.js +129 -0
  32. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  33. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  34. package/dist/__tests__/types.test.js +5 -5
  35. package/dist/_core/device.d.ts +19 -9
  36. package/dist/_core/device.js +20 -14
  37. package/dist/capture.d.ts +6 -0
  38. package/dist/capture.js +53 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +11 -1
  41. package/dist/reloadActions.d.ts +88 -0
  42. package/dist/reloadActions.js +200 -0
  43. package/dist/storeShots.d.ts +67 -0
  44. package/dist/storeShots.js +137 -0
  45. package/dist/types.d.ts +215 -3
  46. package/dist/voice.d.ts +61 -0
  47. package/dist/voice.js +246 -0
  48. package/package.json +14 -3
  49. package/src/BlackBox.ts +10 -1
  50. package/src/DeployPanel.tsx +74 -0
  51. package/src/Discovery.ts +13 -2
  52. package/src/FeedbackModal.tsx +563 -87
  53. package/src/MachinePickerScreen.tsx +12 -3
  54. package/src/P2PClient.ts +314 -2
  55. package/src/ShakeDetector.ts +1 -0
  56. package/src/VibeChatScreen.tsx +219 -0
  57. package/src/YaverFeedback.ts +498 -46
  58. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  59. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  60. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  61. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  62. package/src/__tests__/P2PClient.test.ts +2 -2
  63. package/src/__tests__/ReportIdentity.test.ts +203 -0
  64. package/src/__tests__/SDKToken.test.ts +1 -1
  65. package/src/__tests__/ShakeToggle.test.ts +153 -0
  66. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  67. package/src/__tests__/reloadActions.test.ts +171 -0
  68. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  69. package/src/__tests__/types.test.ts +5 -5
  70. package/src/_core/device.ts +20 -14
  71. package/src/capture.ts +51 -0
  72. package/src/index.ts +21 -0
  73. package/src/reloadActions.ts +273 -0
  74. package/src/storeShots.ts +189 -0
  75. package/src/types.ts +217 -3
  76. package/src/voice.ts +270 -0
@@ -6,8 +6,15 @@ const Discovery_1 = require("./Discovery");
6
6
  const BlackBox_1 = require("./BlackBox");
7
7
  const P2PClient_1 = require("./P2PClient");
8
8
  const ShakeDetector_1 = require("./ShakeDetector");
9
+ const storeShots_1 = require("./storeShots");
9
10
  const auth_1 = require("./auth");
10
11
  const preferences_1 = require("./preferences");
12
+ function unrefTimer(timer) {
13
+ const maybeNodeTimer = timer;
14
+ if (typeof maybeNodeTimer.unref === 'function') {
15
+ maybeNodeTimer.unref();
16
+ }
17
+ }
11
18
  // Is this JS runtime the Yaver mobile app's super-host bridge? The
12
19
  // YaverInfo native module is only registered inside Yaver's container
13
20
  // (mobile/ios/Yaver/YaverInfo.{swift,m} + Android counterpart); a
@@ -134,9 +141,16 @@ let config = null;
134
141
  let enabled = false;
135
142
  let p2pClient = null;
136
143
  let shakeDetector = null;
144
+ /** Unsubscribe for the BlackBox command handler registered by init(). Held so
145
+ * a re-init (or destroy) can drop the old one instead of stacking. */
146
+ let commandUnsubscribe = null;
137
147
  let p2pAuthToken = null;
138
148
  let p2pRelayPassword = '';
149
+ /** Pending BlackBox auto-start retry. Held so destroy()/re-init can cancel it
150
+ * instead of leaving a timer chain running against a torn-down config. */
151
+ let autoStartTimer = null;
139
152
  let reportLaunchInFlight = false;
153
+ let crashReportInFlight = false;
140
154
  /** Resolve the user's relay password by validating their auth token
141
155
  * against Convex. Used whenever we (re)build the P2PClient so a
142
156
  * relay-routed agentUrl carries a valid X-Relay-Password — without
@@ -182,6 +196,7 @@ async function resolveRelayPassword(authToken, convexUrl) {
182
196
  /** Ring buffer of captured errors. */
183
197
  let errorBuffer = [];
184
198
  let maxErrors = 5;
199
+ let crashHandlerInstalled = false;
185
200
  /** Track whether BlackBox was running before disable (to restart on enable). */
186
201
  let blackBoxWasStreaming = false;
187
202
  /**
@@ -245,6 +260,118 @@ class YaverFeedback {
245
260
  * If no `agentUrl` is provided, the SDK will attempt auto-discovery
246
261
  * via `YaverDiscovery` on the first `startReport()` call.
247
262
  */
263
+ /** The runtime lane, when running on web inside a Yaver preview WebView. */
264
+ static detectWebLane() {
265
+ try {
266
+ if (react_native_1.Platform.OS !== 'web' || typeof window === 'undefined')
267
+ return null;
268
+ const l = String(window.__yaverLane || '').toLowerCase();
269
+ if (l === 'browser' || l === 'webrtc')
270
+ return l;
271
+ }
272
+ catch { /* noop */ }
273
+ return null;
274
+ }
275
+ /**
276
+ * Mount a draggable DOM floating "Y" icon for the BROWSER lane (RN-web). It
277
+ * lives in the WebView's document.body (position:fixed, max z-index), so it
278
+ * renders ON TOP of the page inside the WebView and is never occluded by the
279
+ * fullScreen preview modal — the one occlusion-proof affordance on iOS.
280
+ * Tap opens the SDK's existing report flow (the RN FeedbackModal renders in
281
+ * the same WebView DOM, also un-occluded). No-op off-web or off-lane, and
282
+ * idempotent (a re-init reuses the existing node).
283
+ */
284
+ static mountBrowserLaneIcon() {
285
+ if (YaverFeedback.detectWebLane() !== 'browser')
286
+ return;
287
+ try {
288
+ const doc = globalThis.document;
289
+ if (!doc || !doc.body)
290
+ return;
291
+ if (doc.getElementById('yaver-feedback-btn'))
292
+ return; // idempotent
293
+ const btn = doc.createElement('div');
294
+ btn.id = 'yaver-feedback-btn';
295
+ btn.textContent = 'Y';
296
+ btn.title = 'Yaver Feedback — drag to move · tap to report';
297
+ btn.style.cssText =
298
+ 'position:fixed;bottom:20px;right:20px;width:44px;height:44px;border-radius:50%;' +
299
+ 'background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;' +
300
+ 'cursor:pointer;z-index:99999;font-size:18px;font-weight:bold;touch-action:none;' +
301
+ 'box-shadow:0 4px 12px rgba(99,102,241,0.4);transition:transform 0.2s;';
302
+ const drag = { on: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
303
+ const w = window;
304
+ const applyPos = (x, y) => {
305
+ const m = 8;
306
+ const nx = Math.max(m, Math.min(w.innerWidth - 44 - m, x));
307
+ const ny = Math.max(m, Math.min(w.innerHeight - 44 - m, y));
308
+ btn.style.left = nx + 'px';
309
+ btn.style.top = ny + 'px';
310
+ btn.style.right = 'auto';
311
+ btn.style.bottom = 'auto';
312
+ try {
313
+ localStorage.setItem('yaver-feedback-btn-pos', JSON.stringify({ x: nx, y: ny }));
314
+ }
315
+ catch { /* noop */ }
316
+ };
317
+ try {
318
+ const s = localStorage.getItem('yaver-feedback-btn-pos');
319
+ if (s) {
320
+ const p = JSON.parse(s);
321
+ if (typeof p?.x === 'number')
322
+ applyPos(p.x, p.y);
323
+ }
324
+ }
325
+ catch { /* noop */ }
326
+ btn.addEventListener('pointerdown', (e) => {
327
+ drag.on = true;
328
+ drag.moved = false;
329
+ const r = btn.getBoundingClientRect();
330
+ drag.sx = e.clientX;
331
+ drag.sy = e.clientY;
332
+ drag.ox = r.left;
333
+ drag.oy = r.top;
334
+ btn.setPointerCapture(e.pointerId);
335
+ btn.style.transition = 'none';
336
+ });
337
+ btn.addEventListener('pointermove', (e) => {
338
+ if (!drag.on)
339
+ return;
340
+ const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
341
+ if (Math.abs(dx) + Math.abs(dy) > 5)
342
+ drag.moved = true;
343
+ if (drag.moved)
344
+ applyPos(drag.ox + dx, drag.oy + dy);
345
+ });
346
+ const end = (e) => {
347
+ if (!drag.on)
348
+ return;
349
+ drag.on = false;
350
+ btn.style.transition = 'transform 0.2s';
351
+ try {
352
+ btn.releasePointerCapture(e.pointerId);
353
+ }
354
+ catch { /* noop */ }
355
+ if (!drag.moved) {
356
+ try {
357
+ void YaverFeedback.startReport();
358
+ }
359
+ catch { /* noop */ }
360
+ }
361
+ };
362
+ btn.addEventListener('pointerup', end);
363
+ btn.addEventListener('pointercancel', end);
364
+ // Also answer the host's forwarded shake (Yaver injects yaver-feedback:launch).
365
+ const launch = () => { try {
366
+ void YaverFeedback.startReport();
367
+ }
368
+ catch { /* noop */ } };
369
+ window.addEventListener('yaver-feedback:launch', launch);
370
+ window.__yaverFeedbackLaunch = launch;
371
+ doc.body.appendChild(btn);
372
+ }
373
+ catch { /* noop — never break the guest app over a feedback affordance */ }
374
+ }
248
375
  static init(cfg) {
249
376
  config = {
250
377
  trigger: 'shake',
@@ -272,6 +399,9 @@ class YaverFeedback {
272
399
  }
273
400
  maxErrors = cfg.maxCapturedErrors ?? 5;
274
401
  errorBuffer = [];
402
+ if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
403
+ YaverFeedback.installCrashHandler();
404
+ }
275
405
  return;
276
406
  }
277
407
  if (config.disableShakeGesture && (!config.quickIcon || config.quickIcon === 'auto')) {
@@ -335,23 +465,23 @@ class YaverFeedback {
335
465
  // Set up error capture buffer size
336
466
  maxErrors = cfg.maxCapturedErrors ?? 5;
337
467
  errorBuffer = [];
338
- // Wire up shake detection when trigger is 'shake'
339
- if (shakeDetector) {
340
- shakeDetector.stop();
341
- shakeDetector = null;
342
- }
343
- if (enabled && config.trigger === 'shake' && !config.disableShakeGesture) {
344
- shakeDetector = new ShakeDetector_1.ShakeDetector();
345
- shakeDetector.start(() => {
346
- YaverFeedback.notifyShake();
347
- if (config?.reportingOnly) {
348
- YaverFeedback.sendAutoReport();
349
- }
350
- else {
351
- YaverFeedback.startReport();
352
- }
353
- });
468
+ if (cfg.crashReporting?.enabled && cfg.crashReporting?.installGlobalHandler) {
469
+ YaverFeedback.installCrashHandler();
354
470
  }
471
+ // Wire up shake detection when trigger is 'shake'. Rebuild from scratch:
472
+ // config was just replaced, so the old detector may be closing over stale
473
+ // settings.
474
+ YaverFeedback.stopShakeDetector();
475
+ YaverFeedback.syncShakeDetector();
476
+ // Browser lane (RN-web inside Yaver's fullScreen WebView preview): the
477
+ // native shake module doesn't exist on web, and Yaver's own container
478
+ // overlay is OCCLUDED behind the WebView modal on iOS. So mount an
479
+ // occlusion-proof DRAGGABLE DOM icon INSIDE the WebView — same pattern as
480
+ // yaver-feedback-web. Lane is stamped by Yaver via
481
+ // window.__yaverLane='browser' (mobile PREVIEW_LANE_SCRIPT). See
482
+ // docs/audits/feedback-sdk-lanes-audit-2026-07-28.md.
483
+ if (enabled)
484
+ YaverFeedback.mountBrowserLaneIcon();
355
485
  // Wire up BlackBox command handlers for reload + status signals from
356
486
  // the agent. Opens an SSE channel the agent uses to:
357
487
  // - broadcast reload_bundle so the phone picks up a fresh Hermes
@@ -360,7 +490,14 @@ class YaverFeedback {
360
490
  // assets…", "Done") so the SDK can surface it like a normal
361
491
  // "Working…" spinner instead of a silent 60-second freeze.
362
492
  if (enabled) {
363
- BlackBox_1.BlackBox.onCommand((cmd) => {
493
+ // onCommand appends to a static array and hands back an unsubscribe.
494
+ // Dropping that unsubscribe meant every re-init stacked another handler
495
+ // on top of the last, so an app that re-initialised (say, a settings
496
+ // toggle) reloaded twice per agent command, then three times, and so on.
497
+ // destroy() never cleared them either. Drop the previous registration
498
+ // first so init() is idempotent here.
499
+ commandUnsubscribe?.();
500
+ commandUnsubscribe = BlackBox_1.BlackBox.onCommand((cmd) => {
364
501
  if (cmd.command === 'reload') {
365
502
  if (cfg.onReload) {
366
503
  cfg.onReload();
@@ -423,17 +560,24 @@ class YaverFeedback {
423
560
  // SFMG used to call BlackBox.start() inside YaverFeedbackWidget
424
561
  // after auth — that path still works (start() is idempotent), so
425
562
  // upgrading SDK without removing the manual call is safe.
563
+ // 4. RETRY, rather than checking once. This was a single 500ms
564
+ // timeout, and on a cold start it could not succeed: an app that
565
+ // passes neither agentUrl nor authToken to init() (the normal
566
+ // case — they're restored from storage) gets its session from
567
+ // `void hydrateSession()`, which reads two AsyncStorage keys and
568
+ // then awaits discoverAgent(), a Convex round trip plus LAN
569
+ // probing. That does not finish in 500ms, so the check found no
570
+ // agentUrl, gave up, and BlackBox never started — meaning no SSE
571
+ // channel, so the agent's `reload_bundle` command had nowhere to
572
+ // land and Hermes hot reload silently did nothing until the user
573
+ // shook the device and drove discovery by hand.
574
+ //
575
+ // The both-conditions guard is what keeps the 401 retry storm
576
+ // from coming back, so it is preserved exactly; we only re-check
577
+ // it over time instead of once. Bounded so a device that never
578
+ // signs in doesn't poll forever.
426
579
  if (cfg.autoStartBlackBox !== false) {
427
- setTimeout(() => {
428
- if (config?.agentUrl && (config?.authToken || p2pAuthToken)) {
429
- try {
430
- BlackBox_1.BlackBox.start();
431
- }
432
- catch (err) {
433
- console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
434
- }
435
- }
436
- }, 500);
580
+ YaverFeedback.scheduleBlackBoxAutoStart();
437
581
  }
438
582
  }
439
583
  // NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
@@ -446,6 +590,41 @@ class YaverFeedback {
446
590
  // - YaverFeedback.wrapErrorHandler(existingHandler) to create a
447
591
  // pass-through wrapper they insert into their own error chain
448
592
  }
593
+ /**
594
+ * Opt-in global crash handler. This wraps the existing React Native
595
+ * ErrorUtils handler and still calls it, so Sentry/Crashlytics/Bugsnag can
596
+ * remain the system of record. The SDK only uploads a crash report and,
597
+ * when configured, asks the agent to create a fix task.
598
+ */
599
+ static installCrashHandler() {
600
+ if (crashHandlerInstalled)
601
+ return;
602
+ try {
603
+ const errorUtils = globalThis?.ErrorUtils;
604
+ if (!errorUtils?.getGlobalHandler || !errorUtils?.setGlobalHandler) {
605
+ return;
606
+ }
607
+ const next = errorUtils.getGlobalHandler();
608
+ errorUtils.setGlobalHandler((error, isFatal) => {
609
+ YaverFeedback.attachError(error, {
610
+ source: 'global-handler',
611
+ crashAware: true,
612
+ });
613
+ if (errorBuffer.length > 0) {
614
+ errorBuffer[errorBuffer.length - 1].isFatal = isFatal ?? true;
615
+ }
616
+ void YaverFeedback.reportCrash(error, {
617
+ isFatal: isFatal ?? true,
618
+ source: 'global-handler',
619
+ });
620
+ next?.(error, isFatal);
621
+ });
622
+ crashHandlerInstalled = true;
623
+ }
624
+ catch (err) {
625
+ console.warn('[YaverFeedback] installCrashHandler failed:', err);
626
+ }
627
+ }
449
628
  /**
450
629
  * Run agent discovery in the background.
451
630
  * Called automatically from init() when no agentUrl is provided.
@@ -547,11 +726,64 @@ class YaverFeedback {
547
726
  else {
548
727
  await YaverFeedback.discoverAgent();
549
728
  }
729
+ // Signing in is the event the auto-start was waiting for. If its bounded
730
+ // retry already gave up (device left signed out past the window), this is
731
+ // what brings the command channel up without requiring an app restart.
732
+ if (config.autoStartBlackBox !== false && !BlackBox_1.BlackBox.isStreaming) {
733
+ YaverFeedback.scheduleBlackBoxAutoStart();
734
+ }
550
735
  }
551
736
  /** Returns true once the SDK has a session token it can use. */
552
737
  static isAuthed() {
553
738
  return Boolean(config?.authToken);
554
739
  }
740
+ /**
741
+ * On-device App Store screenshot capture (Engine 2). Walks the routes
742
+ * the host hands us, screenshots each, and uploads to the agent which
743
+ * runs the App Store Connect backend. agentUrl / authToken / relay
744
+ * password default to the SDK's resolved session; the host only has to
745
+ * supply a navigation ref + the ordered route list.
746
+ *
747
+ * YaverFeedback.captureStoreScreenshots({
748
+ * app: 'sfmg',
749
+ * navigationRef,
750
+ * routes: ['/(tabs)/dashboard', '/(tabs)/messages', '/(tabs)/clients'],
751
+ * submit: true,
752
+ * })
753
+ */
754
+ static async captureStoreScreenshots(opts) {
755
+ const agentUrl = opts.agentUrl ?? config?.agentUrl ?? '';
756
+ const authToken = opts.authToken ?? config?.authToken ?? p2pAuthToken ?? '';
757
+ const relayPassword = opts.relayPassword ?? p2pRelayPassword;
758
+ if (!agentUrl || !authToken) {
759
+ return {
760
+ ok: false,
761
+ captured: 0,
762
+ uploaded: 0,
763
+ message: 'YaverFeedback not connected to an agent — init() with agentUrl + authToken first.',
764
+ };
765
+ }
766
+ return (0, storeShots_1.captureStoreScreenshots)({ ...opts, agentUrl, authToken, relayPassword });
767
+ }
768
+ /**
769
+ * Let the mobile app / CLI kick THIS device into self-capturing. Registers
770
+ * a BlackBox command listener that fires captureStoreScreenshots when the
771
+ * agent pushes a `capture_store_shots` command (its `data` may override
772
+ * `submit` / `locale`). Returns an unsubscribe fn. Call once after init,
773
+ * passing the app's navigation ref + the routes to walk.
774
+ */
775
+ static enableStoreShotsOnCommand(opts) {
776
+ return BlackBox_1.BlackBox.onCommand((cmd) => {
777
+ if (cmd?.command !== 'capture_store_shots')
778
+ return;
779
+ const data = cmd.data ?? {};
780
+ void YaverFeedback.captureStoreScreenshots({
781
+ ...opts,
782
+ submit: typeof data.submit === 'boolean' ? data.submit : opts.submit,
783
+ locale: typeof data.locale === 'string' ? data.locale : opts.locale,
784
+ });
785
+ });
786
+ }
555
787
  /**
556
788
  * Request the embedded FeedbackModal to show the login screen. Works by
557
789
  * emitting an event the modal listens for — avoids forcing the host app
@@ -747,32 +979,138 @@ class YaverFeedback {
747
979
  }
748
980
  BlackBox_1.BlackBox.unwrapConsole(); // ensure console is restored even if BlackBox wasn't started
749
981
  errorBuffer = [];
750
- if (shakeDetector) {
751
- shakeDetector.stop();
752
- shakeDetector = null;
753
- }
982
+ YaverFeedback.stopShakeDetector();
754
983
  }
755
984
  else {
756
985
  // === ENABLE ===
757
986
  if (blackBoxWasStreaming) {
758
987
  BlackBox_1.BlackBox.start(); // restart with previous config
759
988
  }
760
- // Restart shake detector if trigger is 'shake'
761
- if (config?.trigger === 'shake' && !config?.disableShakeGesture && !shakeDetector) {
762
- shakeDetector = new ShakeDetector_1.ShakeDetector();
763
- shakeDetector.start(() => {
764
- YaverFeedback.notifyShake();
765
- if (config?.reportingOnly) {
766
- YaverFeedback.sendAutoReport();
767
- }
768
- else {
769
- YaverFeedback.startReport();
770
- }
771
- });
772
- }
989
+ enabled = true; // syncShakeDetector reads this
990
+ YaverFeedback.syncShakeDetector();
773
991
  }
774
992
  enabled = value;
775
993
  }
994
+ /**
995
+ * Turn shake-to-report on or off without tearing the SDK down.
996
+ *
997
+ * `trigger` is read only inside init(), so before this the only ways to
998
+ * stop listening for a shake were setEnabled(false) — which also kills the
999
+ * flight recorder and the agent command channel — or a re-init, which used
1000
+ * to stack a duplicate command handler. Neither is what "turn the shake
1001
+ * catcher off" should cost.
1002
+ *
1003
+ * Persists onto config.disableShakeGesture, so a later setEnabled(true)
1004
+ * honours it rather than resurrecting the listener.
1005
+ */
1006
+ static setShakeEnabled(value) {
1007
+ if (!config)
1008
+ return;
1009
+ config.disableShakeGesture = !value;
1010
+ YaverFeedback.syncShakeDetector();
1011
+ }
1012
+ /** Whether the shake listener is currently armed. */
1013
+ static isShakeEnabled() {
1014
+ return shakeDetector !== null;
1015
+ }
1016
+ /**
1017
+ * Bring the shake listener in line with the current config. Idempotent —
1018
+ * safe to call whenever `enabled`, `trigger`, or `disableShakeGesture`
1019
+ * moves.
1020
+ */
1021
+ static syncShakeDetector() {
1022
+ const want = enabled && config?.trigger === 'shake' && !config?.disableShakeGesture;
1023
+ if (want && !shakeDetector) {
1024
+ shakeDetector = new ShakeDetector_1.ShakeDetector();
1025
+ shakeDetector.start(() => {
1026
+ YaverFeedback.notifyShake();
1027
+ if (config?.reportingOnly) {
1028
+ YaverFeedback.sendAutoReport();
1029
+ }
1030
+ else {
1031
+ YaverFeedback.startReport();
1032
+ }
1033
+ });
1034
+ }
1035
+ else if (!want && shakeDetector) {
1036
+ YaverFeedback.stopShakeDetector();
1037
+ }
1038
+ }
1039
+ static stopShakeDetector() {
1040
+ if (shakeDetector) {
1041
+ shakeDetector.stop();
1042
+ shakeDetector = null;
1043
+ }
1044
+ }
1045
+ /** Cancel a pending BlackBox auto-start retry chain. */
1046
+ static cancelBlackBoxAutoStart() {
1047
+ if (autoStartTimer) {
1048
+ clearTimeout(autoStartTimer);
1049
+ autoStartTimer = null;
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Start BlackBox as soon as we have BOTH an agentUrl and a token, polling
1054
+ * until then.
1055
+ *
1056
+ * Both conditions are mandatory and deliberate: starting without a token
1057
+ * makes the SSE channel 401 and retry with backoff, which is the tight
1058
+ * string-concat + JSON-parse loop that used to SIGSEGV Hermes on iOS 18.3.1
1059
+ * during Screenshot & Fix. This only re-checks that same guard over time.
1060
+ *
1061
+ * Bounded at ~60s: long enough for AsyncStorage hydration plus a Convex
1062
+ * discovery round trip on a cold, off-LAN start, short enough that a device
1063
+ * whose user never signs in stops polling. Giving up here costs nothing —
1064
+ * setAuthToken() and startReport() both reschedule, so signing in later
1065
+ * still brings the channel up.
1066
+ */
1067
+ static scheduleBlackBoxAutoStart(attempt = 0) {
1068
+ const MAX_ATTEMPTS = 60;
1069
+ const FIRST_DELAY_MS = 500;
1070
+ const RETRY_DELAY_MS = 1000;
1071
+ // Re-run discovery every Nth tick rather than every tick: it is a Convex
1072
+ // round trip plus LAN probing, so once a second would hammer both. Every
1073
+ // 3s is responsive enough that waking the dev machine reconnects the phone
1074
+ // on its own, without the user wondering whether to restart the app.
1075
+ const REDISCOVER_EVERY = 3;
1076
+ YaverFeedback.cancelBlackBoxAutoStart();
1077
+ if (!enabled || attempt >= MAX_ATTEMPTS)
1078
+ return;
1079
+ const timer = setTimeout(() => {
1080
+ autoStartTimer = null;
1081
+ // A destroy() or setEnabled(false) between scheduling and firing.
1082
+ if (!enabled || !config)
1083
+ return;
1084
+ if (BlackBox_1.BlackBox.isStreaming)
1085
+ return;
1086
+ if (config.agentUrl && (config.authToken || p2pAuthToken)) {
1087
+ try {
1088
+ // Pass the host's BlackBox config through. This used to call
1089
+ // start() bare, so a host that configured the flight recorder
1090
+ // — as the README itself shows — had it silently replaced by
1091
+ // defaults, and appName came out ''.
1092
+ BlackBox_1.BlackBox.start(config.blackBox);
1093
+ }
1094
+ catch (err) {
1095
+ console.warn('[YaverFeedback] BlackBox auto-start failed:', err);
1096
+ }
1097
+ return;
1098
+ }
1099
+ // No agent yet. Re-attempt discovery instead of only re-reading the
1100
+ // flag: hydrateSession() runs discoverAgent() exactly once at init, so
1101
+ // if the dev machine was unreachable at that moment — asleep, phone
1102
+ // still on cellular before the relay came up, agent not yet serving —
1103
+ // agentUrl stayed null for the whole process lifetime and only an app
1104
+ // restart could recover it. discoverAgent() self-guards on "already
1105
+ // have a URL" and "no token", so calling it again is cheap and safe.
1106
+ if (!config.agentUrl && attempt > 0 && attempt % REDISCOVER_EVERY === 0) {
1107
+ void YaverFeedback.discoverAgent();
1108
+ }
1109
+ YaverFeedback.scheduleBlackBoxAutoStart(attempt + 1);
1110
+ }, attempt === 0 ? FIRST_DELAY_MS : RETRY_DELAY_MS);
1111
+ autoStartTimer = timer;
1112
+ unrefTimer(timer);
1113
+ }
776
1114
  /** Returns whether the SDK is currently enabled. */
777
1115
  static isEnabled() {
778
1116
  return enabled;
@@ -843,6 +1181,104 @@ class YaverFeedback {
843
1181
  next?.(error, isFatal);
844
1182
  };
845
1183
  }
1184
+ /**
1185
+ * Upload a crash-aware feedback bundle. Apps can call this from their own
1186
+ * error boundary/global handler. If crashReporting.autoFix is true, the SDK
1187
+ * also triggers the agent's feedback-fix task; reload delivery still happens
1188
+ * through the existing BlackBox command stream.
1189
+ */
1190
+ static async reportCrash(error, opts) {
1191
+ if (!config || !enabled || !config.crashReporting?.enabled)
1192
+ return {};
1193
+ if (crashReportInFlight)
1194
+ return {};
1195
+ crashReportInFlight = true;
1196
+ try {
1197
+ YaverFeedback.attachError(error, {
1198
+ ...(config.crashReporting.metadata || {}),
1199
+ ...(opts?.metadata || {}),
1200
+ crashAware: true,
1201
+ });
1202
+ if (errorBuffer.length > 0) {
1203
+ errorBuffer[errorBuffer.length - 1].isFatal = opts?.isFatal ?? true;
1204
+ }
1205
+ if (!config.agentUrl) {
1206
+ await YaverFeedback.discoverAgent();
1207
+ }
1208
+ if (!config.agentUrl) {
1209
+ console.warn('[YaverFeedback] No agent URL — cannot send crash report.');
1210
+ return {};
1211
+ }
1212
+ const { Platform, Dimensions } = require('react-native');
1213
+ const { uploadFeedback } = require('./upload');
1214
+ const { width, height } = Dimensions.get('window');
1215
+ let screenshotPath;
1216
+ if (config.crashReporting.captureScreenshot !== false) {
1217
+ try {
1218
+ const { captureScreenshot } = require('./capture');
1219
+ screenshotPath = await captureScreenshot();
1220
+ }
1221
+ catch { }
1222
+ }
1223
+ const autoFix = opts?.autoFix ?? config.crashReporting.autoFix === true;
1224
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
1225
+ projectName: config?.projectName,
1226
+ bundleId: config?.bundleId,
1227
+ surface: config?.surface,
1228
+ surfaces: config?.surfaces,
1229
+ stack: config?.stack,
1230
+ stacks: config?.stacks,
1231
+ testSurfaces: config?.testSurfaces,
1232
+ feedbackSdk: config?.feedbackSdk,
1233
+ feedbackTransport: config?.feedbackTransport,
1234
+ voiceCapabilities: config?.voiceCapabilities,
1235
+ sttProvider: config?.sttProvider,
1236
+ ttsProvider: config?.ttsProvider,
1237
+ });
1238
+ const bundle = {
1239
+ metadata: {
1240
+ timestamp: new Date().toISOString(),
1241
+ reportKind: 'crash',
1242
+ deviceInfo: {
1243
+ platform: Platform.OS,
1244
+ osVersion: String(Platform.Version),
1245
+ model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
1246
+ screenWidth: width,
1247
+ screenHeight: height,
1248
+ appName: identity.appName,
1249
+ },
1250
+ app: identity.app,
1251
+ project: identity.project,
1252
+ userNote: '[Crash report]',
1253
+ crash: {
1254
+ message: error.message,
1255
+ isFatal: opts?.isFatal ?? true,
1256
+ source: opts?.source ?? 'manual',
1257
+ autoFixRequested: autoFix,
1258
+ },
1259
+ },
1260
+ screenshots: screenshotPath ? [screenshotPath] : [],
1261
+ errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
1262
+ };
1263
+ const uploaded = await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle, p2pRelayPassword);
1264
+ const reportId = uploaded?.id ??
1265
+ uploaded?.reportId;
1266
+ let taskId;
1267
+ if (autoFix && reportId) {
1268
+ const client = p2pClient ?? new P2PClient_1.P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
1269
+ const fix = await client.triggerFix(reportId);
1270
+ taskId = fix?.taskId;
1271
+ }
1272
+ return { reportId, taskId };
1273
+ }
1274
+ catch (err) {
1275
+ console.warn('[YaverFeedback] Crash report failed:', err);
1276
+ return {};
1277
+ }
1278
+ finally {
1279
+ crashReportInFlight = false;
1280
+ }
1281
+ }
846
1282
  /**
847
1283
  * Returns the P2P client instance.
848
1284
  * Available after init if agentUrl is set, or after first successful discovery.
@@ -981,17 +1417,33 @@ class YaverFeedback {
981
1417
  catch {
982
1418
  // Screenshot capture may fail (e.g. no view ref) — continue without it
983
1419
  }
1420
+ const identity = (0, P2PClient_1.resolveReportIdentity)({
1421
+ projectName: config?.projectName,
1422
+ bundleId: config?.bundleId,
1423
+ surface: config?.surface,
1424
+ surfaces: config?.surfaces,
1425
+ stack: config?.stack,
1426
+ stacks: config?.stacks,
1427
+ testSurfaces: config?.testSurfaces,
1428
+ feedbackSdk: config?.feedbackSdk,
1429
+ feedbackTransport: config?.feedbackTransport,
1430
+ voiceCapabilities: config?.voiceCapabilities,
1431
+ sttProvider: config?.sttProvider,
1432
+ ttsProvider: config?.ttsProvider,
1433
+ });
984
1434
  const bundle = {
985
1435
  metadata: {
986
1436
  timestamp: new Date().toISOString(),
987
- device: {
1437
+ deviceInfo: {
988
1438
  platform: Platform.OS,
989
1439
  osVersion: String(Platform.Version),
990
1440
  model: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
991
1441
  screenWidth: width,
992
1442
  screenHeight: height,
1443
+ appName: identity.appName,
993
1444
  },
994
- app: {},
1445
+ app: identity.app,
1446
+ project: identity.project,
995
1447
  userNote: '[Auto-report via shake]',
996
1448
  },
997
1449
  screenshots: screenshotPath ? [screenshotPath] : [],
@@ -1143,6 +1595,17 @@ class YaverFeedback {
1143
1595
  shakeDetector.stop();
1144
1596
  shakeDetector = null;
1145
1597
  }
1598
+ // Drop the agent command handler and un-monkey-patch console. Without
1599
+ // these, destroy() left the SDK half-alive: console stayed wrapped, and
1600
+ // the reload handler kept firing on a "destroyed" SDK — then a later
1601
+ // init() stacked a second one on top.
1602
+ commandUnsubscribe?.();
1603
+ commandUnsubscribe = null;
1604
+ // Before `enabled = false` / `config = null` below, so an in-flight retry
1605
+ // can't fire against a torn-down config.
1606
+ YaverFeedback.cancelBlackBoxAutoStart();
1607
+ BlackBox_1.BlackBox.stop();
1608
+ BlackBox_1.BlackBox.unwrapConsole();
1146
1609
  firstShakeFired = false;
1147
1610
  enabled = false;
1148
1611
  config = null;
@@ -0,0 +1 @@
1
+ export {};