yaver-feedback-react-native 0.9.0 → 0.9.3

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 (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
package/README.md CHANGED
@@ -60,7 +60,7 @@ Starting with **0.6.0**, the SDK login screen mirrors the Yaver mobile app: nati
60
60
  Drop in `<FeedbackModal />` and the first time a user triggers feedback without a cached session, they get:
61
61
 
62
62
  1. A full-screen login modal — Apple (native) / Google / GitHub / GitLab / Microsoft (in-app browser) / email + password (inline).
63
- 2. A picker of the machines they can reach — **their own dev boxes** plus **guest-shared machines** (where another user invited them).
63
+ 2. A picker of the authenticated account's own dev boxes.
64
64
 
65
65
  Both screens persist their selection to `AsyncStorage`, so subsequent launches reconnect silently.
66
66
 
@@ -89,6 +89,107 @@ function App() {
89
89
 
90
90
  Shake your phone to open the feedback modal. On mobile the quick-access icon stays hidden until that first shake, then remains tappable for the rest of the session unless the user hides it. For launch, the sheet exposes three actions only: hot reload, vibing, and screenshot & fix.
91
91
 
92
+ ### Account-scoped Dogfood mode
93
+
94
+ An app can send only explicitly approved app accounts directly into Vibing,
95
+ while everyone else keeps the normal Feedback sheet:
96
+
97
+ ```tsx
98
+ YaverFeedback.init({
99
+ dogfood: {
100
+ enabled: true,
101
+ currentAccountId: session.user.id,
102
+ accountIds: ['approved-app-account-id'],
103
+ label: 'My App',
104
+ onExit: () => setDogfoodPreference(false),
105
+ },
106
+ });
107
+ ```
108
+
109
+ The match is exact and fails closed when the current account is absent. The
110
+ small `Y` identifies Dogfood mode; tapping it offers a confirmed exit back to
111
+ Feedback mode. This allowlist controls SDK presentation only—tasks and reloads
112
+ still require the user's normal Yaver authentication.
113
+
114
+ ### Device-enrolled Dogfood for apps without their own OAuth/backend
115
+
116
+ Mount `FeedbackModal`, then call the reusable wizard from the host app's
117
+ Settings button:
118
+
119
+ ```tsx
120
+ YaverFeedback.beginDogfoodOnboarding({
121
+ appId: 'io.example.app',
122
+ label: 'Example',
123
+ projectName: 'example',
124
+ framework: 'expo',
125
+ });
126
+ ```
127
+
128
+ The wizard runs Yaver OAuth first, then machine, coding runner/model, project,
129
+ and Browser/Hermes/WebRTC selection. It renders raw build logs and exposes the
130
+ ready preview. The app also creates a random installation ID and an Ed25519 key
131
+ inside SecureStore. The ID is a public handle, not a credential: enrollment
132
+ and each short session require a server challenge signed by the private key.
133
+ An owner approves, cancels, or revokes the installation from Yaver Settings or
134
+ MCP. Re-registering rotates the key/ID, preserves only the logical local slot,
135
+ and supersedes that slot's prior active generation without disabling another
136
+ phone.
137
+
138
+ Install `expo-secure-store` and `expo-crypto` in Expo hosts. Bare React Native
139
+ hosts may provide a `secureStore` implementation. Runtime/build controls still
140
+ require full Yaver OAuth; the account-free installation token defaults to
141
+ `feedback` + `blackbox` and cannot be rotated into a long-lived owner token.
142
+
143
+ ### Embeddable Dogfood runtime
144
+
145
+ App owners can also embed the actual source → dev-server → render lifecycle.
146
+ `DogfoodController` is deliberately headless: constructing it does nothing;
147
+ your own button calls `trigger()`, your preview renders `result.url`, and your
148
+ console renders `snapshot.logs`. Retry and partial-session cleanup are handled
149
+ by the controller.
150
+
151
+ ```tsx
152
+ import {
153
+ DogfoodController,
154
+ P2PClient,
155
+ createP2PDogfoodDriver,
156
+ } from 'yaver-feedback-react-native';
157
+
158
+ const client = new P2PClient(agentUrl, signedInUserToken);
159
+ const dogfood = new DogfoodController(
160
+ {
161
+ name: 'My App',
162
+ workDir: '/workspace/my-app',
163
+ framework: 'expo', // also react-native, flutter, vite, next
164
+ lane: 'browser', // Hermes is Expo/RN only; Flutter uses browser/WebRTC
165
+ },
166
+ createP2PDogfoodDriver(client),
167
+ {
168
+ onChange(snapshot) {
169
+ setPhase(snapshot.phase);
170
+ setConsoleText(snapshot.logs.map((line) => line.text).join('\n'));
171
+ },
172
+ },
173
+ );
174
+
175
+ // Explicit user intent: call only from your Dogfood button.
176
+ const onDogfoodPress = () => dogfood.trigger().then(({ url }) => setPreviewUrl(url));
177
+ const onRetryPress = () => dogfood.retry();
178
+ const onLeaveScreen = () => dogfood.stop();
179
+ ```
180
+
181
+ The default P2P driver calls the same `/dev/start`, `/dev/status`, `/dev/stop`,
182
+ and `/dev/events` endpoints as Yaver's Projects browser lane. Package-manager
183
+ and compiler output—including `$ npm install`, Expo/Metro, and Flutter web
184
+ output—arrives uncollapsed in `snapshot.logs`. Starting tools requires the
185
+ normal signed-in Yaver user token; a narrow feedback-only SDK token cannot
186
+ spawn processes.
187
+
188
+ For Yaver itself, installing `yaver-cli` supplies these agent endpoints. The
189
+ native Yaver app checks the selected box for a real Yaver checkout and offers
190
+ `Clone Yaver source` when it is absent; cloning still occurs only after that
191
+ explicit tap.
192
+
92
193
  ### Quick Icon Styling
93
194
 
94
195
  The quick icon is configurable at compile time through `YaverFeedback.init(...)`, with render-time overrides still available through `<QuickActionIcon />` props when needed. If you do nothing, the SDK uses a high-visibility orange bubble by default so it stays distinct from the blue/indigo FAB styling many mobile apps already have.
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) {
@@ -1,16 +1,3 @@
1
1
  import React from 'react';
2
- /**
3
- * Presentation layer for the SDK's auth + machine-picker modals.
4
- *
5
- * Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
6
- * get in-app login with no extra wiring. It listens for events emitted by
7
- * `YaverFeedback.showLogin()` / `showMachinePicker()`:
8
- *
9
- * yaverFeedback:startLogin → show login modal
10
- * yaverFeedback:startMachinePicker → show machine picker
11
- *
12
- * The overlay closes itself once login/pick succeeds, then re-emits
13
- * `yaverFeedback:startReport` so the user continues straight into the
14
- * feedback flow they originally triggered.
15
- */
2
+ /** Owner sign-in and owner-machine selection for the Feedback SDK. */
16
3
  export declare const AuthOverlay: React.FC;
@@ -38,74 +38,47 @@ const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
39
  const LoginScreen_1 = require("./LoginScreen");
40
40
  const MachinePickerScreen_1 = require("./MachinePickerScreen");
41
- const GuestOnboardingScreen_1 = require("./GuestOnboardingScreen");
42
41
  const YaverFeedback_1 = require("./YaverFeedback");
43
42
  const auth_1 = require("./auth");
44
- /**
45
- * Presentation layer for the SDK's auth + machine-picker modals.
46
- *
47
- * Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
48
- * get in-app login with no extra wiring. It listens for events emitted by
49
- * `YaverFeedback.showLogin()` / `showMachinePicker()`:
50
- *
51
- * yaverFeedback:startLogin → show login modal
52
- * yaverFeedback:startMachinePicker → show machine picker
53
- *
54
- * The overlay closes itself once login/pick succeeds, then re-emits
55
- * `yaverFeedback:startReport` so the user continues straight into the
56
- * feedback flow they originally triggered.
57
- */
43
+ /** Owner sign-in and owner-machine selection for the Feedback SDK. */
58
44
  const AuthOverlay = () => {
59
45
  const [loginVisible, setLoginVisible] = (0, react_1.useState)(false);
60
- const [guestVisible, setGuestVisible] = (0, react_1.useState)(false);
61
46
  const [pickerVisible, setPickerVisible] = (0, react_1.useState)(false);
62
47
  const [token, setToken] = (0, react_1.useState)(null);
63
- const [pendingInviteCode, setPendingInviteCode] = (0, react_1.useState)(null);
64
48
  const activeOverlayRef = (0, react_1.useRef)('none');
65
49
  const openLogin = (0, react_1.useCallback)(() => {
66
50
  activeOverlayRef.current = 'login';
67
- setGuestVisible(false);
68
51
  setPickerVisible(false);
69
52
  setLoginVisible(true);
70
53
  }, []);
71
- const openGuest = (0, react_1.useCallback)(() => {
72
- activeOverlayRef.current = 'guest';
73
- setLoginVisible(false);
74
- setPickerVisible(false);
75
- setGuestVisible(true);
76
- }, []);
77
54
  const openPicker = (0, react_1.useCallback)(() => {
78
55
  activeOverlayRef.current = 'picker';
79
56
  setLoginVisible(false);
80
- setGuestVisible(false);
81
57
  setPickerVisible(true);
82
58
  }, []);
83
59
  const closeAll = (0, react_1.useCallback)(() => {
84
60
  activeOverlayRef.current = 'none';
85
61
  setLoginVisible(false);
86
- setGuestVisible(false);
87
62
  setPickerVisible(false);
88
63
  }, []);
89
64
  (0, react_1.useEffect)(() => {
90
65
  let mounted = true;
91
- (async () => {
92
- const cached = await (0, auth_1.getToken)();
66
+ void (0, auth_1.getToken)().then((cached) => {
93
67
  if (mounted && cached)
94
68
  setToken(cached);
95
- })();
69
+ });
96
70
  const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => {
97
- if (activeOverlayRef.current !== 'none')
98
- return;
99
- openLogin();
71
+ if (activeOverlayRef.current === 'none')
72
+ openLogin();
100
73
  });
101
74
  const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', async () => {
102
75
  if (activeOverlayRef.current !== 'none')
103
76
  return;
104
77
  const cached = await (0, auth_1.getToken)();
105
- if (cached)
78
+ if (cached) {
106
79
  setToken(cached);
107
- if (cached)
108
80
  openPicker();
81
+ }
109
82
  });
110
83
  return () => {
111
84
  mounted = false;
@@ -113,49 +86,23 @@ const AuthOverlay = () => {
113
86
  pickerSub.remove();
114
87
  };
115
88
  }, [openLogin, openPicker]);
116
- const continueAfterAuth = async (newToken, inviteCode) => {
89
+ const handleLoggedIn = async (newToken) => {
117
90
  setToken(newToken);
118
91
  await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
119
- const devices = await (0, auth_1.listReachableDevices)(newToken).catch(() => ({ owned: [], shared: [] }));
120
- const cleanedInviteCode = (inviteCode ?? '').trim().toUpperCase();
121
- if (cleanedInviteCode) {
122
- setPendingInviteCode(cleanedInviteCode);
123
- openGuest();
124
- return;
125
- }
126
- if (devices.owned.length === 0 && devices.shared.length === 0) {
127
- openGuest();
128
- return;
129
- }
130
92
  openPicker();
131
93
  };
132
- const handleLoggedIn = async (newToken, opts) => {
133
- await continueAfterAuth(newToken, opts?.inviteCode);
134
- };
135
94
  const handleDevicePicked = async (device) => {
136
95
  await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
137
96
  closeAll();
138
- // Continue straight into the feedback flow the user originally triggered.
139
97
  react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
140
98
  };
141
99
  return (<>
142
100
  <react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
143
- <LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode}/>
101
+ <LoginScreen_1.YaverLoginScreen onLoggedIn={handleLoggedIn} onCancel={closeAll}/>
144
102
  </react_native_1.Modal>
145
-
146
103
  <react_native_1.Modal visible={pickerVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
147
104
  {token && (<MachinePickerScreen_1.YaverMachinePickerScreen token={token} currentDeviceId={YaverFeedback_1.YaverFeedback.getConfig()?.preferredDeviceId} onPick={handleDevicePicked} onCancel={closeAll}/>)}
148
105
  </react_native_1.Modal>
149
-
150
- <react_native_1.Modal visible={guestVisible && !!token} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
151
- {token && (<GuestOnboardingScreen_1.YaverGuestOnboardingScreen token={token} initialInviteCode={pendingInviteCode ?? YaverFeedback_1.YaverFeedback.getConfig()?.guestInviteCode} onContinue={() => {
152
- setPendingInviteCode(null);
153
- openPicker();
154
- }} onCancel={() => {
155
- setPendingInviteCode(null);
156
- openPicker();
157
- }}/>)}
158
- </react_native_1.Modal>
159
106
  </>);
160
107
  };
161
108
  exports.AuthOverlay = AuthOverlay;
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');
@@ -173,12 +179,6 @@ class YaverDiscovery {
173
179
  needsAuth: !!d.needsAuth,
174
180
  runnerDown: !!d.runnerDown,
175
181
  lastHeartbeat: d.lastHeartbeat ?? 0,
176
- isGuest: !!d.isGuest,
177
- hostUserId: d.hostUserId,
178
- hostName: d.hostName,
179
- hostEmail: d.hostEmail,
180
- hostUserIdString: d.hostUserIdString,
181
- accessScope: d.accessScope ?? 'owner',
182
182
  quicHost: d.quicHost ?? d.host ?? '',
183
183
  quicPort: d.quicPort ?? 0,
184
184
  httpPort: d.httpPort ?? d.quicPort,
@@ -333,14 +333,15 @@ class YaverDiscovery {
333
333
  static async probe(url) {
334
334
  const base = url.replace(/\/$/, '');
335
335
  const start = Date.now();
336
+ let timeoutId = null;
336
337
  try {
337
338
  const controller = new AbortController();
338
- const timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
339
+ timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
340
+ unrefTimer(timeoutId);
339
341
  const response = await fetch(`${base}/health`, {
340
342
  method: 'GET',
341
343
  signal: controller.signal,
342
344
  });
343
- clearTimeout(timeoutId);
344
345
  if (!response.ok)
345
346
  return null;
346
347
  const latency = Date.now() - start;
@@ -359,6 +360,10 @@ class YaverDiscovery {
359
360
  catch {
360
361
  return null;
361
362
  }
363
+ finally {
364
+ if (timeoutId)
365
+ clearTimeout(timeoutId);
366
+ }
362
367
  }
363
368
  static async connect(url) {
364
369
  const result = await YaverDiscovery.probe(url);