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/dist/auth.d.ts CHANGED
@@ -47,6 +47,13 @@ export declare function saveUser(user: User): Promise<void>;
47
47
  export declare function getSelectedDeviceId(): Promise<string | null>;
48
48
  export declare function saveSelectedDeviceId(deviceId: string): Promise<void>;
49
49
  export declare function clearSelectedDeviceId(): Promise<void>;
50
+ /** Verify that this full Yaver session owns the exact registered Dogfood app.
51
+ * Any login is not enough, and installation-scoped SDK tokens return false. */
52
+ export declare function getDogfoodAccountAccess(appId: string, token: string, installationId?: string): Promise<{
53
+ authenticated: boolean;
54
+ ownerAuthorized: boolean;
55
+ installationAuthorized: boolean;
56
+ }>;
50
57
  export declare function validateToken(token: string): Promise<User | null>;
51
58
  /**
52
59
  * Sign in with Apple using the native ASAuthorization flow. Requires
package/dist/auth.js CHANGED
@@ -31,6 +31,7 @@ exports.saveUser = saveUser;
31
31
  exports.getSelectedDeviceId = getSelectedDeviceId;
32
32
  exports.saveSelectedDeviceId = saveSelectedDeviceId;
33
33
  exports.clearSelectedDeviceId = clearSelectedDeviceId;
34
+ exports.getDogfoodAccountAccess = getDogfoodAccountAccess;
34
35
  exports.validateToken = validateToken;
35
36
  exports.signInWithApple = signInWithApple;
36
37
  exports.signInWithOAuth = signInWithOAuth;
@@ -186,6 +187,33 @@ async function clearSelectedDeviceId() {
186
187
  // best effort
187
188
  }
188
189
  }
190
+ /** Verify that this full Yaver session owns the exact registered Dogfood app.
191
+ * Any login is not enough, and installation-scoped SDK tokens return false. */
192
+ async function getDogfoodAccountAccess(appId, token, installationId) {
193
+ try {
194
+ const controller = new AbortController();
195
+ const timeout = setTimeout(() => controller.abort(), 5000);
196
+ const query = new URLSearchParams({ appId });
197
+ if (installationId)
198
+ query.set('installationId', installationId);
199
+ const response = await fetch(`${convexSiteUrl}/dogfood/access?${query.toString()}`, {
200
+ headers: { Authorization: `Bearer ${token}` },
201
+ signal: controller.signal,
202
+ });
203
+ clearTimeout(timeout);
204
+ if (!response.ok)
205
+ return { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
206
+ const result = await response.json();
207
+ return {
208
+ authenticated: result?.authenticated === true,
209
+ ownerAuthorized: result?.ownerAuthorized === true,
210
+ installationAuthorized: result?.installationAuthorized === true,
211
+ };
212
+ }
213
+ catch {
214
+ return { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
215
+ }
216
+ }
189
217
  // ─── Token validation ──────────────────────────────────────────────────
190
218
  async function validateToken(token) {
191
219
  try {
@@ -9,6 +9,9 @@ export interface DeviceDogfoodOptions {
9
9
  appId: string;
10
10
  label?: string;
11
11
  backendUrl?: string;
12
+ /** Full Yaver OAuth token. Required for enrollment; never persisted with the
13
+ * installation private key. The backend binds its user to this key. */
14
+ authToken?: string;
12
15
  /** Advanced bare-RN integration. Expo apps use SecureStore automatically. */
13
16
  secureStore?: SecureStoreLike;
14
17
  }
@@ -22,8 +25,9 @@ export interface DeviceDogfoodSession {
22
25
  projectSlug?: string;
23
26
  targetDeviceId?: string;
24
27
  }
25
- /** Account-free third-party Dogfood enrollment. The UUID is a public lookup
26
- * handle; possession is proven by the private key retained in this app. */
28
+ /** Account-bound third-party Dogfood enrollment. The installation ID is a
29
+ * public lookup handle; possession is proven by the private key retained in
30
+ * this app, while enrollment is bound to the signed-in Yaver account. */
27
31
  export declare class YaverDeviceDogfood {
28
32
  private readonly options;
29
33
  private readonly backendUrl;
@@ -47,8 +47,9 @@ function enrollmentMessage(appId, installationId, challenge) {
47
47
  function sessionMessage(appId, installationId, challenge) {
48
48
  return new TextEncoder().encode(`yaver-dogfood-session-v1\n${appId}\n${installationId}\n${challenge}`);
49
49
  }
50
- /** Account-free third-party Dogfood enrollment. The UUID is a public lookup
51
- * handle; possession is proven by the private key retained in this app. */
50
+ /** Account-bound third-party Dogfood enrollment. The installation ID is a
51
+ * public lookup handle; possession is proven by the private key retained in
52
+ * this app, while enrollment is bound to the signed-in Yaver account. */
52
53
  class YaverDeviceDogfood {
53
54
  constructor(options) {
54
55
  this.options = options;
@@ -83,9 +84,11 @@ class YaverDeviceDogfood {
83
84
  return { appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey };
84
85
  }
85
86
  async enroll(platform = 'unknown') {
87
+ if (!this.options.authToken)
88
+ throw new Error('Sign in to Yaver before registering this device for Dogfood.');
86
89
  const identity = await this.identity();
87
90
  const started = await responseJSON(await fetch(`${this.backendUrl}/dogfood/enroll/start`, {
88
- method: 'POST', headers: { 'Content-Type': 'application/json' },
91
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.options.authToken}` },
89
92
  body: JSON.stringify({ appId: this.options.appId, installationId: identity.installationId, registrationSlot: identity.registrationSlot, publicKey: identity.publicKey, platform, label: this.options.label }),
90
93
  }));
91
94
  if (started.status === 'active')
@@ -1,3 +1,19 @@
1
+ export interface DogfoodAccessSnapshot {
2
+ appId: string;
3
+ /** A Yaver session exists locally; not itself an authorization decision. */
4
+ yaverAuthenticated: boolean;
5
+ /** Backend-confirmed owner/maintainer of this exact appId. */
6
+ ownerAuthorized: boolean;
7
+ installationId?: string;
8
+ deviceState: 'unknown' | 'unregistered' | 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
9
+ /** True only for a backend-approved device key or an authenticated owner. */
10
+ authorized: boolean;
11
+ }
12
+ export interface DogfoodFlowSnapshot {
13
+ phase: 'idle' | 'denied' | 'auth-required' | 'machine-required' | 'opening' | 'error';
14
+ appId?: string;
15
+ error?: string;
16
+ }
1
17
  export interface SDKDogfoodConfig {
2
18
  /** Explicit opt-in. Omitted/false keeps the normal Feedback SDK. */
3
19
  enabled?: boolean;
@@ -5,7 +21,7 @@ export interface SDKDogfoodConfig {
5
21
  accountIds?: string[];
6
22
  /** The third-party app's currently authenticated account ID. */
7
23
  currentAccountId?: string;
8
- /** Account-free installation identity resolved by YaverDeviceDogfood. */
24
+ /** Account-bound installation identity resolved by YaverDeviceDogfood. */
9
25
  appId?: string;
10
26
  installationId?: string;
11
27
  installationStatus?: 'pending' | 'active' | 'cancelled' | 'revoked' | 'superseded';
@@ -13,6 +29,21 @@ export interface SDKDogfoodConfig {
13
29
  onExit?: () => void | Promise<void>;
14
30
  /** Optional app label shown beside Dogfood mode. */
15
31
  label?: string;
32
+ /** Project/framework hints used by the zero-orchestration onboarding UI. */
33
+ projectName?: string;
34
+ framework?: string;
35
+ /** Advanced override for staging/self-hosted enrollment. */
36
+ backendUrl?: string;
37
+ /** Presentation-only ACL hook, e.g. `() => user.isAdmin`. Server-side OAuth
38
+ * or device-signature verification remains the authority boundary. */
39
+ canShow?: (access: DogfoodAccessSnapshot) => boolean | Promise<boolean>;
40
+ /** Optional callback alternative to onDogfoodFlowState(). */
41
+ onStateChange?: (state: DogfoodFlowSnapshot) => void;
42
+ /** ACL-backed Home Screen/App Shortcut. Dynamic and absent until Yaver
43
+ * owner auth or this installation's approved key authorizes it. */
44
+ appShortcut?: boolean | {
45
+ label?: string;
46
+ };
16
47
  }
17
48
  export interface SDKDogfoodStatus {
18
49
  active: boolean;
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * ```
29
29
  */
30
30
  export { YaverFeedback } from './YaverFeedback';
31
- export type { DogfoodOnboardingOptions } from './YaverFeedback';
31
+ export type { DogfoodOnboardingOptions, DogfoodFlowPhase, DogfoodFlowState } from './YaverFeedback';
32
32
  export { captureStoreScreenshots } from './storeShots';
33
33
  export type { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult, StoreShotFrame, } from './storeShots';
34
34
  export { BlackBox } from './BlackBox';
@@ -54,7 +54,7 @@ export { FloatingButton } from './FloatingButton';
54
54
  export { YaverModeBadge, hideYaverModeBadge, showYaverModeBadge, isYaverModeBadgeHidden } from './YaverModeBadge';
55
55
  export type { YaverModeBadgeProps } from './YaverModeBadge';
56
56
  export { resolveSDKDogfood } from './dogfoodPolicy';
57
- export type { SDKDogfoodConfig, SDKDogfoodStatus } from './dogfoodPolicy';
57
+ export type { DogfoodAccessSnapshot, DogfoodFlowSnapshot, SDKDogfoodConfig, SDKDogfoodStatus } from './dogfoodPolicy';
58
58
  export { YaverDeviceDogfood } from './deviceDogfood';
59
59
  export type { DeviceDogfoodOptions, DeviceDogfoodSession, DeviceDogfoodState } from './deviceDogfood';
60
60
  export { DogfoodController, DogfoodRuntimeError, defaultDogfoodLane, dogfoodLaneOptions, dogfoodLogLinesFromDevEvent, runtimeLogLinesFromDevEvent, validateDogfoodProject, } from './DogfoodRuntime';
@@ -63,8 +63,8 @@ export { FeedbackModal } from './FeedbackModal';
63
63
  export { QuickActionIcon } from './QuickActionIcon';
64
64
  export type { QuickActionIconProps } from './QuickActionIcon';
65
65
  export { FixReport } from './FixReport';
66
- export { getQuickIconDisabled, setQuickIconDisabled, clearQuickIconDisabled, } from './preferences';
67
- export { configureAuthEndpoints, getConvexSiteUrl, getWebBaseUrl, getToken, saveToken, clearToken, getUser, saveUser, getSelectedDeviceId, saveSelectedDeviceId, clearSelectedDeviceId, validateToken, signInWithApple, signInWithOAuth, signupWithEmail, loginWithEmail, listReachableDevices, DEFAULT_CONVEX_SITE_URL, DEFAULT_WEB_BASE_URL, DEFAULT_OAUTH_REDIRECT, } from './auth';
66
+ export { getQuickIconDisabled, setQuickIconDisabled, clearQuickIconDisabled, getPreferredDogfoodLane, setPreferredDogfoodLane, } from './preferences';
67
+ export { configureAuthEndpoints, getConvexSiteUrl, getWebBaseUrl, getToken, saveToken, clearToken, getUser, saveUser, getSelectedDeviceId, getDogfoodAccountAccess, saveSelectedDeviceId, clearSelectedDeviceId, validateToken, signInWithApple, signInWithOAuth, signupWithEmail, loginWithEmail, listReachableDevices, DEFAULT_CONVEX_SITE_URL, DEFAULT_WEB_BASE_URL, DEFAULT_OAUTH_REDIRECT, } from './auth';
68
68
  export type { OAuthProvider, User, RemoteDevice, DeviceList, } from './auth';
69
69
  export { captureScreenshot, pickFeedbackFile, startVideoRecording, stopVideoRecording, isVideoRecording, } from './capture';
70
70
  export { uploadFeedback } from './upload';
package/dist/index.js CHANGED
@@ -29,8 +29,8 @@
29
29
  * ```
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.FeedbackModal = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
33
- exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = void 0;
32
+ exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.setPreferredDogfoodLane = exports.getPreferredDogfoodLane = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.FeedbackModal = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
33
+ exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getDogfoodAccountAccess = exports.getSelectedDeviceId = void 0;
34
34
  var YaverFeedback_1 = require("./YaverFeedback");
35
35
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
36
36
  var storeShots_1 = require("./storeShots");
@@ -98,6 +98,8 @@ var preferences_1 = require("./preferences");
98
98
  Object.defineProperty(exports, "getQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.getQuickIconDisabled; } });
99
99
  Object.defineProperty(exports, "setQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.setQuickIconDisabled; } });
100
100
  Object.defineProperty(exports, "clearQuickIconDisabled", { enumerable: true, get: function () { return preferences_1.clearQuickIconDisabled; } });
101
+ Object.defineProperty(exports, "getPreferredDogfoodLane", { enumerable: true, get: function () { return preferences_1.getPreferredDogfoodLane; } });
102
+ Object.defineProperty(exports, "setPreferredDogfoodLane", { enumerable: true, get: function () { return preferences_1.setPreferredDogfoodLane; } });
101
103
  var auth_1 = require("./auth");
102
104
  Object.defineProperty(exports, "configureAuthEndpoints", { enumerable: true, get: function () { return auth_1.configureAuthEndpoints; } });
103
105
  Object.defineProperty(exports, "getConvexSiteUrl", { enumerable: true, get: function () { return auth_1.getConvexSiteUrl; } });
@@ -108,6 +110,7 @@ Object.defineProperty(exports, "clearToken", { enumerable: true, get: function (
108
110
  Object.defineProperty(exports, "getUser", { enumerable: true, get: function () { return auth_1.getUser; } });
109
111
  Object.defineProperty(exports, "saveUser", { enumerable: true, get: function () { return auth_1.saveUser; } });
110
112
  Object.defineProperty(exports, "getSelectedDeviceId", { enumerable: true, get: function () { return auth_1.getSelectedDeviceId; } });
113
+ Object.defineProperty(exports, "getDogfoodAccountAccess", { enumerable: true, get: function () { return auth_1.getDogfoodAccountAccess; } });
111
114
  Object.defineProperty(exports, "saveSelectedDeviceId", { enumerable: true, get: function () { return auth_1.saveSelectedDeviceId; } });
112
115
  Object.defineProperty(exports, "clearSelectedDeviceId", { enumerable: true, get: function () { return auth_1.clearSelectedDeviceId; } });
113
116
  Object.defineProperty(exports, "validateToken", { enumerable: true, get: function () { return auth_1.validateToken; } });
@@ -31,3 +31,5 @@ export declare function getPreferredRunner(): Promise<string | null>;
31
31
  export declare function setPreferredRunner(runner: string | null): Promise<void>;
32
32
  export declare function getPreferredModel(): Promise<string | null>;
33
33
  export declare function setPreferredModel(model: string | null): Promise<void>;
34
+ export declare function getPreferredDogfoodLane(appId: string): Promise<'browser' | 'hermes' | 'webrtc' | null>;
35
+ export declare function setPreferredDogfoodLane(appId: string, lane: 'browser' | 'hermes' | 'webrtc'): Promise<void>;
@@ -25,6 +25,8 @@ exports.getPreferredRunner = getPreferredRunner;
25
25
  exports.setPreferredRunner = setPreferredRunner;
26
26
  exports.getPreferredModel = getPreferredModel;
27
27
  exports.setPreferredModel = setPreferredModel;
28
+ exports.getPreferredDogfoodLane = getPreferredDogfoodLane;
29
+ exports.setPreferredDogfoodLane = setPreferredDogfoodLane;
28
30
  let AsyncStorage = null;
29
31
  try {
30
32
  AsyncStorage = require('@react-native-async-storage/async-storage').default;
@@ -149,6 +151,7 @@ async function clearQuickIconColorPreset() {
149
151
  // run picks whatever's signed-in via getRunnerStatus().)
150
152
  const PREFERRED_RUNNER_KEY = 'yaver_feedback_preferred_runner';
151
153
  const PREFERRED_MODEL_KEY = 'yaver_feedback_preferred_model';
154
+ const PREFERRED_DOGFOOD_LANE_PREFIX = 'yaver_feedback_dogfood_lane_';
152
155
  async function getPreferredRunner() {
153
156
  if (!AsyncStorage)
154
157
  return null;
@@ -199,3 +202,24 @@ async function setPreferredModel(model) {
199
202
  /* best-effort */
200
203
  }
201
204
  }
205
+ async function getPreferredDogfoodLane(appId) {
206
+ if (!AsyncStorage || !appId)
207
+ return null;
208
+ try {
209
+ const value = await AsyncStorage.getItem(`${PREFERRED_DOGFOOD_LANE_PREFIX}${appId}`);
210
+ return value === 'browser' || value === 'hermes' || value === 'webrtc' ? value : null;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ async function setPreferredDogfoodLane(appId, lane) {
217
+ if (!AsyncStorage || !appId)
218
+ return;
219
+ try {
220
+ await AsyncStorage.setItem(`${PREFERRED_DOGFOOD_LANE_PREFIX}${appId}`, lane);
221
+ }
222
+ catch {
223
+ /* best-effort */
224
+ }
225
+ }
@@ -13,4 +13,12 @@ RCT_EXTERN_METHOD(hasBundle:(RCTPromiseResolveBlock)resolve
13
13
  RCT_EXTERN_METHOD(clearBundle:(RCTPromiseResolveBlock)resolve
14
14
  rejecter:(RCTPromiseRejectBlock)reject)
15
15
 
16
+ RCT_EXTERN_METHOD(setDogfoodShortcut:(BOOL)enabled
17
+ label:(NSString *)label
18
+ resolver:(RCTPromiseResolveBlock)resolve
19
+ rejecter:(RCTPromiseRejectBlock)reject)
20
+
21
+ RCT_EXTERN_METHOD(consumeDogfoodShortcut:(RCTPromiseResolveBlock)resolve
22
+ rejecter:(RCTPromiseRejectBlock)reject)
23
+
16
24
  @end
@@ -18,6 +18,8 @@ class YaverHotReload: NSObject {
18
18
  static let bundleDir = "yaver-hot-reload"
19
19
  static let bundleFile = "main.jsbundle"
20
20
  static let reloadNotification = Notification.Name("YaverHotReloadBundle")
21
+ static let dogfoodShortcutType = "io.yaver.feedback.dogfood"
22
+ static let dogfoodShortcutPendingKey = "yaverDogfoodShortcutPending"
21
23
 
22
24
  // `requiresMainQueueSetup` is an RCTBridgeModule protocol method,
23
25
  // not an NSObject method — so it must not be marked `override`.
@@ -101,6 +103,41 @@ class YaverHotReload: NSObject {
101
103
  resolve(true)
102
104
  }
103
105
 
106
+ /** Dynamic (ACL-backed) Home Screen quick action. Never put this in
107
+ * Info.plist: static shortcuts are visible before account authorization. */
108
+ @objc func setDogfoodShortcut(_ enabled: Bool,
109
+ label: String,
110
+ resolver resolve: @escaping RCTPromiseResolveBlock,
111
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
112
+ DispatchQueue.main.async {
113
+ var items = UIApplication.shared.shortcutItems ?? []
114
+ items.removeAll { $0.type == YaverHotReload.dogfoodShortcutType }
115
+ if enabled {
116
+ items.append(UIApplicationShortcutItem(
117
+ type: YaverHotReload.dogfoodShortcutType,
118
+ localizedTitle: label.isEmpty ? "Dogfood" : label,
119
+ localizedSubtitle: nil,
120
+ icon: UIApplicationShortcutIcon(type: .play),
121
+ userInfo: nil
122
+ ))
123
+ }
124
+ UIApplication.shared.shortcutItems = items
125
+ resolve(enabled)
126
+ }
127
+ }
128
+
129
+ @objc func consumeDogfoodShortcut(_ resolve: RCTPromiseResolveBlock,
130
+ rejecter reject: RCTPromiseRejectBlock) {
131
+ let defaults = UserDefaults.standard
132
+ let pending = defaults.bool(forKey: YaverHotReload.dogfoodShortcutPendingKey)
133
+ if pending { defaults.removeObject(forKey: YaverHotReload.dogfoodShortcutPendingKey) }
134
+ resolve(pending)
135
+ }
136
+
137
+ @objc static func markDogfoodShortcutPending() {
138
+ UserDefaults.standard.set(true, forKey: dogfoodShortcutPendingKey)
139
+ }
140
+
104
141
  // MARK: - Static helpers
105
142
 
106
143
  static func savedBundlePath() -> URL {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.9.4",
3
+ "version": "0.9.7",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -79,7 +79,7 @@
79
79
  "typescript": "^5.0.0"
80
80
  },
81
81
  "scripts": {
82
- "build": "rm -rf dist && (tsc -p tsconfig.json || true) && test -f dist/index.js",
82
+ "build": "rm -rf dist && tsc -p tsconfig.json && test -f dist/index.js",
83
83
  "prepublishOnly": "npm run build",
84
84
  "test": "jest --runInBand",
85
85
  "test:ci": "npm run build && npm test"
@@ -56,13 +56,22 @@ export const AuthOverlay: React.FC = () => {
56
56
  const handleLoggedIn = async (newToken: string) => {
57
57
  setToken(newToken);
58
58
  await YaverFeedback.setAuthToken(newToken);
59
- openPicker();
59
+ if (YaverFeedback.getDogfoodOnboarding()) {
60
+ closeAll();
61
+ await YaverFeedback.continueDogfoodOnboarding();
62
+ } else {
63
+ openPicker();
64
+ }
60
65
  };
61
66
 
62
67
  const handleDevicePicked = async (device: RemoteDevice) => {
63
68
  await YaverFeedback.setPreferredDevice(device.deviceId);
64
69
  closeAll();
65
- DeviceEventEmitter.emit('yaverFeedback:startReport');
70
+ if (YaverFeedback.getDogfoodOnboarding()) {
71
+ await YaverFeedback.continueDogfoodOnboarding();
72
+ } else {
73
+ DeviceEventEmitter.emit('yaverFeedback:startReport');
74
+ }
66
75
  };
67
76
 
68
77
  return (
@@ -47,8 +47,10 @@ import {
47
47
  QuickIconColorPreset,
48
48
  getPreferredModel,
49
49
  getPreferredRunner,
50
+ getPreferredDogfoodLane,
50
51
  setPreferredModel,
51
52
  setPreferredRunner,
53
+ setPreferredDogfoodLane,
52
54
  } from './preferences';
53
55
  import {
54
56
  DogfoodController,
@@ -323,7 +325,16 @@ export const FeedbackModal: React.FC = () => {
323
325
  || projects[0]
324
326
  || null;
325
327
  setDogfoodProject((current) => current && projects.some((item) => item.path === current.path) ? current : preferred);
326
- if (preferred) setDogfoodLane(defaultDogfoodLane(preferred.framework || onboarding.framework || 'expo'));
328
+ if (preferred) {
329
+ const framework = preferred.framework || onboarding.framework || 'expo';
330
+ const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(preferred.path, framework).catch(() => null);
331
+ const nativeRuntimeAvailable = !!capabilities?.targets.some((target) => target.enabled && target.id !== 'browser-window');
332
+ if (mountedRef.current) setDogfoodNativeAvailable(nativeRuntimeAvailable);
333
+ const savedLane = await getPreferredDogfoodLane(onboarding.appId);
334
+ const savedSupported = dogfoodLaneOptions(framework, { nativeRuntimeAvailable })
335
+ .some((option) => option.lane === savedLane && option.supported);
336
+ setDogfoodLane(savedLane && savedSupported ? savedLane : defaultDogfoodLane(framework));
337
+ }
327
338
  } catch (cause) {
328
339
  if (mountedRef.current) setDogfoodEnrollment({ status: 'failed', error: cause instanceof Error ? cause.message : String(cause) });
329
340
  } finally {
@@ -551,8 +562,12 @@ export const FeedbackModal: React.FC = () => {
551
562
  useEffect(() => {
552
563
  mountedRef.current = true;
553
564
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
554
- if (YaverFeedback.isEnabled()) {
555
- const onboarding = YaverFeedback.getDogfoodOnboarding();
565
+ const onboarding = YaverFeedback.getDogfoodOnboarding();
566
+ // Explicit Dogfood remains usable when passive capture/shake is off.
567
+ // The host still owns visibility; server OAuth/device signatures own
568
+ // authority. Keeping this event path independent avoids toggling the
569
+ // user's feedback preference merely to open Developer Mode.
570
+ if (YaverFeedback.isEnabled() || onboarding) {
556
571
  const directDogfood = YaverFeedback.getDogfoodStatus().active;
557
572
  setDogfoodActive(directDogfood);
558
573
  setVisible(true);
@@ -1127,6 +1142,14 @@ export const FeedbackModal: React.FC = () => {
1127
1142
  const needsAuthRunnerCount = runnerCards.filter(
1128
1143
  (row) => row.installed && !row.authConfigured && !row.ready,
1129
1144
  ).length;
1145
+ const selectedDogfoodRunner = preferredRunner
1146
+ ? runnerCards.find((row) => row.id === preferredRunner) ?? null
1147
+ : null;
1148
+ const dogfoodRunnerReady = !!selectedDogfoodRunner
1149
+ && (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
1150
+ const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1151
+ || !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
1152
+ const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady;
1130
1153
 
1131
1154
  // Once the user fires off a vibe task, swap the entire modal body
1132
1155
  // for the live chat screen. The chat manages its own SSE
@@ -1302,6 +1325,52 @@ export const FeedbackModal: React.FC = () => {
1302
1325
  </Pressable>
1303
1326
  ))}
1304
1327
  </ScrollView>
1328
+ <Text style={styles.dogfoodStepLabel}>Coding agent</Text>
1329
+ <View style={styles.dogfoodChoiceRow}>
1330
+ {runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (
1331
+ <Pressable
1332
+ key={row.id}
1333
+ onPress={() => {
1334
+ const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
1335
+ setPreferredRunnerState(row.id);
1336
+ setPreferredModelState(nextModel);
1337
+ void setPreferredRunner(row.id);
1338
+ void setPreferredModel(nextModel || null);
1339
+ }}
1340
+ style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]}
1341
+ accessibilityRole="button"
1342
+ accessibilityState={{ selected: preferredRunner === row.id }}
1343
+ accessibilityLabel={`Use ${row.name} for Dogfood`}
1344
+ >
1345
+ <Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</Text>
1346
+ </Pressable>
1347
+ ))}
1348
+ </View>
1349
+ {readyRunnerCount === 0 ? (
1350
+ <Text style={styles.dogfoodWizardHint}>Sign in or configure a coding agent under Coding Agents below.</Text>
1351
+ ) : null}
1352
+ {selectedDogfoodRunner?.models?.length ? (
1353
+ <>
1354
+ <Text style={styles.dogfoodStepLabel}>Model</Text>
1355
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
1356
+ {selectedDogfoodRunner.models.map((model) => (
1357
+ <Pressable
1358
+ key={model.id}
1359
+ onPress={() => {
1360
+ setPreferredModelState(model.id);
1361
+ void setPreferredModel(model.id);
1362
+ }}
1363
+ style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]}
1364
+ accessibilityRole="button"
1365
+ accessibilityState={{ selected: preferredModel === model.id }}
1366
+ accessibilityLabel={`Use ${model.name || model.id} model for Dogfood`}
1367
+ >
1368
+ <Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</Text>
1369
+ </Pressable>
1370
+ ))}
1371
+ </ScrollView>
1372
+ </>
1373
+ ) : null}
1305
1374
  <Text style={styles.dogfoodStepLabel}>Runtime lane</Text>
1306
1375
  <View style={styles.dogfoodChoiceRow}>
1307
1376
  {dogfoodLaneOptions(
@@ -1310,7 +1379,12 @@ export const FeedbackModal: React.FC = () => {
1310
1379
  ).map((option) => (
1311
1380
  <Pressable
1312
1381
  key={option.lane}
1313
- onPress={() => option.supported && setDogfoodLane(option.lane)}
1382
+ onPress={() => {
1383
+ if (!option.supported) return;
1384
+ setDogfoodLane(option.lane);
1385
+ const appId = YaverFeedback.getDogfoodOnboarding()?.appId;
1386
+ if (appId) void setPreferredDogfoodLane(appId, option.lane);
1387
+ }}
1314
1388
  style={[
1315
1389
  styles.dogfoodChoice,
1316
1390
  dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
@@ -1323,13 +1397,13 @@ export const FeedbackModal: React.FC = () => {
1323
1397
  ))}
1324
1398
  </View>
1325
1399
  <Text style={styles.dogfoodWizardHint}>
1326
- Runner: {[preferredRunner || 'automatic', preferredModel].filter(Boolean).join(' · ')}. Change it under Coding Agents below.
1400
+ {[preferredRunner || 'Choose a coding agent', preferredModel].filter(Boolean).join(' · ')}
1327
1401
  </Text>
1328
1402
  <ActionRow
1329
1403
  label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'}
1330
1404
  tint="#818cf8"
1331
1405
  onPress={() => void startDogfoodRuntime()}
1332
- disabled={!dogfoodProject || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))}
1406
+ disabled={dogfoodStartBlocked || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))}
1333
1407
  busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}
1334
1408
  />
1335
1409
  {dogfoodRuntime ? (
@@ -1534,6 +1608,22 @@ export const FeedbackModal: React.FC = () => {
1534
1608
  </View>
1535
1609
  )}
1536
1610
 
1611
+ {YaverFeedback.isAuthed() ? (
1612
+ <Pressable
1613
+ onPress={() => {
1614
+ void YaverFeedback.signOut().then(() => {
1615
+ handleClose();
1616
+ YaverFeedback.showLogin();
1617
+ });
1618
+ }}
1619
+ style={({ pressed }) => [styles.yaverSignOutBtn, pressed && styles.buttonPressed]}
1620
+ accessibilityRole="button"
1621
+ accessibilityLabel="Sign out of Yaver"
1622
+ >
1623
+ <Text style={styles.yaverSignOutText}>Sign out of Yaver</Text>
1624
+ </Pressable>
1625
+ ) : null}
1626
+
1537
1627
  <View style={styles.iconSelector}>
1538
1628
  <Text style={styles.iconSelectorTitle}>Quick Icon Color</Text>
1539
1629
  <Text style={styles.iconSelectorText}>
@@ -1803,6 +1893,8 @@ const styles = StyleSheet.create({
1803
1893
  dogfoodConsoleError: { color: '#fca5a5', fontSize: 11, lineHeight: 16, marginTop: 5 },
1804
1894
  dogfoodOpenPreview: { alignSelf: 'flex-start', borderRadius: 9, paddingHorizontal: 11, paddingVertical: 8, marginTop: 6, backgroundColor: '#6555df' },
1805
1895
  dogfoodOpenPreviewText: { color: '#fff', fontSize: 12, fontWeight: '800' },
1896
+ yaverSignOutBtn: { alignSelf: 'flex-start', paddingHorizontal: 4, paddingVertical: 8 },
1897
+ yaverSignOutText: { color: '#b42318', fontSize: 13, fontWeight: '700' },
1806
1898
  reloadRow: {
1807
1899
  gap: 4,
1808
1900
  },