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
package/app.plugin.js CHANGED
@@ -387,14 +387,135 @@ function withYaverAndroidHotReload(config) {
387
387
  },
388
388
  ]);
389
389
 
390
- // Patch MainApplication to register the package and use hot bundle
390
+ // Patch MainApplication to register the package and use hot bundle.
391
+ //
392
+ // MainApplication is Kotlin on React Native 0.73+ (every current Expo
393
+ // template) and Java before that. The two need genuinely different code —
394
+ // `new Foo()`, `final`, and `@Override` are all syntax errors in Kotlin —
395
+ // so branch on the language Expo reports rather than assuming.
391
396
  config = withMainApplication(config, (config) => {
392
- let contents = config.modResults.contents;
393
-
394
- if (contents.includes("YaverHotReload")) {
397
+ if (config.modResults.contents.includes("YaverHotReload")) {
395
398
  return config;
396
399
  }
400
+ config.modResults.contents =
401
+ config.modResults.language === "kt"
402
+ ? patchMainApplicationKotlin(config.modResults.contents)
403
+ : patchMainApplicationJava(config.modResults.contents);
404
+ return config;
405
+ });
406
+
407
+ return config;
408
+ }
409
+
410
+ /**
411
+ * Insert `snippet` immediately before the closing brace of the method whose
412
+ * signature contains `anchor`, by matching braces from the method's opening
413
+ * one.
414
+ *
415
+ * The boot guard has to run at the END of onCreate: it touches
416
+ * reactNativeHost, and reaching that before super.onCreate() and
417
+ * SoLoader.init() would initialise React before its native libraries are
418
+ * loaded. Returns the contents unchanged if the anchor isn't found — a
419
+ * missing safety net is survivable, a corrupted MainApplication is not.
420
+ */
421
+ function insertAtEndOfMethod(contents, anchor, snippet) {
422
+ const anchorIdx = contents.indexOf(anchor);
423
+ if (anchorIdx === -1) return contents;
424
+ const open = contents.indexOf("{", anchorIdx);
425
+ if (open === -1) return contents;
426
+
427
+ let depth = 0;
428
+ for (let i = open; i < contents.length; i++) {
429
+ const ch = contents[i];
430
+ if (ch === "{") depth++;
431
+ else if (ch === "}") {
432
+ depth--;
433
+ if (depth === 0) {
434
+ return contents.slice(0, i) + snippet + contents.slice(i);
435
+ }
436
+ }
437
+ }
438
+ return contents;
439
+ }
397
440
 
441
+ /** Kotlin MainApplication (React Native 0.73+). */
442
+ function patchMainApplicationKotlin(contents) {
443
+ // Imports. Kotlin takes no semicolons.
444
+ contents = contents.replace(
445
+ "import com.facebook.react.ReactApplication",
446
+ "import com.facebook.react.ReactApplication\nimport io.yaver.feedback.YaverHotReloadModule\nimport io.yaver.feedback.YaverHotReloadPackage"
447
+ );
448
+
449
+ // Register the package. Anchor on `return packages` rather than on
450
+ // `packages.add(` — the template's only occurrence of that is inside a
451
+ // commented-out example line.
452
+ contents = contents.replace(
453
+ /(\n([ \t]*)return packages\n)/,
454
+ "\n$2packages.add(YaverHotReloadPackage())\n$1"
455
+ );
456
+
457
+ // Load a hot-pushed bundle when one is present. Inserted before
458
+ // getJSMainModuleName, which every Expo template defines.
459
+ if (!contents.includes("getJSBundleFile")) {
460
+ contents = contents.replace(
461
+ /(\n([ \t]*)override fun getJSMainModuleName\(\))/,
462
+ `
463
+ $2override fun getJSBundleFile(): String? {
464
+ $2 // Yaver Feedback SDK: load hot-reloaded bundle if available
465
+ $2 val hotBundle = YaverHotReloadModule.getSavedBundleFile(application.applicationContext)
466
+ $2 return hotBundle?.absolutePath ?: super.getJSBundleFile()
467
+ $2}
468
+ $1`
469
+ );
470
+ }
471
+
472
+ // Crash-revert safety net: clear the boot-attempt counter once the React
473
+ // context initialises (bundle loaded successfully), AND via a 10-s fallback
474
+ // in case that listener never fires (e.g. an infinite loop in the root
475
+ // component). If neither fires, YaverHotReloadModule.getSavedBundleFile()
476
+ // reverts to the APK-bundled bundle after 3 failed cold starts. Parity with
477
+ // YaverHotReload.swift on iOS.
478
+ if (!contents.includes("yaverHotReloadBootListener")) {
479
+ contents = insertAtEndOfMethod(
480
+ contents,
481
+ "override fun onCreate()",
482
+ `
483
+ // Yaver Feedback SDK hot-reload crash-revert safety net
484
+ val yaverHotReloadCtx: android.content.Context = applicationContext
485
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(
486
+ { YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx) },
487
+ 10000
488
+ )
489
+ try {
490
+ // Anonymous object, not a lambda: SAM conversion needs a Java interface
491
+ // or a Kotlin \`fun interface\`, and as of RN 0.81
492
+ // com.facebook.react.ReactInstanceEventListener is a plain Kotlin
493
+ // interface (ReactInstanceEventListener.kt). A lambda there fails
494
+ // :app:compileReleaseKotlin with "Argument type mismatch: actual type is
495
+ // 'Function0<Unit>'" — i.e. the host app cannot build a release AAB at
496
+ // all. The Java path below already did this correctly.
497
+ reactNativeHost.reactInstanceManager.addReactInstanceEventListener(
498
+ object : com.facebook.react.ReactInstanceEventListener {
499
+ override fun onReactContextInitialized(
500
+ context: com.facebook.react.bridge.ReactContext
501
+ ) {
502
+ YaverHotReloadModule.markBootSuccessful(yaverHotReloadCtx)
503
+ }
504
+ }
505
+ )
506
+ } catch (yaverHotReloadBootListener: Throwable) {
507
+ // Bridgeless / New Architecture does not expose reactInstanceManager;
508
+ // the 10-s fallback above still covers us.
509
+ }
510
+ `
511
+ );
512
+ }
513
+
514
+ return contents;
515
+ }
516
+
517
+ /** Java MainApplication (React Native < 0.73). */
518
+ function patchMainApplicationJava(contents) {
398
519
  // Add import
399
520
  contents = contents.replace(
400
521
  "import com.facebook.react.ReactApplication",
@@ -468,11 +589,7 @@ function withYaverAndroidHotReload(config) {
468
589
  contents.slice(0, insertionPoint) + bootGuard + contents.slice(insertionPoint);
469
590
  }
470
591
 
471
- config.modResults.contents = contents;
472
- return config;
473
- });
474
-
475
- return config;
592
+ return contents;
476
593
  }
477
594
 
478
595
  function withYaverFeedback(config, props) {
package/dist/BlackBox.js CHANGED
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BlackBox = void 0;
4
4
  const react_native_1 = require("react-native");
5
5
  const YaverFeedback_1 = require("./YaverFeedback");
6
+ function unrefTimer(timer) {
7
+ const maybeNodeTimer = timer;
8
+ if (typeof maybeNodeTimer.unref === 'function') {
9
+ maybeNodeTimer.unref();
10
+ }
11
+ }
6
12
  class BlackBox {
7
13
  /**
8
14
  * Start the black box stream. Call after `YaverFeedback.init()`.
@@ -18,7 +24,7 @@ class BlackBox {
18
24
  }
19
25
  BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
20
26
  BlackBox.authToken = feedbackConfig.authToken ?? null;
21
- BlackBox.relayPassword = feedbackConfig.relayPassword ?? '';
27
+ BlackBox.relayPassword = YaverFeedback_1.YaverFeedback.getRelayPassword();
22
28
  BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
23
29
  BlackBox.appName = config?.appName ?? '';
24
30
  BlackBox.flushInterval = config?.flushInterval ?? 2000;
@@ -29,6 +35,7 @@ class BlackBox {
29
35
  if (BlackBox.flushTimer)
30
36
  clearInterval(BlackBox.flushTimer);
31
37
  BlackBox.flushTimer = setInterval(() => BlackBox.flush(), BlackBox.flushInterval);
38
+ unrefTimer(BlackBox.flushTimer);
32
39
  // Log the session start
33
40
  BlackBox.push({
34
41
  type: 'lifecycle',
@@ -343,6 +350,7 @@ class BlackBox {
343
350
  if (BlackBox.started)
344
351
  BlackBox.connectSSE();
345
352
  }, 5000);
353
+ unrefTimer(BlackBox.sseReconnectTimer);
346
354
  }
347
355
  // ─── Internal ────────────────────────────────────────────────────
348
356
  static push(event) {
@@ -151,6 +151,45 @@ const DeployPanel = ({ onClose }) => {
151
151
  </react_native_1.Text>
152
152
  </react_native_1.Pressable>);
153
153
  };
154
+ // First-class App Store screenshots: walk the app's routes on THIS
155
+ // device, screenshot each, upload to the agent (which runs the ASC
156
+ // backend). Closes the overlay first so the captures are clean (no
157
+ // modal in frame), then reports via an alert.
158
+ const runStoreShots = () => {
159
+ const cfg = YaverFeedback_1.YaverFeedback.getConfig();
160
+ const ss = cfg?.storeShots;
161
+ if (!ss?.routes?.length) {
162
+ setStatus('Set config.storeShots.routes (and a navigationRef) to enable.');
163
+ setStatusTone('error');
164
+ return;
165
+ }
166
+ const app = resolveAppSlug();
167
+ onClose();
168
+ // Defer so the overlay is fully dismissed before the first capture.
169
+ setTimeout(async () => {
170
+ try {
171
+ const res = await YaverFeedback_1.YaverFeedback.captureStoreScreenshots({
172
+ app,
173
+ routes: ss.routes,
174
+ navigationRef: ss.navigationRef,
175
+ screens: ss.screens,
176
+ submit: ss.submit,
177
+ });
178
+ const msg = res.ok
179
+ ? res.submitted
180
+ ? 'Submitted for App Store review 🎉'
181
+ : res.staged
182
+ ? `Uploaded ${res.uploaded} screenshots — staged. One tap left in App Store Connect.`
183
+ : `Uploaded ${res.uploaded} App Store screenshots.`
184
+ : res.message || 'Capture failed.';
185
+ react_native_1.Alert.alert('App Store screenshots', msg);
186
+ }
187
+ catch (e) {
188
+ react_native_1.Alert.alert('App Store screenshots', e?.message ?? 'Capture failed.');
189
+ }
190
+ }, 500);
191
+ };
192
+ const storeShotsEnabled = (YaverFeedback_1.YaverFeedback.getConfig()?.storeShots?.routes?.length ?? 0) > 0;
154
193
  const triggerDeploy = async (machine) => {
155
194
  if (!options)
156
195
  return;
@@ -233,6 +272,13 @@ const DeployPanel = ({ onClose }) => {
233
272
  <react_native_1.ActivityIndicator color="rgba(255,255,255,0.6)"/>
234
273
  </react_native_1.View>) : error ? (<react_native_1.Text style={styles.error}>{error}</react_native_1.Text>) : options ? (<react_native_1.ScrollView style={styles.list}>{options.devices.map(machineRow)}</react_native_1.ScrollView>) : null}
235
274
 
275
+ {storeShotsEnabled && (<react_native_1.Pressable onPress={runStoreShots} disabled={shipping} style={({ pressed }) => [styles.shotsBtn, pressed && styles.rowPressed]}>
276
+ <react_native_1.Text style={styles.shotsBtnText}>📸 App Store screenshots</react_native_1.Text>
277
+ <react_native_1.Text style={styles.shotsBtnSub}>
278
+ capture this app on-device + upload to App Store Connect
279
+ </react_native_1.Text>
280
+ </react_native_1.Pressable>)}
281
+
236
282
  {status && (<react_native_1.Text style={[
237
283
  styles.status,
238
284
  statusTone === 'success' && styles.statusSuccess,
@@ -339,6 +385,25 @@ const styles = react_native_1.StyleSheet.create({
339
385
  rowMetaWarning: {
340
386
  color: 'rgb(255,178,115)',
341
387
  },
388
+ shotsBtn: {
389
+ marginTop: 12,
390
+ paddingVertical: 12,
391
+ paddingHorizontal: 14,
392
+ borderRadius: 10,
393
+ backgroundColor: 'rgba(124,109,255,0.16)',
394
+ borderWidth: 1,
395
+ borderColor: 'rgba(124,109,255,0.4)',
396
+ },
397
+ shotsBtnText: {
398
+ color: '#fff',
399
+ fontSize: 14,
400
+ fontWeight: '600',
401
+ },
402
+ shotsBtnSub: {
403
+ color: 'rgba(255,255,255,0.55)',
404
+ fontSize: 12,
405
+ marginTop: 2,
406
+ },
342
407
  status: {
343
408
  color: 'rgba(255,255,255,0.55)',
344
409
  fontSize: 12,
package/dist/Discovery.js CHANGED
@@ -34,6 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.YaverDiscovery = void 0;
37
+ function unrefTimer(timer) {
38
+ const maybeNodeTimer = timer;
39
+ if (typeof maybeNodeTimer.unref === 'function') {
40
+ maybeNodeTimer.unref();
41
+ }
42
+ }
37
43
  function getAsyncStorage() {
38
44
  try {
39
45
  const mod = require('@react-native-async-storage/async-storage');
@@ -333,14 +339,15 @@ class YaverDiscovery {
333
339
  static async probe(url) {
334
340
  const base = url.replace(/\/$/, '');
335
341
  const start = Date.now();
342
+ let timeoutId = null;
336
343
  try {
337
344
  const controller = new AbortController();
338
- const timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
345
+ timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
346
+ unrefTimer(timeoutId);
339
347
  const response = await fetch(`${base}/health`, {
340
348
  method: 'GET',
341
349
  signal: controller.signal,
342
350
  });
343
- clearTimeout(timeoutId);
344
351
  if (!response.ok)
345
352
  return null;
346
353
  const latency = Date.now() - start;
@@ -359,6 +366,10 @@ class YaverDiscovery {
359
366
  catch {
360
367
  return null;
361
368
  }
369
+ finally {
370
+ if (timeoutId)
371
+ clearTimeout(timeoutId);
372
+ }
362
373
  }
363
374
  static async connect(url) {
364
375
  const result = await YaverDiscovery.probe(url);