yaver-feedback-react-native 0.9.6 → 0.9.8

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.
@@ -44,12 +44,14 @@ const AuthOverlay_1 = require("./AuthOverlay");
44
44
  const QuickActionIcon_1 = require("./QuickActionIcon");
45
45
  const YaverModeBadge_1 = require("./YaverModeBadge");
46
46
  const VibeChatScreen_1 = require("./VibeChatScreen");
47
+ const DogfoodQuickControls_1 = require("./DogfoodQuickControls");
47
48
  const DeployPanel_1 = require("./DeployPanel");
48
49
  const auth_1 = require("./auth");
49
50
  const reloadActions_1 = require("./reloadActions");
50
51
  const preferences_1 = require("./preferences");
51
52
  const DogfoodRuntime_1 = require("./DogfoodRuntime");
52
53
  const P2PDogfoodDriver_1 = require("./P2PDogfoodDriver");
54
+ const DogfoodSessionUi_1 = require("./DogfoodSessionUi");
53
55
  const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'];
54
56
  function normalizeRunnerStatusRows(rows) {
55
57
  const byId = new Map();
@@ -1023,7 +1025,42 @@ const FeedbackModal = () => {
1023
1025
  && (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
1024
1026
  const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
1025
1027
  || !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
1026
- const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady;
1028
+ const dogfoodLaneChoices = (0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable });
1029
+ const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported);
1030
+ const dogfoodReadinessSteps = [
1031
+ {
1032
+ key: 'oauth', label: 'Yaver OAuth',
1033
+ detail: YaverFeedback_1.YaverFeedback.isAuthed() ? 'Signed in · session saved' : 'Sign in required',
1034
+ tone: YaverFeedback_1.YaverFeedback.isAuthed() ? 'ready' : 'attention',
1035
+ },
1036
+ {
1037
+ key: 'machine', label: 'Remote PC',
1038
+ detail: machineCard.device ? machineCard.title : 'Choose a reachable development machine',
1039
+ tone: machineCard.device && machineCard.status === 'live' ? 'ready' : 'attention',
1040
+ },
1041
+ {
1042
+ key: 'installation', label: 'This installation',
1043
+ detail: dogfoodEnrollment?.status === 'active' ? 'Device key approved' : dogfoodEnrollment?.status || 'Checking device key',
1044
+ tone: dogfoodEnrollment?.status === 'active' ? 'ready'
1045
+ : dogfoodEnrollment?.status === 'failed' ? 'blocked' : 'pending',
1046
+ },
1047
+ {
1048
+ key: 'runner', label: 'Runner',
1049
+ detail: dogfoodRunnerReady ? selectedDogfoodRunner?.name || preferredRunner || 'Ready' : 'Choose or configure a coding runner',
1050
+ tone: dogfoodRunnerReady ? 'ready' : 'attention',
1051
+ },
1052
+ {
1053
+ key: 'model', label: 'Model',
1054
+ detail: dogfoodModelReady ? preferredModel || 'Runner default' : 'Choose a model',
1055
+ tone: dogfoodModelReady ? 'ready' : 'attention',
1056
+ },
1057
+ {
1058
+ key: 'lane', label: 'Runtime lane',
1059
+ detail: dogfoodLaneChoices.find((option) => option.lane === dogfoodLane)?.label || dogfoodLane,
1060
+ tone: dogfoodLaneReady ? 'ready' : 'blocked',
1061
+ },
1062
+ ];
1063
+ const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady || !dogfoodLaneReady;
1027
1064
  // Once the user fires off a vibe task, swap the entire modal body
1028
1065
  // for the live chat screen. The chat manages its own SSE
1029
1066
  // subscription, multi-turn follow-ups, and Reload button. Closing
@@ -1032,6 +1069,7 @@ const FeedbackModal = () => {
1032
1069
  const client = YaverFeedback_1.YaverFeedback.getP2PClient();
1033
1070
  return (<>
1034
1071
  <AuthOverlay_1.AuthOverlay />
1072
+ <DogfoodQuickControls_1.DogfoodQuickControls />
1035
1073
  <QuickActionIcon_1.QuickActionIcon />
1036
1074
  <YaverModeBadgeGate />
1037
1075
  <react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
@@ -1049,6 +1087,7 @@ const FeedbackModal = () => {
1049
1087
  }
1050
1088
  return (<>
1051
1089
  <AuthOverlay_1.AuthOverlay />
1090
+ <DogfoodQuickControls_1.DogfoodQuickControls />
1052
1091
  <QuickActionIcon_1.QuickActionIcon />
1053
1092
  <YaverModeBadgeGate />
1054
1093
  {visible && (<react_native_1.Modal visible={visible} animationType="slide" transparent onRequestClose={handleClose}>
@@ -1101,6 +1140,7 @@ const FeedbackModal = () => {
1101
1140
  <react_native_1.Text style={styles.dogfoodWizardHint}>
1102
1141
  OAuth ✓ · machine {machineCard.device ? '✓' : 'required'} · installation {dogfoodEnrollment?.status || 'checking'}
1103
1142
  </react_native_1.Text>
1143
+ <DogfoodSessionUi_1.DogfoodStatusRail steps={dogfoodReadinessSteps}/>
1104
1144
  {dogfoodEnrollment?.installationId ? (<react_native_1.Text selectable style={styles.dogfoodInstallationId}>
1105
1145
  This device · {dogfoodEnrollment.installationId}
1106
1146
  </react_native_1.Text>) : null}
@@ -1148,34 +1188,22 @@ const FeedbackModal = () => {
1148
1188
  </react_native_1.ScrollView>
1149
1189
  </>) : null}
1150
1190
  <react_native_1.Text style={styles.dogfoodStepLabel}>Runtime lane</react_native_1.Text>
1151
- <react_native_1.View style={styles.dogfoodChoiceRow}>
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={[
1160
- styles.dogfoodChoice,
1161
- dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
1162
- !option.supported && styles.actionBtnDisabled,
1163
- ]} accessibilityState={{ disabled: !option.supported, selected: dogfoodLane === option.lane }}>
1164
- <react_native_1.Text style={[styles.dogfoodChoiceText, dogfoodLane === option.lane && styles.dogfoodChoiceTextSelected]}>{option.label}</react_native_1.Text>
1165
- </react_native_1.Pressable>))}
1166
- </react_native_1.View>
1191
+ <DogfoodSessionUi_1.DogfoodLanePicker options={dogfoodLaneChoices} selected={dogfoodLane} onSelect={(lane) => {
1192
+ setDogfoodLane(lane);
1193
+ const appId = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.appId;
1194
+ if (appId)
1195
+ void (0, preferences_1.setPreferredDogfoodLane)(appId, lane);
1196
+ }}/>
1167
1197
  <react_native_1.Text style={styles.dogfoodWizardHint}>
1168
1198
  {[preferredRunner || 'Choose a coding agent', preferredModel].filter(Boolean).join(' · ')}
1169
1199
  </react_native_1.Text>
1170
1200
  <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)}/>
1171
- {dogfoodRuntime ? (<react_native_1.View style={styles.dogfoodConsole}>
1172
- <react_native_1.Text style={styles.dogfoodConsoleStatus}>{dogfoodRuntime.message}</react_native_1.Text>
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>))}
1174
- {dogfoodRuntime.failure ? (<react_native_1.Text style={styles.dogfoodConsoleError}>{dogfoodRuntime.failure.error}{'\n'}{dogfoodRuntime.failure.remedy}</react_native_1.Text>) : null}
1201
+ {dogfoodRuntime ? (<>
1202
+ <DogfoodSessionUi_1.DogfoodLiveConsole lane={dogfoodRuntime.project.lane} phase={dogfoodRuntime.phase} message={dogfoodRuntime.message} logs={dogfoodRuntime.logs} failure={dogfoodRuntime.failure}/>
1175
1203
  {dogfoodRuntime.result?.url ? (<react_native_1.Pressable onPress={() => void react_native_1.Linking.openURL(dogfoodRuntime.result.url)} style={styles.dogfoodOpenPreview}>
1176
1204
  <react_native_1.Text style={styles.dogfoodOpenPreviewText}>Open dogfooded app</react_native_1.Text>
1177
1205
  </react_native_1.Pressable>) : null}
1178
- </react_native_1.View>) : null}
1206
+ </>) : null}
1179
1207
  </>)}
1180
1208
  </react_native_1.View>) : null}
1181
1209
  <react_native_1.Pressable onPress={() => {
@@ -1,7 +1,7 @@
1
1
  import { FeedbackConfig, CapturedError } from './types';
2
2
  import { P2PClient } from './P2PClient';
3
3
  import { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult } from './storeShots';
4
- import { QuickIconColorPreset } from './preferences';
4
+ import { type DogfoodControlPresentation, QuickIconColorPreset } from './preferences';
5
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 {
@@ -16,6 +16,25 @@ export interface DogfoodFlowState {
16
16
  appId?: string;
17
17
  error?: string;
18
18
  }
19
+ export interface DogfoodControlTriggerState {
20
+ configured: boolean;
21
+ authorized: boolean;
22
+ appId?: string;
23
+ installationId?: string;
24
+ gestureSupported: boolean;
25
+ gestureEnabled: boolean;
26
+ fallbackVisible: boolean;
27
+ presentation: DogfoodControlPresentation;
28
+ onboardingSeen: boolean;
29
+ reason: string;
30
+ platform?: string;
31
+ }
32
+ interface DogfoodControlSyncOptions {
33
+ presentation?: DogfoodControlPresentation;
34
+ onboardingSeen?: boolean;
35
+ /** User-initiated changes fail visibly when Convex cannot persist them. */
36
+ requirePersistence?: boolean;
37
+ }
19
38
  /**
20
39
  * Main entry point for the Yaver Feedback SDK.
21
40
  * Call `YaverFeedback.init()` once at app startup.
@@ -130,6 +149,20 @@ export declare class YaverFeedback {
130
149
  * owner/device ACL state. Static plist shortcuts are intentionally avoided:
131
150
  * an unauthorized install must never advertise a hidden developer action. */
132
151
  static syncDogfoodAppShortcut(): Promise<boolean>;
152
+ /** Capability-gated in-app Dogfood controls. Every new exact installation
153
+ * starts with Y until Convex records onboarding. A supported standalone app
154
+ * can then select a passive three-finger hold and no persistent pixels;
155
+ * missing native support or accessibility touch exploration keeps Y. Yaver's
156
+ * own container suppresses both because its split preview/chat UI owns them. */
157
+ static syncDogfoodControlGesture(syncOptions?: DogfoodControlSyncOptions): Promise<DogfoodControlTriggerState>;
158
+ /** Complete first-run onboarding or change the later control preference.
159
+ * Convex is authoritative and keyed by full Yaver user + app + installation;
160
+ * the local value is only a cold-start cache. */
161
+ static setDogfoodControlPresentation(presentation: DogfoodControlPresentation): Promise<DogfoodControlTriggerState>;
162
+ /** One-tap fast reload for the compact Dogfood card. It preserves the same
163
+ * selected render machine and bearer-authenticated P2P route as the full
164
+ * Feedback modal, and names missing auth/machine state instead of no-oping. */
165
+ static requestDogfoodFastReload(): Promise<string>;
133
166
  /** Backwards-compatible entry point for existing integrations. */
134
167
  static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): Promise<DogfoodFlowState>;
135
168
  /** Continue after OAuth or machine selection. Public so custom host UI can
@@ -416,3 +449,4 @@ export declare class YaverFeedback {
416
449
  /** Tear down the SDK (stop shake detector, clear state). */
417
450
  static destroy(): void;
418
451
  }
452
+ export {};
@@ -194,6 +194,11 @@ let dogfoodShortcutAppStateSubscription = null;
194
194
  let dogfoodActivationSubscription = null;
195
195
  let dogfoodShortcutLaunchInFlight = false;
196
196
  let lastDogfoodActivationUrl = '';
197
+ function dogfoodControlPreferenceScope(appId, installationId) {
198
+ if (!appId || !installationId)
199
+ return undefined;
200
+ return `${appId}:${installationId}`;
201
+ }
197
202
  function publishDogfoodFlow(state) {
198
203
  dogfoodFlowState = state;
199
204
  try {
@@ -533,20 +538,30 @@ class YaverFeedback {
533
538
  if (config.autoLogin !== false && enabled) {
534
539
  void YaverFeedback.hydrateSession();
535
540
  }
536
- if (config.dogfood?.appShortcut) {
537
- void YaverFeedback.syncDogfoodAppShortcut();
538
- void consumeNativeDogfoodShortcut();
541
+ if (config.dogfood?.appShortcut || config.dogfood?.controlGesture) {
542
+ if (config.dogfood?.appShortcut) {
543
+ void YaverFeedback.syncDogfoodAppShortcut();
544
+ void consumeNativeDogfoodShortcut();
545
+ }
546
+ if (config.dogfood?.controlGesture) {
547
+ void YaverFeedback.syncDogfoodControlGesture();
548
+ }
539
549
  try {
540
550
  const { AppState } = require('react-native');
541
551
  dogfoodShortcutAppStateSubscription?.remove();
542
552
  dogfoodShortcutAppStateSubscription = AppState.addEventListener('change', (state) => {
543
553
  if (state === 'active') {
544
- void YaverFeedback.syncDogfoodAppShortcut();
545
- void consumeNativeDogfoodShortcut();
554
+ if (config?.dogfood?.appShortcut) {
555
+ void YaverFeedback.syncDogfoodAppShortcut();
556
+ void consumeNativeDogfoodShortcut();
557
+ }
558
+ if (config?.dogfood?.controlGesture) {
559
+ void YaverFeedback.syncDogfoodControlGesture();
560
+ }
546
561
  }
547
562
  });
548
563
  }
549
- catch { /* native shortcut unavailable on web/test runtimes */ }
564
+ catch { /* native quick controls unavailable on web/test runtimes */ }
550
565
  }
551
566
  if (config.dogfood) {
552
567
  try {
@@ -855,6 +870,7 @@ class YaverFeedback {
855
870
  YaverFeedback.scheduleBlackBoxAutoStart();
856
871
  }
857
872
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
873
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
858
874
  }
859
875
  /** Returns true once the SDK has a session token it can use. */
860
876
  static isAuthed() {
@@ -1005,16 +1021,21 @@ class YaverFeedback {
1005
1021
  const token = config?.authToken || await (0, auth_1.getToken)();
1006
1022
  const account = token
1007
1023
  ? await (0, auth_1.getDogfoodAccountAccess)(appId, token, installationId)
1008
- : { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
1024
+ : { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
1009
1025
  const yaverAuthenticated = account.authenticated;
1010
1026
  const ownerAuthorized = account.ownerAuthorized;
1011
1027
  return {
1012
1028
  appId,
1013
1029
  yaverAuthenticated,
1014
1030
  ownerAuthorized,
1031
+ accountAuthorized: account.accountAuthorized,
1015
1032
  installationId,
1016
1033
  deviceState,
1017
1034
  authorized: yaverAuthenticated && account.installationAuthorized && deviceState === 'active',
1035
+ controlPresentation: account.controlPresentation,
1036
+ gestureSupported: account.gestureSupported,
1037
+ gestureCapabilityReason: account.gestureCapabilityReason,
1038
+ controlOnboardingSeen: account.controlOnboardingSeen,
1018
1039
  };
1019
1040
  }
1020
1041
  /** Add/remove the platform Home Screen shortcut from backend-authoritative
@@ -1044,6 +1065,243 @@ class YaverFeedback {
1044
1065
  await native.setDogfoodShortcut(visible, label || 'Dogfood');
1045
1066
  return visible;
1046
1067
  }
1068
+ /** Capability-gated in-app Dogfood controls. Every new exact installation
1069
+ * starts with Y until Convex records onboarding. A supported standalone app
1070
+ * can then select a passive three-finger hold and no persistent pixels;
1071
+ * missing native support or accessibility touch exploration keeps Y. Yaver's
1072
+ * own container suppresses both because its split preview/chat UI owns them. */
1073
+ static async syncDogfoodControlGesture(syncOptions = {}) {
1074
+ const configured = config?.dogfood?.controlGesture;
1075
+ const native = react_native_1.NativeModules?.YaverDogfoodGesture;
1076
+ const base = {
1077
+ configured: Boolean(configured),
1078
+ authorized: false,
1079
+ gestureSupported: false,
1080
+ gestureEnabled: false,
1081
+ fallbackVisible: false,
1082
+ presentation: syncOptions.presentation || 'auto',
1083
+ onboardingSeen: syncOptions.onboardingSeen === true,
1084
+ reason: configured ? 'native-module-unavailable' : 'not-configured',
1085
+ platform: react_native_1.Platform.OS,
1086
+ };
1087
+ if (!configured) {
1088
+ if (typeof native?.setEnabled === 'function') {
1089
+ await native.setEnabled(false, 900).catch(() => undefined);
1090
+ }
1091
+ return base;
1092
+ }
1093
+ if (IS_HOST_MODE || isRunningInsideYaverHost()) {
1094
+ if (typeof native?.setEnabled === 'function') {
1095
+ await native.setEnabled(false, 900).catch(() => undefined);
1096
+ }
1097
+ return { ...base, reason: 'yaver-host-owns-controls' };
1098
+ }
1099
+ const access = await YaverFeedback.getDogfoodAccess();
1100
+ let authorized = access.authorized;
1101
+ if (authorized && config?.dogfood?.canShow) {
1102
+ try {
1103
+ authorized = await config.dogfood.canShow(access);
1104
+ }
1105
+ catch {
1106
+ authorized = false;
1107
+ }
1108
+ }
1109
+ const options = typeof configured === 'object' ? configured : {};
1110
+ const durationMs = Math.min(2000, Math.max(650, options.durationMs ?? 900));
1111
+ const allowFallback = options.fallback !== 'none';
1112
+ if (!authorized) {
1113
+ if (typeof native?.setEnabled === 'function') {
1114
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1115
+ }
1116
+ return { ...base, appId: access.appId, installationId: access.installationId, reason: 'installation-not-authorized' };
1117
+ }
1118
+ const preferenceScope = dogfoodControlPreferenceScope(access.appId, access.installationId);
1119
+ const cachedPresentation = await (0, preferences_1.getDogfoodControlPresentation)(preferenceScope);
1120
+ const cachedOnboardingSeen = await (0, preferences_1.getDogfoodControlOnboardingSeen)(preferenceScope);
1121
+ const onboardingSeen = syncOptions.onboardingSeen
1122
+ ?? access.controlOnboardingSeen
1123
+ ?? cachedOnboardingSeen;
1124
+ // A newly authorized tester must never be left staring at an invisible
1125
+ // feature. Until the exact installation has completed onboarding in
1126
+ // Convex, the edge-docked Y wins even when the gesture is supported.
1127
+ const preferredPresentation = syncOptions.presentation
1128
+ || access.controlPresentation
1129
+ || cachedPresentation
1130
+ || options.defaultPresentation
1131
+ || 'auto';
1132
+ const presentation = onboardingSeen
1133
+ ? preferredPresentation
1134
+ : 'minimized-y';
1135
+ if (typeof native?.getCapability !== 'function' || typeof native?.setEnabled !== 'function') {
1136
+ const result = {
1137
+ ...base,
1138
+ authorized: true,
1139
+ appId: access.appId,
1140
+ installationId: access.installationId,
1141
+ presentation: 'minimized-y',
1142
+ onboardingSeen,
1143
+ fallbackVisible: allowFallback,
1144
+ };
1145
+ if (syncOptions.requirePersistence) {
1146
+ const token = config?.authToken || await (0, auth_1.getToken)();
1147
+ if (!token || !access.installationId) {
1148
+ throw new Error('A full Yaver session is required to save Dogfood settings.');
1149
+ }
1150
+ const persisted = await (0, auth_1.setDogfoodControlPreference)({
1151
+ appId: access.appId,
1152
+ installationId: access.installationId,
1153
+ token,
1154
+ presentation: 'minimized-y',
1155
+ gestureSupported: false,
1156
+ gestureCapabilityReason: 'native-module-unavailable',
1157
+ gesturePlatform: react_native_1.Platform.OS,
1158
+ controlOnboardingSeen: onboardingSeen,
1159
+ });
1160
+ if (!persisted)
1161
+ throw new Error('Could not save this Dogfood control preference to Yaver.');
1162
+ await (0, preferences_1.setDogfoodControlPresentation)('minimized-y', preferenceScope);
1163
+ if (onboardingSeen)
1164
+ await (0, preferences_1.setDogfoodControlOnboardingSeen)(true, preferenceScope);
1165
+ }
1166
+ return result;
1167
+ }
1168
+ try {
1169
+ const capability = await native.getCapability();
1170
+ const gestureSupported = capability?.supported === true;
1171
+ const shouldEnableGesture = gestureSupported && onboardingSeen && presentation === 'auto';
1172
+ const applied = await native.setEnabled(shouldEnableGesture, durationMs);
1173
+ const gestureEnabled = shouldEnableGesture && applied?.enabled === true;
1174
+ const gestureEnableFailed = shouldEnableGesture && !gestureEnabled;
1175
+ const result = {
1176
+ configured: true,
1177
+ authorized: true,
1178
+ appId: access.appId,
1179
+ installationId: access.installationId,
1180
+ gestureSupported,
1181
+ gestureEnabled,
1182
+ fallbackVisible: allowFallback && (!gestureSupported || presentation === 'minimized-y' || gestureEnableFailed),
1183
+ presentation,
1184
+ onboardingSeen,
1185
+ reason: gestureEnableFailed
1186
+ ? 'gesture-enable-failed'
1187
+ : String(applied?.reason || capability?.reason || (gestureSupported ? 'supported' : 'unsupported')),
1188
+ platform: String(applied?.platform || capability?.platform || react_native_1.Platform.OS),
1189
+ };
1190
+ const token = config?.authToken || await (0, auth_1.getToken)();
1191
+ if (syncOptions.requirePersistence && (!token || !access.installationId)) {
1192
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1193
+ throw new Error('A full Yaver session is required to save Dogfood settings.');
1194
+ }
1195
+ if (token && access.installationId) {
1196
+ const needsPersistence = access.controlPresentation !== presentation
1197
+ || access.gestureSupported !== gestureSupported
1198
+ || access.gestureCapabilityReason !== result.reason
1199
+ || (syncOptions.onboardingSeen === true && access.controlOnboardingSeen !== true);
1200
+ if (needsPersistence) {
1201
+ const persisted = await (0, auth_1.setDogfoodControlPreference)({
1202
+ appId: access.appId,
1203
+ installationId: access.installationId,
1204
+ token,
1205
+ presentation,
1206
+ gestureSupported,
1207
+ gestureCapabilityReason: result.reason,
1208
+ gesturePlatform: result.platform || react_native_1.Platform.OS,
1209
+ controlOnboardingSeen: onboardingSeen,
1210
+ });
1211
+ if (!persisted && syncOptions.requirePersistence) {
1212
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1213
+ throw new Error('Could not save this Dogfood control preference to Yaver.');
1214
+ }
1215
+ if (persisted) {
1216
+ await (0, preferences_1.setDogfoodControlPresentation)(presentation, preferenceScope);
1217
+ if (onboardingSeen)
1218
+ await (0, preferences_1.setDogfoodControlOnboardingSeen)(true, preferenceScope);
1219
+ }
1220
+ }
1221
+ else {
1222
+ await (0, preferences_1.setDogfoodControlPresentation)(presentation, preferenceScope);
1223
+ if (onboardingSeen)
1224
+ await (0, preferences_1.setDogfoodControlOnboardingSeen)(true, preferenceScope);
1225
+ }
1226
+ }
1227
+ return result;
1228
+ }
1229
+ catch (error) {
1230
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1231
+ if (syncOptions.requirePersistence) {
1232
+ throw error instanceof Error
1233
+ ? error
1234
+ : new Error('Could not save this Dogfood control preference to Yaver.');
1235
+ }
1236
+ return {
1237
+ ...base,
1238
+ authorized: true,
1239
+ appId: access.appId,
1240
+ installationId: access.installationId,
1241
+ presentation: 'minimized-y',
1242
+ onboardingSeen,
1243
+ fallbackVisible: allowFallback,
1244
+ reason: 'native-capability-check-failed',
1245
+ };
1246
+ }
1247
+ }
1248
+ /** Complete first-run onboarding or change the later control preference.
1249
+ * Convex is authoritative and keyed by full Yaver user + app + installation;
1250
+ * the local value is only a cold-start cache. */
1251
+ static async setDogfoodControlPresentation(presentation) {
1252
+ if (presentation !== 'auto' && presentation !== 'minimized-y') {
1253
+ throw new Error('Invalid Dogfood control presentation.');
1254
+ }
1255
+ return YaverFeedback.syncDogfoodControlGesture({
1256
+ presentation,
1257
+ onboardingSeen: true,
1258
+ requirePersistence: true,
1259
+ });
1260
+ }
1261
+ /** One-tap fast reload for the compact Dogfood card. It preserves the same
1262
+ * selected render machine and bearer-authenticated P2P route as the full
1263
+ * Feedback modal, and names missing auth/machine state instead of no-oping. */
1264
+ static async requestDogfoodFastReload() {
1265
+ await YaverFeedback.hydrateSession();
1266
+ if (!config?.authToken) {
1267
+ YaverFeedback.showLogin();
1268
+ throw new Error('Sign in to Yaver before requesting Fast Reload.');
1269
+ }
1270
+ if (!config.preferredDeviceId) {
1271
+ YaverFeedback.showMachinePicker();
1272
+ throw new Error('Choose a render machine before requesting Fast Reload.');
1273
+ }
1274
+ const selected = await YaverFeedback.getSelectedRemoteDevice();
1275
+ if (!selected) {
1276
+ YaverFeedback.showMachinePicker();
1277
+ throw new Error('The selected render machine is no longer available.');
1278
+ }
1279
+ if (selected.needsAuth) {
1280
+ YaverFeedback.showMachinePicker();
1281
+ throw new Error('The selected render machine needs pairing again.');
1282
+ }
1283
+ if (!selected.isOnline) {
1284
+ throw new Error('The selected render machine is offline.');
1285
+ }
1286
+ let client = YaverFeedback.getRenderP2PClient();
1287
+ if (!client && await YaverFeedback.reconnect()) {
1288
+ client = YaverFeedback.getRenderP2PClient();
1289
+ }
1290
+ if (!client)
1291
+ throw new Error('The render machine is not connected yet.');
1292
+ try {
1293
+ const ack = await client.reloadWithMode('fast');
1294
+ return ack.message;
1295
+ }
1296
+ catch (firstError) {
1297
+ if (await YaverFeedback.reconnect()) {
1298
+ const retry = YaverFeedback.getRenderP2PClient();
1299
+ if (retry)
1300
+ return (await retry.reloadWithMode('fast')).message;
1301
+ }
1302
+ throw firstError;
1303
+ }
1304
+ }
1047
1305
  /** Backwards-compatible entry point for existing integrations. */
1048
1306
  static async beginDogfoodOnboarding(options) {
1049
1307
  YaverFeedback.configureDogfood(options);
@@ -1066,11 +1324,24 @@ class YaverFeedback {
1066
1324
  publishDogfoodFlow(state);
1067
1325
  return state;
1068
1326
  }
1069
- await YaverFeedback.hydrateSession();
1070
- const token = config?.authToken || await (0, auth_1.getToken)();
1071
- const account = token
1072
- ? await (0, auth_1.getDogfoodAccountAccess)(appId, token)
1073
- : { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
1327
+ let token;
1328
+ let account;
1329
+ try {
1330
+ await YaverFeedback.hydrateSession();
1331
+ token = config?.authToken || await (0, auth_1.getToken)();
1332
+ account = token
1333
+ ? await (0, auth_1.getDogfoodAccountAccess)(appId, token)
1334
+ : { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
1335
+ }
1336
+ catch (cause) {
1337
+ const state = {
1338
+ phase: 'error',
1339
+ appId,
1340
+ error: cause instanceof Error ? cause.message : 'Yaver could not verify Dogfood access.',
1341
+ };
1342
+ publishDogfoodFlow(state);
1343
+ return state;
1344
+ }
1074
1345
  // A device key is a second factor for this installation, never a
1075
1346
  // replacement for a real Yaver account. Narrow installation sessions and
1076
1347
  // stale/invalid cached tokens both return authenticated=false here.
@@ -1080,6 +1351,15 @@ class YaverFeedback {
1080
1351
  YaverFeedback.showLogin();
1081
1352
  return state;
1082
1353
  }
1354
+ if (!account.accountAuthorized) {
1355
+ const state = {
1356
+ phase: 'denied',
1357
+ appId,
1358
+ error: 'The app owner has not enabled Dogfood for this Yaver account.',
1359
+ };
1360
+ publishDogfoodFlow(state);
1361
+ return state;
1362
+ }
1083
1363
  if (!config?.preferredDeviceId) {
1084
1364
  const state = { phase: 'machine-required', appId };
1085
1365
  publishDogfoodFlow(state);
@@ -1191,6 +1471,7 @@ class YaverFeedback {
1191
1471
  renderP2PClient = null;
1192
1472
  p2pAuthToken = null;
1193
1473
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
1474
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1194
1475
  }
1195
1476
  /**
1196
1477
  * Manually trigger the feedback collection flow.
@@ -1475,6 +1756,7 @@ class YaverFeedback {
1475
1756
  }
1476
1757
  catch { /* noop */ }
1477
1758
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
1759
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1478
1760
  return { status, installationId: info.installationId, session };
1479
1761
  }
1480
1762
  /** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
@@ -1490,6 +1772,7 @@ class YaverFeedback {
1490
1772
  DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: false, exited: true });
1491
1773
  }
1492
1774
  catch { /* noop */ }
1775
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1493
1776
  }
1494
1777
  /** Returns the resolved relay password the SDK is currently using.
1495
1778
  * Empty string when no relay routing is in play (direct LAN agent
@@ -1988,6 +2271,13 @@ class YaverFeedback {
1988
2271
  dogfoodShortcutAppStateSubscription = null;
1989
2272
  dogfoodActivationSubscription?.remove();
1990
2273
  dogfoodActivationSubscription = null;
2274
+ try {
2275
+ const native = react_native_1.NativeModules?.YaverDogfoodGesture;
2276
+ if (typeof native?.setEnabled === 'function') {
2277
+ void native.setEnabled(false, 900).catch(() => undefined);
2278
+ }
2279
+ }
2280
+ catch { /* native trigger teardown is best-effort */ }
1991
2281
  lastDogfoodActivationUrl = '';
1992
2282
  // Before `enabled = false` / `config = null` below, so an in-flight retry
1993
2283
  // can't fire against a torn-down config.
@@ -22,6 +22,24 @@ public class AppDelegate: ExpoAppDelegate {
22
22
  expect(once).toContain('markDogfoodShortcutPending()');
23
23
  expect(plugin.__test.patchDogfoodAppShortcut(once)).toBe(once);
24
24
  });
25
+ it('patches cold and warm scene-lifecycle delivery idempotently', () => {
26
+ const source = `
27
+ final class TalosSceneDelegate: UIResponder, UIWindowSceneDelegate {
28
+ func scene(
29
+ _ scene: UIScene,
30
+ willConnectTo session: UISceneSession,
31
+ options connectionOptions: UIScene.ConnectionOptions
32
+ ) {
33
+ guard scene is UIWindowScene else { return }
34
+ }
35
+ }
36
+ `;
37
+ const once = plugin.__test.patchDogfoodSceneDelegate(source);
38
+ expect(once).toContain('connectionOptions.shortcutItem');
39
+ expect(once).toContain('func windowScene(');
40
+ expect(once).toContain('markDogfoodShortcutPending()');
41
+ expect(plugin.__test.patchDogfoodSceneDelegate(once)).toBe(once);
42
+ });
25
43
  it('uses dynamic native shortcuts on both platforms', () => {
26
44
  const ios = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../ios/YaverHotReload.swift'), 'utf8');
27
45
  const android = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java'), 'utf8');
@@ -31,4 +49,18 @@ public class AppDelegate: ExpoAppDelegate {
31
49
  expect(android).toContain('addDynamicShortcuts');
32
50
  expect(android).toContain('consumeDogfoodShortcut');
33
51
  });
52
+ it('observes a three-finger hold without consuming host app touches', () => {
53
+ const ios = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../ios/YaverDogfoodGesture.swift'), 'utf8');
54
+ const android = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../android/src/main/java/io/yaver/feedback/YaverDogfoodGestureModule.java'), 'utf8');
55
+ const controls = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../DogfoodQuickControls.tsx'), 'utf8');
56
+ expect(ios).toContain('numberOfTouchesRequired = 3');
57
+ expect(ios).toContain('cancelsTouchesInView = false');
58
+ expect(ios).toContain('isVoiceOverRunning');
59
+ expect(android).toContain('event.getPointerCount() >= 3');
60
+ expect(android).toContain('method.invoke(callback, args)');
61
+ expect(android).toContain('isTouchExplorationEnabled');
62
+ expect(controls).toContain('yaver-dogfood-fast-reload');
63
+ expect(controls).toContain('yaver-dogfood-chat');
64
+ expect(controls).toContain('yaver-dogfood-minimized-control');
65
+ });
34
66
  });