yaver-feedback-react-native 0.9.4 → 0.9.7

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 (38) hide show
  1. package/README.md +31 -16
  2. package/android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java +59 -0
  3. package/app.plugin.js +200 -3
  4. package/dist/AuthOverlay.js +13 -2
  5. package/dist/FeedbackModal.js +70 -7
  6. package/dist/YaverFeedback.d.ts +34 -9
  7. package/dist/YaverFeedback.js +304 -20
  8. package/dist/__tests__/NativeDogfoodShortcut.test.d.ts +1 -0
  9. package/dist/__tests__/NativeDogfoodShortcut.test.js +52 -0
  10. package/dist/__tests__/ReportIdentity.test.js +6 -3
  11. package/dist/__tests__/YaverFeedback.test.js +97 -4
  12. package/dist/__tests__/deviceDogfood.test.js +2 -2
  13. package/dist/__tests__/dogfoodPolicy.test.js +1 -1
  14. package/dist/auth.d.ts +7 -0
  15. package/dist/auth.js +28 -0
  16. package/dist/deviceDogfood.d.ts +6 -2
  17. package/dist/deviceDogfood.js +6 -3
  18. package/dist/dogfoodPolicy.d.ts +32 -1
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +5 -2
  21. package/dist/preferences.d.ts +2 -0
  22. package/dist/preferences.js +24 -0
  23. package/ios/YaverHotReload.m +8 -0
  24. package/ios/YaverHotReload.swift +37 -0
  25. package/package.json +2 -2
  26. package/src/AuthOverlay.tsx +11 -2
  27. package/src/FeedbackModal.tsx +98 -6
  28. package/src/YaverFeedback.ts +271 -19
  29. package/src/__tests__/NativeDogfoodShortcut.test.ts +59 -0
  30. package/src/__tests__/ReportIdentity.test.ts +6 -4
  31. package/src/__tests__/YaverFeedback.test.ts +104 -5
  32. package/src/__tests__/deviceDogfood.test.ts +2 -2
  33. package/src/__tests__/dogfoodPolicy.test.ts +1 -1
  34. package/src/auth.ts +29 -0
  35. package/src/deviceDogfood.ts +8 -3
  36. package/src/dogfoodPolicy.ts +32 -1
  37. package/src/index.ts +5 -2
  38. package/src/preferences.ts +20 -0
package/README.md CHANGED
@@ -113,31 +113,46 @@ still require the user's normal Yaver authentication.
113
113
 
114
114
  ### Device-enrolled Dogfood for apps without their own OAuth/backend
115
115
 
116
- Mount `FeedbackModal`, then call the reusable wizard from the host app's
117
- Settings button:
116
+ Mount `FeedbackModal` once and configure the ACL-backed dynamic app shortcut.
117
+ No permanent Settings row or shake gesture is required:
118
118
 
119
119
  ```tsx
120
- YaverFeedback.beginDogfoodOnboarding({
121
- appId: 'io.example.app',
122
- label: 'Example',
120
+ YaverFeedback.init({
121
+ enabled: true,
122
+ trigger: 'manual',
123
+ quickIcon: 'off',
124
+ disableShakeGesture: true,
125
+ bundleId: 'io.example.app',
123
126
  projectName: 'example',
124
- framework: 'expo',
127
+ dogfood: {
128
+ label: 'Example',
129
+ framework: 'expo',
130
+ appShortcut: { label: 'Dogfood Example' },
131
+ // Optional host presentation ACL. Backend account + phone-key approval
132
+ // remains mandatory even when this returns true.
133
+ canShow: (access) => appUser.isAdmin && access.authorized,
134
+ },
125
135
  });
126
136
  ```
127
137
 
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.
138
+ For first-time setup, a signed-in user opens the app from Yaver's Dogfood app
139
+ catalog. The public deep link contains no token. The SDK runs stateful Yaver
140
+ OAuth, machine, coding runner/model, project, and Browser/Hermes/WebRTC setup.
141
+ After the owner approves that exact account + app ID + phone key, iOS and
142
+ Android add the long-press shortcut. Signing out, revocation, or host ACL denial
143
+ removes it on the next foreground sync.
144
+
145
+ The app creates a random installation ID and Ed25519 key inside SecureStore.
146
+ The ID is a public handle, not a credential: enrollment and each short session
147
+ require a server challenge signed by the private key. An owner approves,
148
+ cancels, or revokes from Yaver mobile, CLI, or MCP. Re-registering rotates the
149
+ key/ID, preserves only the logical local slot, and atomically supersedes that
150
+ slot's prior active generation when the replacement is approved, without
151
+ disabling another phone.
137
152
 
138
153
  Install `expo-secure-store` and `expo-crypto` in Expo hosts. Bare React Native
139
154
  hosts may provide a `secureStore` implementation. Runtime/build controls still
140
- require full Yaver OAuth; the account-free installation token defaults to
155
+ require full Yaver OAuth; the account-bound installation token defaults to
141
156
  `feedback` + `blackbox` and cannot be rotated into a long-lived owner token.
142
157
 
143
158
  ### Embeddable Dogfood runtime
@@ -2,7 +2,12 @@ package io.yaver.feedback;
2
2
 
3
3
  import android.app.Activity;
4
4
  import android.content.Context;
5
+ import android.content.Intent;
5
6
  import android.content.SharedPreferences;
7
+ import android.content.pm.ShortcutInfo;
8
+ import android.content.pm.ShortcutManager;
9
+ import android.graphics.drawable.Icon;
10
+ import android.os.Build;
6
11
  import android.os.Handler;
7
12
  import android.os.Looper;
8
13
  import android.util.Log;
@@ -25,6 +30,7 @@ import java.io.InputStream;
25
30
  import java.net.HttpURLConnection;
26
31
  import java.net.URL;
27
32
  import java.util.concurrent.Executors;
33
+ import java.util.Collections;
28
34
 
29
35
  /**
30
36
  * Hot reload native module for the Yaver Feedback SDK (Android).
@@ -45,6 +51,8 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
45
51
  private static final String PREFS_KEY_BOOT_ATTEMPTS = "boot_attempts";
46
52
  private static final String PREFS_KEY_BUNDLE_MTIME = "bundle_mtime";
47
53
  private static final int MAX_BOOT_ATTEMPTS = 3;
54
+ private static final String DOGFOOD_SHORTCUT_ID = "io.yaver.feedback.dogfood";
55
+ private static final String DOGFOOD_SHORTCUT_EXTRA = "io.yaver.feedback.DOGFOOD";
48
56
 
49
57
  public YaverHotReloadModule(ReactApplicationContext context) {
50
58
  super(context);
@@ -153,6 +161,57 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
153
161
  promise.resolve(true);
154
162
  }
155
163
 
164
+ /** Dynamic shortcut: only JS, after backend ACL resolution, may add it. */
165
+ @ReactMethod
166
+ public void setDogfoodShortcut(boolean enabled, String label, Promise promise) {
167
+ try {
168
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
169
+ promise.resolve(false);
170
+ return;
171
+ }
172
+ Context context = getReactApplicationContext();
173
+ ShortcutManager manager = context.getSystemService(ShortcutManager.class);
174
+ if (manager == null) {
175
+ promise.resolve(false);
176
+ return;
177
+ }
178
+ manager.removeDynamicShortcuts(Collections.singletonList(DOGFOOD_SHORTCUT_ID));
179
+ if (enabled) {
180
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
181
+ if (intent == null) throw new IllegalStateException("App launch intent unavailable");
182
+ intent.setAction(DOGFOOD_SHORTCUT_EXTRA);
183
+ intent.putExtra(DOGFOOD_SHORTCUT_EXTRA, true);
184
+ ShortcutInfo shortcut = new ShortcutInfo.Builder(context, DOGFOOD_SHORTCUT_ID)
185
+ .setShortLabel(label == null || label.trim().isEmpty() ? "Dogfood" : label)
186
+ .setLongLabel(label == null || label.trim().isEmpty() ? "Dogfood" : label)
187
+ .setIcon(Icon.createWithResource(context, android.R.drawable.ic_media_play))
188
+ .setIntent(intent)
189
+ .build();
190
+ manager.addDynamicShortcuts(Collections.singletonList(shortcut));
191
+ }
192
+ promise.resolve(enabled);
193
+ } catch (Exception error) {
194
+ promise.reject("DOGFOOD_SHORTCUT_FAILED", error.getMessage(), error);
195
+ }
196
+ }
197
+
198
+ @ReactMethod
199
+ public void consumeDogfoodShortcut(Promise promise) {
200
+ Activity activity = getCurrentActivity();
201
+ if (activity == null || activity.getIntent() == null) {
202
+ promise.resolve(false);
203
+ return;
204
+ }
205
+ Intent intent = activity.getIntent();
206
+ boolean pending = DOGFOOD_SHORTCUT_EXTRA.equals(intent.getAction())
207
+ || intent.getBooleanExtra(DOGFOOD_SHORTCUT_EXTRA, false);
208
+ if (pending) {
209
+ intent.removeExtra(DOGFOOD_SHORTCUT_EXTRA);
210
+ intent.setAction(Intent.ACTION_MAIN);
211
+ }
212
+ promise.resolve(pending);
213
+ }
214
+
156
215
  /**
157
216
  * Recreate the React Native context with the new bundle.
158
217
  */
package/app.plugin.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  withXcodeProject,
21
21
  withAppDelegate,
22
22
  withMainApplication,
23
+ withMainActivity,
23
24
  withDangerousMod,
24
25
  createRunOncePlugin,
25
26
  } = require(configPluginsPath);
@@ -38,6 +39,14 @@ function withYaverFeedbackIOS(config) {
38
39
  config.modResults.NSMicrophoneUsageDescription =
39
40
  "Used for voice annotations in feedback reports during development";
40
41
  }
42
+ const bundleId = config.ios?.bundleIdentifier;
43
+ if (bundleId) {
44
+ const scheme = `yaver-dogfood-${bundleId.toLowerCase().replace(/[^a-z0-9.-]/g, "-")}`;
45
+ const urlTypes = config.modResults.CFBundleURLTypes || [];
46
+ const exists = urlTypes.some((entry) => Array.isArray(entry.CFBundleURLSchemes) && entry.CFBundleURLSchemes.includes(scheme));
47
+ if (!exists) urlTypes.push({ CFBundleURLName: `${bundleId}.yaver-dogfood`, CFBundleURLSchemes: [scheme] });
48
+ config.modResults.CFBundleURLTypes = urlTypes;
49
+ }
41
50
  return config;
42
51
  });
43
52
  }
@@ -78,6 +87,28 @@ function withYaverFeedbackAndroid(config) {
78
87
  }
79
88
  }
80
89
 
90
+ const packageName = config.android?.package;
91
+ const mainActivity = manifest.application?.[0]?.activity?.find((activity) =>
92
+ activity.$?.["android:name"]?.endsWith("MainActivity")
93
+ );
94
+ if (packageName && mainActivity) {
95
+ const scheme = `yaver-dogfood-${packageName.toLowerCase().replace(/[^a-z0-9.-]/g, "-")}`;
96
+ mainActivity["intent-filter"] = mainActivity["intent-filter"] || [];
97
+ const exists = mainActivity["intent-filter"].some((filter) =>
98
+ filter.data?.some((data) => data.$?.["android:scheme"] === scheme)
99
+ );
100
+ if (!exists) {
101
+ mainActivity["intent-filter"].push({
102
+ action: [{ $: { "android:name": "android.intent.action.VIEW" } }],
103
+ category: [
104
+ { $: { "android:name": "android.intent.category.DEFAULT" } },
105
+ { $: { "android:name": "android.intent.category.BROWSABLE" } },
106
+ ],
107
+ data: [{ $: { "android:scheme": scheme, "android:host": "activate" } }],
108
+ });
109
+ }
110
+ }
111
+
81
112
  return config;
82
113
  });
83
114
  }
@@ -183,8 +214,10 @@ function withYaverAppDelegateHook(config) {
183
214
  return withAppDelegate(config, (config) => {
184
215
  const contents = config.modResults.contents;
185
216
 
186
- // Only patch if not already patched
217
+ // Existing consumers may already have the hot-reload hook from an older
218
+ // SDK. Still apply newer, independently-versioned native contracts.
187
219
  if (contents.includes("YaverHotReload")) {
220
+ config.modResults.contents = patchDogfoodAppShortcut(contents);
188
221
  return config;
189
222
  }
190
223
 
@@ -326,11 +359,136 @@ function withYaverAppDelegateHook(config) {
326
359
  patched = patched.replace("return setupYaverHotReload()", "setupYaverHotReload()");
327
360
  }
328
361
 
329
- config.modResults.contents = patched;
362
+ config.modResults.contents = patchDogfoodAppShortcut(patched);
330
363
  return config;
331
364
  });
332
365
  }
333
366
 
367
+ /** Wire the dynamic iOS Home Screen shortcut into the SDK native module.
368
+ * The shortcut itself is created at runtime only after backend ACL success;
369
+ * this hook merely consumes a user-selected action on warm/cold launch. */
370
+ function patchDogfoodAppShortcut(contents) {
371
+ if (contents.includes("Yaver Feedback SDK Dogfood Shortcut")) return contents;
372
+ let patched = contents;
373
+
374
+ // Cold launch: UIKit supplies the shortcut in launchOptions before RN/JS.
375
+ const setupAnchor = " setupYaverHotReload()";
376
+ if (patched.includes(setupAnchor)) {
377
+ patched = patched.replace(
378
+ setupAnchor,
379
+ ` if let yaverShortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem,
380
+ yaverShortcut.type == YaverHotReload.dogfoodShortcutType {
381
+ YaverHotReload.markDogfoodShortcutPending()
382
+ }
383
+ ${setupAnchor}`
384
+ );
385
+ }
386
+
387
+ const classCloseIndex = findAppDelegateClassClose(patched);
388
+ if (classCloseIndex > 0) {
389
+ const handler = `
390
+ // MARK: - Yaver Feedback SDK Dogfood Shortcut
391
+
392
+ public override func application(
393
+ _ application: UIApplication,
394
+ performActionFor shortcutItem: UIApplicationShortcutItem,
395
+ completionHandler: @escaping (Bool) -> Void
396
+ ) {
397
+ if shortcutItem.type == YaverHotReload.dogfoodShortcutType {
398
+ YaverHotReload.markDogfoodShortcutPending()
399
+ completionHandler(true)
400
+ return
401
+ }
402
+ super.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
403
+ }
404
+ `;
405
+ patched = patched.slice(0, classCloseIndex) + handler + patched.slice(classCloseIndex);
406
+ }
407
+ return patched;
408
+ }
409
+
410
+ /** Scene-based iOS apps receive Home Screen quick actions through their
411
+ * UIWindowSceneDelegate, not AppDelegate. A CarPlay scene manifest is enough
412
+ * to opt an otherwise ordinary Expo app into that lifecycle, so patch the
413
+ * phone scene delegate when a host has one. */
414
+ function withYaverSceneDelegateShortcut(config) {
415
+ return withDangerousMod(config, [
416
+ "ios",
417
+ (config) => {
418
+ const appName = config.modRequest.projectName;
419
+ if (!appName) return config;
420
+ const sourceDir = path.join(config.modRequest.platformProjectRoot, appName);
421
+ if (!fs.existsSync(sourceDir)) return config;
422
+
423
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
424
+ if (!entry.isFile() || !entry.name.endsWith("SceneDelegate.swift")) continue;
425
+ const sourcePath = path.join(sourceDir, entry.name);
426
+ const source = fs.readFileSync(sourcePath, "utf8");
427
+ if (!source.includes("UIWindowSceneDelegate")) continue;
428
+ const patched = patchDogfoodSceneDelegate(source);
429
+ if (patched !== source) fs.writeFileSync(sourcePath, patched);
430
+ }
431
+ return config;
432
+ },
433
+ ]);
434
+ }
435
+
436
+ function patchDogfoodSceneDelegate(contents) {
437
+ if (contents.includes("Yaver Feedback SDK Dogfood Scene Shortcut")) return contents;
438
+
439
+ let patched = insertAtEndOfMethod(
440
+ contents,
441
+ "options connectionOptions: UIScene.ConnectionOptions",
442
+ `
443
+ // Yaver Feedback SDK Dogfood Scene Shortcut: cold launch.
444
+ if let yaverShortcut = connectionOptions.shortcutItem,
445
+ yaverShortcut.type == YaverHotReload.dogfoodShortcutType {
446
+ YaverHotReload.markDogfoodShortcutPending()
447
+ }
448
+ `
449
+ );
450
+
451
+ // A custom scene delegate may already forward quick actions to the patched
452
+ // AppDelegate. Keep that host-owned behavior instead of declaring a second
453
+ // method with the same Swift selector.
454
+ if (patched.includes("performActionFor shortcutItem")) return patched;
455
+
456
+ const classCloseIndex = findWindowSceneDelegateClassClose(patched);
457
+ if (classCloseIndex < 0) return patched;
458
+ const handler = `
459
+ // MARK: - Yaver Feedback SDK Dogfood Scene Shortcut
460
+
461
+ func windowScene(
462
+ _ windowScene: UIWindowScene,
463
+ performActionFor shortcutItem: UIApplicationShortcutItem,
464
+ completionHandler: @escaping (Bool) -> Void
465
+ ) {
466
+ guard shortcutItem.type == YaverHotReload.dogfoodShortcutType else {
467
+ completionHandler(false)
468
+ return
469
+ }
470
+ YaverHotReload.markDogfoodShortcutPending()
471
+ completionHandler(true)
472
+ }
473
+ `;
474
+ return patched.slice(0, classCloseIndex) + handler + patched.slice(classCloseIndex);
475
+ }
476
+
477
+ function findWindowSceneDelegateClassClose(contents) {
478
+ const headerMatch = contents.match(/class\s+\w+\s*:[^{]*UIWindowSceneDelegate[^{]*\{/);
479
+ if (!headerMatch) return -1;
480
+ const bodyStart = headerMatch.index + headerMatch[0].length - 1;
481
+ let depth = 0;
482
+ for (let i = bodyStart; i < contents.length; i++) {
483
+ if (contents[i] === "{") depth++;
484
+ else if (contents[i] === "}") {
485
+ depth--;
486
+ if (depth === 0) return i;
487
+ }
488
+ }
489
+ return -1;
490
+ }
491
+
334
492
  // Locate the closing brace of `class AppDelegate: ...` in an Expo
335
493
  // Swift AppDelegate file. Returns the index of the matching `}` for
336
494
  // the AppDelegate class's opening `{`, or -1 if not found. We need
@@ -404,6 +562,33 @@ function withYaverAndroidHotReload(config) {
404
562
  return config;
405
563
  });
406
564
 
565
+ // ReactActivity forwards warm-launch intents to RN but does not update
566
+ // Activity.getIntent(). The native module consumes the explicit shortcut
567
+ // extra from getIntent(), so retain the latest intent for warm launches.
568
+ config = withMainActivity(config, (config) => {
569
+ let contents = config.modResults.contents;
570
+ if (contents.includes("yaverDogfoodShortcutIntent")) return config;
571
+ const classClose = contents.lastIndexOf("}");
572
+ if (classClose < 0) return config;
573
+ const method = config.modResults.language === "kt"
574
+ ? `
575
+ // Yaver Feedback SDK: retain dynamic Dogfood shortcut warm-launch intent.
576
+ override fun onNewIntent(yaverDogfoodShortcutIntent: android.content.Intent) {
577
+ setIntent(yaverDogfoodShortcutIntent)
578
+ super.onNewIntent(yaverDogfoodShortcutIntent)
579
+ }
580
+ `
581
+ : `
582
+ // Yaver Feedback SDK: retain dynamic Dogfood shortcut warm-launch intent.
583
+ @Override public void onNewIntent(android.content.Intent yaverDogfoodShortcutIntent) {
584
+ setIntent(yaverDogfoodShortcutIntent);
585
+ super.onNewIntent(yaverDogfoodShortcutIntent);
586
+ }
587
+ `;
588
+ config.modResults.contents = contents.slice(0, classClose) + method + contents.slice(classClose);
589
+ return config;
590
+ });
591
+
407
592
  return config;
408
593
  }
409
594
 
@@ -608,14 +793,26 @@ function withYaverFeedback(config, props) {
608
793
  if (enableHotReload) {
609
794
  config = withYaverHotReloadNativeModule(config);
610
795
  config = withYaverAppDelegateHook(config);
796
+ config = withYaverSceneDelegateShortcut(config);
611
797
  config = withYaverAndroidHotReload(config);
612
798
  }
613
799
 
614
800
  return config;
615
801
  }
616
802
 
617
- module.exports = createRunOncePlugin(
803
+ const yaverFeedbackPlugin = createRunOncePlugin(
618
804
  withYaverFeedback,
619
805
  pkg.name,
620
806
  pkg.version
621
807
  );
808
+
809
+ // Pure transforms are exposed for contract tests only. Keeping the test at the
810
+ // config-plugin seam catches template drift before a consumer discovers it in
811
+ // an archive build.
812
+ yaverFeedbackPlugin.__test = {
813
+ patchDogfoodAppShortcut,
814
+ patchDogfoodSceneDelegate,
815
+ findAppDelegateClassClose,
816
+ findWindowSceneDelegateClassClose,
817
+ };
818
+ module.exports = yaverFeedbackPlugin;
@@ -89,12 +89,23 @@ const AuthOverlay = () => {
89
89
  const handleLoggedIn = async (newToken) => {
90
90
  setToken(newToken);
91
91
  await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
92
- openPicker();
92
+ if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
93
+ closeAll();
94
+ await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
95
+ }
96
+ else {
97
+ openPicker();
98
+ }
93
99
  };
94
100
  const handleDevicePicked = async (device) => {
95
101
  await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
96
102
  closeAll();
97
- react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
103
+ if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
104
+ await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
105
+ }
106
+ else {
107
+ react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
108
+ }
98
109
  };
99
110
  return (<>
100
111
  <react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
@@ -236,8 +236,17 @@ const FeedbackModal = () => {
236
236
  || projects[0]
237
237
  || null;
238
238
  setDogfoodProject((current) => current && projects.some((item) => item.path === current.path) ? current : preferred);
239
- if (preferred)
240
- setDogfoodLane((0, DogfoodRuntime_1.defaultDogfoodLane)(preferred.framework || onboarding.framework || 'expo'));
239
+ if (preferred) {
240
+ const framework = preferred.framework || onboarding.framework || 'expo';
241
+ const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(preferred.path, framework).catch(() => null);
242
+ const nativeRuntimeAvailable = !!capabilities?.targets.some((target) => target.enabled && target.id !== 'browser-window');
243
+ if (mountedRef.current)
244
+ setDogfoodNativeAvailable(nativeRuntimeAvailable);
245
+ const savedLane = await (0, preferences_1.getPreferredDogfoodLane)(onboarding.appId);
246
+ const savedSupported = (0, DogfoodRuntime_1.dogfoodLaneOptions)(framework, { nativeRuntimeAvailable })
247
+ .some((option) => option.lane === savedLane && option.supported);
248
+ setDogfoodLane(savedLane && savedSupported ? savedLane : (0, DogfoodRuntime_1.defaultDogfoodLane)(framework));
249
+ }
241
250
  }
242
251
  catch (cause) {
243
252
  if (mountedRef.current)
@@ -468,8 +477,12 @@ const FeedbackModal = () => {
468
477
  (0, react_1.useEffect)(() => {
469
478
  mountedRef.current = true;
470
479
  const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
471
- if (YaverFeedback_1.YaverFeedback.isEnabled()) {
472
- const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
480
+ const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
481
+ // Explicit Dogfood remains usable when passive capture/shake is off.
482
+ // The host still owns visibility; server OAuth/device signatures own
483
+ // authority. Keeping this event path independent avoids toggling the
484
+ // user's feedback preference merely to open Developer Mode.
485
+ if (YaverFeedback_1.YaverFeedback.isEnabled() || onboarding) {
473
486
  const directDogfood = YaverFeedback_1.YaverFeedback.getDogfoodStatus().active;
474
487
  setDogfoodActive(directDogfood);
475
488
  setVisible(true);
@@ -1003,6 +1016,14 @@ const FeedbackModal = () => {
1003
1016
  const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
1004
1017
  const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
1005
1018
  const needsAuthRunnerCount = runnerCards.filter((row) => row.installed && !row.authConfigured && !row.ready).length;
1019
+ const selectedDogfoodRunner = preferredRunner
1020
+ ? runnerCards.find((row) => row.id === preferredRunner) ?? null
1021
+ : null;
1022
+ const dogfoodRunnerReady = !!selectedDogfoodRunner
1023
+ && (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
1024
+ const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1025
+ || !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
1026
+ const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady;
1006
1027
  // Once the user fires off a vibe task, swap the entire modal body
1007
1028
  // for the live chat screen. The chat manages its own SSE
1008
1029
  // subscription, multi-turn follow-ups, and Reload button. Closing
@@ -1102,9 +1123,40 @@ const FeedbackModal = () => {
1102
1123
  <react_native_1.Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</react_native_1.Text>
1103
1124
  </react_native_1.Pressable>))}
1104
1125
  </react_native_1.ScrollView>
1126
+ <react_native_1.Text style={styles.dogfoodStepLabel}>Coding agent</react_native_1.Text>
1127
+ <react_native_1.View style={styles.dogfoodChoiceRow}>
1128
+ {runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (<react_native_1.Pressable key={row.id} onPress={() => {
1129
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1130
+ setPreferredRunnerState(row.id);
1131
+ setPreferredModelState(nextModel);
1132
+ void (0, preferences_1.setPreferredRunner)(row.id);
1133
+ void (0, preferences_1.setPreferredModel)(nextModel || null);
1134
+ }} style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]} accessibilityRole="button" accessibilityState={{ selected: preferredRunner === row.id }} accessibilityLabel={`Use ${row.name} for Dogfood`}>
1135
+ <react_native_1.Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</react_native_1.Text>
1136
+ </react_native_1.Pressable>))}
1137
+ </react_native_1.View>
1138
+ {readyRunnerCount === 0 ? (<react_native_1.Text style={styles.dogfoodWizardHint}>Sign in or configure a coding agent under Coding Agents below.</react_native_1.Text>) : null}
1139
+ {selectedDogfoodRunner?.models?.length ? (<>
1140
+ <react_native_1.Text style={styles.dogfoodStepLabel}>Model</react_native_1.Text>
1141
+ <react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1142
+ {selectedDogfoodRunner.models.map((model) => (<react_native_1.Pressable key={model.id} onPress={() => {
1143
+ setPreferredModelState(model.id);
1144
+ void (0, preferences_1.setPreferredModel)(model.id);
1145
+ }} style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]} accessibilityRole="button" accessibilityState={{ selected: preferredModel === model.id }} accessibilityLabel={`Use ${model.name || model.id} model for Dogfood`}>
1146
+ <react_native_1.Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</react_native_1.Text>
1147
+ </react_native_1.Pressable>))}
1148
+ </react_native_1.ScrollView>
1149
+ </>) : null}
1105
1150
  <react_native_1.Text style={styles.dogfoodStepLabel}>Runtime lane</react_native_1.Text>
1106
1151
  <react_native_1.View style={styles.dogfoodChoiceRow}>
1107
- {(0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable }).map((option) => (<react_native_1.Pressable key={option.lane} onPress={() => option.supported && setDogfoodLane(option.lane)} style={[
1152
+ {(0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable }).map((option) => (<react_native_1.Pressable key={option.lane} onPress={() => {
1153
+ if (!option.supported)
1154
+ return;
1155
+ setDogfoodLane(option.lane);
1156
+ const appId = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.appId;
1157
+ if (appId)
1158
+ void (0, preferences_1.setPreferredDogfoodLane)(appId, option.lane);
1159
+ }} style={[
1108
1160
  styles.dogfoodChoice,
1109
1161
  dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
1110
1162
  !option.supported && styles.actionBtnDisabled,
@@ -1113,9 +1165,9 @@ const FeedbackModal = () => {
1113
1165
  </react_native_1.Pressable>))}
1114
1166
  </react_native_1.View>
1115
1167
  <react_native_1.Text style={styles.dogfoodWizardHint}>
1116
- Runner: {[preferredRunner || 'automatic', preferredModel].filter(Boolean).join(' · ')}. Change it under Coding Agents below.
1168
+ {[preferredRunner || 'Choose a coding agent', preferredModel].filter(Boolean).join(' · ')}
1117
1169
  </react_native_1.Text>
1118
- <ActionRow label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'} tint="#818cf8" onPress={() => void startDogfoodRuntime()} disabled={!dogfoodProject || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))} busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}/>
1170
+ <ActionRow label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'} tint="#818cf8" onPress={() => void startDogfoodRuntime()} disabled={dogfoodStartBlocked || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))} busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}/>
1119
1171
  {dogfoodRuntime ? (<react_native_1.View style={styles.dogfoodConsole}>
1120
1172
  <react_native_1.Text style={styles.dogfoodConsoleStatus}>{dogfoodRuntime.message}</react_native_1.Text>
1121
1173
  {dogfoodRuntime.logs.slice(-80).map((line, index) => (<react_native_1.Text key={`${line.at}-${index}`} selectable style={styles.dogfoodConsoleLine}>{line.text}</react_native_1.Text>))}
@@ -1258,6 +1310,15 @@ const FeedbackModal = () => {
1258
1310
  </react_native_1.Pressable>
1259
1311
  </react_native_1.View>)}
1260
1312
 
1313
+ {YaverFeedback_1.YaverFeedback.isAuthed() ? (<react_native_1.Pressable onPress={() => {
1314
+ void YaverFeedback_1.YaverFeedback.signOut().then(() => {
1315
+ handleClose();
1316
+ YaverFeedback_1.YaverFeedback.showLogin();
1317
+ });
1318
+ }} style={({ pressed }) => [styles.yaverSignOutBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel="Sign out of Yaver">
1319
+ <react_native_1.Text style={styles.yaverSignOutText}>Sign out of Yaver</react_native_1.Text>
1320
+ </react_native_1.Pressable>) : null}
1321
+
1261
1322
  <react_native_1.View style={styles.iconSelector}>
1262
1323
  <react_native_1.Text style={styles.iconSelectorTitle}>Quick Icon Color</react_native_1.Text>
1263
1324
  <react_native_1.Text style={styles.iconSelectorText}>
@@ -1414,6 +1475,8 @@ const styles = react_native_1.StyleSheet.create({
1414
1475
  dogfoodConsoleError: { color: '#fca5a5', fontSize: 11, lineHeight: 16, marginTop: 5 },
1415
1476
  dogfoodOpenPreview: { alignSelf: 'flex-start', borderRadius: 9, paddingHorizontal: 11, paddingVertical: 8, marginTop: 6, backgroundColor: '#6555df' },
1416
1477
  dogfoodOpenPreviewText: { color: '#fff', fontSize: 12, fontWeight: '800' },
1478
+ yaverSignOutBtn: { alignSelf: 'flex-start', paddingHorizontal: 4, paddingVertical: 8 },
1479
+ yaverSignOutText: { color: '#b42318', fontSize: 13, fontWeight: '700' },
1417
1480
  reloadRow: {
1418
1481
  gap: 4,
1419
1482
  },
@@ -2,7 +2,7 @@ import { FeedbackConfig, CapturedError } from './types';
2
2
  import { P2PClient } from './P2PClient';
3
3
  import { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult } from './storeShots';
4
4
  import { QuickIconColorPreset } from './preferences';
5
- import { type SDKDogfoodStatus } from './dogfoodPolicy';
5
+ import { type SDKDogfoodStatus, type DogfoodAccessSnapshot } from './dogfoodPolicy';
6
6
  import { type DeviceDogfoodOptions, type DeviceDogfoodSession, type DeviceDogfoodState } from './deviceDogfood';
7
7
  export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
8
8
  /** Hint used to preselect the matching project returned by the owner machine. */
@@ -10,6 +10,12 @@ export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
10
10
  /** Framework fallback when the machine has not classified the project yet. */
11
11
  framework?: string;
12
12
  }
13
+ export type DogfoodFlowPhase = 'idle' | 'denied' | 'auth-required' | 'machine-required' | 'opening' | 'error';
14
+ export interface DogfoodFlowState {
15
+ phase: DogfoodFlowPhase;
16
+ appId?: string;
17
+ error?: string;
18
+ }
13
19
  /**
14
20
  * Main entry point for the Yaver Feedback SDK.
15
21
  * Call `YaverFeedback.init()` once at app startup.
@@ -108,10 +114,29 @@ export declare class YaverFeedback {
108
114
  * an active session; no-ops otherwise.
109
115
  */
110
116
  static showMachinePicker(): void;
111
- /** Begin the reusable host-app Dogfood wizard. Owner OAuth and machine
112
- * selection intentionally happen before installation enrollment/runtime
113
- * controls because starting builds or runners is owner-level authority. */
114
- static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): void;
117
+ /** Configure Dogfood once during host init. Hosts still decide whether and
118
+ * where to render an affordance; `openDogfood()` owns all flow mechanics. */
119
+ static configureDogfood(options: DogfoodOnboardingOptions): void;
120
+ /**
121
+ * Open Dogfood using config.dogfood + the normal app identity. Cached OAuth,
122
+ * machine, runner and model choices are reused. The corresponding picker is
123
+ * shown only when a required choice is missing.
124
+ */
125
+ static openDogfood(overrides?: Partial<DogfoodOnboardingOptions>): Promise<DogfoodFlowState>;
126
+ /** Resolve the host-facing ACL snapshot without granting authority. This is
127
+ * the one endpoint custom Settings screens need for visibility/status UI. */
128
+ static getDogfoodAccess(): Promise<DogfoodAccessSnapshot>;
129
+ /** Add/remove the platform Home Screen shortcut from backend-authoritative
130
+ * owner/device ACL state. Static plist shortcuts are intentionally avoided:
131
+ * an unauthorized install must never advertise a hidden developer action. */
132
+ static syncDogfoodAppShortcut(): Promise<boolean>;
133
+ /** Backwards-compatible entry point for existing integrations. */
134
+ static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): Promise<DogfoodFlowState>;
135
+ /** Continue after OAuth or machine selection. Public so custom host UI can
136
+ * hand control back without recreating the SDK state machine. */
137
+ static continueDogfoodOnboarding(): Promise<DogfoodFlowState>;
138
+ static getDogfoodFlowState(): DogfoodFlowState;
139
+ static onDogfoodFlowState(listener: (state: DogfoodFlowState) => void): () => void;
115
140
  static getDogfoodOnboarding(): DogfoodOnboardingOptions | null;
116
141
  static clearDogfoodOnboarding(): void;
117
142
  /**
@@ -215,10 +240,10 @@ export declare class YaverFeedback {
215
240
  static getConfig(): FeedbackConfig | null;
216
241
  /** Current third-party SDK mode. Fails closed unless enabled + account match. */
217
242
  static getDogfoodStatus(): SDKDogfoodStatus;
218
- /** One-call, account-free Dogfood bootstrap for third-party apps. On first
219
- * launch it creates/proves the installation key and returns pending; after
220
- * owner approval the same call obtains a short-lived scoped Yaver session
221
- * and enables Dogfood UX without the host app implementing OAuth. */
243
+ /** One-call account-bound Dogfood bootstrap for third-party apps. The host
244
+ * app needs no auth backend of its own: SDK OAuth supplies the full Yaver
245
+ * account, then this creates/proves the installation key. Owner approval
246
+ * binds that account + appId + phone key before a scoped session is minted. */
222
247
  static enableDeviceDogfood(options: DeviceDogfoodOptions): Promise<{
223
248
  status: DeviceDogfoodState;
224
249
  installationId: string;