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.
@@ -15,6 +15,7 @@ import {
15
15
  getToken,
16
16
  getSelectedDeviceId,
17
17
  getDogfoodAccountAccess,
18
+ setDogfoodControlPreference,
18
19
  listReachableDevices,
19
20
  clearToken,
20
21
  clearSelectedDeviceId,
@@ -25,6 +26,11 @@ import {
25
26
  getQuickIconColorPreset,
26
27
  setQuickIconDisabled,
27
28
  setQuickIconColorPreset,
29
+ getDogfoodControlPresentation,
30
+ setDogfoodControlPresentation as cacheDogfoodControlPresentation,
31
+ getDogfoodControlOnboardingSeen,
32
+ setDogfoodControlOnboardingSeen,
33
+ type DogfoodControlPresentation,
28
34
  QuickIconColorPreset,
29
35
  } from './preferences';
30
36
  import { resolveSDKDogfood, type SDKDogfoodStatus, type DogfoodAccessSnapshot } from './dogfoodPolicy';
@@ -44,6 +50,27 @@ export interface DogfoodFlowState {
44
50
  error?: string;
45
51
  }
46
52
 
53
+ export interface DogfoodControlTriggerState {
54
+ configured: boolean;
55
+ authorized: boolean;
56
+ appId?: string;
57
+ installationId?: string;
58
+ gestureSupported: boolean;
59
+ gestureEnabled: boolean;
60
+ fallbackVisible: boolean;
61
+ presentation: DogfoodControlPresentation;
62
+ onboardingSeen: boolean;
63
+ reason: string;
64
+ platform?: string;
65
+ }
66
+
67
+ interface DogfoodControlSyncOptions {
68
+ presentation?: DogfoodControlPresentation;
69
+ onboardingSeen?: boolean;
70
+ /** User-initiated changes fail visibly when Convex cannot persist them. */
71
+ requirePersistence?: boolean;
72
+ }
73
+
47
74
  function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
48
75
  const maybeNodeTimer = timer as unknown as { unref?: () => void };
49
76
  if (typeof maybeNodeTimer.unref === 'function') {
@@ -194,6 +221,11 @@ let dogfoodActivationSubscription: { remove: () => void } | null = null;
194
221
  let dogfoodShortcutLaunchInFlight = false;
195
222
  let lastDogfoodActivationUrl = '';
196
223
 
224
+ function dogfoodControlPreferenceScope(appId?: string, installationId?: string): string | undefined {
225
+ if (!appId || !installationId) return undefined;
226
+ return `${appId}:${installationId}`;
227
+ }
228
+
197
229
  function publishDogfoodFlow(state: DogfoodFlowState): void {
198
230
  dogfoodFlowState = state;
199
231
  try { config?.dogfood?.onStateChange?.(state); } catch { /* host callback */ }
@@ -490,19 +522,29 @@ export class YaverFeedback {
490
522
  void YaverFeedback.hydrateSession();
491
523
  }
492
524
 
493
- if (config.dogfood?.appShortcut) {
494
- void YaverFeedback.syncDogfoodAppShortcut();
495
- void consumeNativeDogfoodShortcut();
525
+ if (config.dogfood?.appShortcut || config.dogfood?.controlGesture) {
526
+ if (config.dogfood?.appShortcut) {
527
+ void YaverFeedback.syncDogfoodAppShortcut();
528
+ void consumeNativeDogfoodShortcut();
529
+ }
530
+ if (config.dogfood?.controlGesture) {
531
+ void YaverFeedback.syncDogfoodControlGesture();
532
+ }
496
533
  try {
497
534
  const { AppState } = require('react-native');
498
535
  dogfoodShortcutAppStateSubscription?.remove();
499
536
  dogfoodShortcutAppStateSubscription = AppState.addEventListener('change', (state: string) => {
500
537
  if (state === 'active') {
501
- void YaverFeedback.syncDogfoodAppShortcut();
502
- void consumeNativeDogfoodShortcut();
538
+ if (config?.dogfood?.appShortcut) {
539
+ void YaverFeedback.syncDogfoodAppShortcut();
540
+ void consumeNativeDogfoodShortcut();
541
+ }
542
+ if (config?.dogfood?.controlGesture) {
543
+ void YaverFeedback.syncDogfoodControlGesture();
544
+ }
503
545
  }
504
546
  });
505
- } catch { /* native shortcut unavailable on web/test runtimes */ }
547
+ } catch { /* native quick controls unavailable on web/test runtimes */ }
506
548
  }
507
549
  if (config.dogfood) {
508
550
  try {
@@ -805,6 +847,7 @@ export class YaverFeedback {
805
847
  YaverFeedback.scheduleBlackBoxAutoStart();
806
848
  }
807
849
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
850
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
808
851
  }
809
852
 
810
853
  /** Returns true once the SDK has a session token it can use. */
@@ -963,16 +1006,21 @@ export class YaverFeedback {
963
1006
  const token = config?.authToken || await getToken();
964
1007
  const account = token
965
1008
  ? await getDogfoodAccountAccess(appId, token, installationId)
966
- : { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
1009
+ : { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
967
1010
  const yaverAuthenticated = account.authenticated;
968
1011
  const ownerAuthorized = account.ownerAuthorized;
969
1012
  return {
970
1013
  appId,
971
1014
  yaverAuthenticated,
972
1015
  ownerAuthorized,
1016
+ accountAuthorized: account.accountAuthorized,
973
1017
  installationId,
974
1018
  deviceState,
975
1019
  authorized: yaverAuthenticated && account.installationAuthorized && deviceState === 'active',
1020
+ controlPresentation: account.controlPresentation,
1021
+ gestureSupported: account.gestureSupported,
1022
+ gestureCapabilityReason: account.gestureCapabilityReason,
1023
+ controlOnboardingSeen: account.controlOnboardingSeen,
976
1024
  };
977
1025
  }
978
1026
 
@@ -1002,6 +1050,244 @@ export class YaverFeedback {
1002
1050
  return visible;
1003
1051
  }
1004
1052
 
1053
+ /** Capability-gated in-app Dogfood controls. Every new exact installation
1054
+ * starts with Y until Convex records onboarding. A supported standalone app
1055
+ * can then select a passive three-finger hold and no persistent pixels;
1056
+ * missing native support or accessibility touch exploration keeps Y. Yaver's
1057
+ * own container suppresses both because its split preview/chat UI owns them. */
1058
+ static async syncDogfoodControlGesture(
1059
+ syncOptions: DogfoodControlSyncOptions = {},
1060
+ ): Promise<DogfoodControlTriggerState> {
1061
+ const configured = config?.dogfood?.controlGesture;
1062
+ const native = (NativeModules as any)?.YaverDogfoodGesture;
1063
+ const base: DogfoodControlTriggerState = {
1064
+ configured: Boolean(configured),
1065
+ authorized: false,
1066
+ gestureSupported: false,
1067
+ gestureEnabled: false,
1068
+ fallbackVisible: false,
1069
+ presentation: syncOptions.presentation || 'auto',
1070
+ onboardingSeen: syncOptions.onboardingSeen === true,
1071
+ reason: configured ? 'native-module-unavailable' : 'not-configured',
1072
+ platform: Platform.OS,
1073
+ };
1074
+ if (!configured) {
1075
+ if (typeof native?.setEnabled === 'function') {
1076
+ await native.setEnabled(false, 900).catch(() => undefined);
1077
+ }
1078
+ return base;
1079
+ }
1080
+ if (IS_HOST_MODE || isRunningInsideYaverHost()) {
1081
+ if (typeof native?.setEnabled === 'function') {
1082
+ await native.setEnabled(false, 900).catch(() => undefined);
1083
+ }
1084
+ return { ...base, reason: 'yaver-host-owns-controls' };
1085
+ }
1086
+
1087
+ const access = await YaverFeedback.getDogfoodAccess();
1088
+ let authorized = access.authorized;
1089
+ if (authorized && config?.dogfood?.canShow) {
1090
+ try {
1091
+ authorized = await config.dogfood.canShow(access);
1092
+ } catch {
1093
+ authorized = false;
1094
+ }
1095
+ }
1096
+ const options = typeof configured === 'object' ? configured : {};
1097
+ const durationMs = Math.min(2000, Math.max(650, options.durationMs ?? 900));
1098
+ const allowFallback = options.fallback !== 'none';
1099
+ if (!authorized) {
1100
+ if (typeof native?.setEnabled === 'function') {
1101
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1102
+ }
1103
+ return { ...base, appId: access.appId, installationId: access.installationId, reason: 'installation-not-authorized' };
1104
+ }
1105
+
1106
+ const preferenceScope = dogfoodControlPreferenceScope(access.appId, access.installationId);
1107
+ const cachedPresentation = await getDogfoodControlPresentation(preferenceScope);
1108
+ const cachedOnboardingSeen = await getDogfoodControlOnboardingSeen(preferenceScope);
1109
+ const onboardingSeen = syncOptions.onboardingSeen
1110
+ ?? access.controlOnboardingSeen
1111
+ ?? cachedOnboardingSeen;
1112
+ // A newly authorized tester must never be left staring at an invisible
1113
+ // feature. Until the exact installation has completed onboarding in
1114
+ // Convex, the edge-docked Y wins even when the gesture is supported.
1115
+ const preferredPresentation = syncOptions.presentation
1116
+ || access.controlPresentation
1117
+ || cachedPresentation
1118
+ || options.defaultPresentation
1119
+ || 'auto';
1120
+ const presentation: DogfoodControlPresentation = onboardingSeen
1121
+ ? preferredPresentation
1122
+ : 'minimized-y';
1123
+
1124
+ if (typeof native?.getCapability !== 'function' || typeof native?.setEnabled !== 'function') {
1125
+ const result: DogfoodControlTriggerState = {
1126
+ ...base,
1127
+ authorized: true,
1128
+ appId: access.appId,
1129
+ installationId: access.installationId,
1130
+ presentation: 'minimized-y',
1131
+ onboardingSeen,
1132
+ fallbackVisible: allowFallback,
1133
+ };
1134
+ if (syncOptions.requirePersistence) {
1135
+ const token = config?.authToken || await getToken();
1136
+ if (!token || !access.installationId) {
1137
+ throw new Error('A full Yaver session is required to save Dogfood settings.');
1138
+ }
1139
+ const persisted = await setDogfoodControlPreference({
1140
+ appId: access.appId,
1141
+ installationId: access.installationId,
1142
+ token,
1143
+ presentation: 'minimized-y',
1144
+ gestureSupported: false,
1145
+ gestureCapabilityReason: 'native-module-unavailable',
1146
+ gesturePlatform: Platform.OS,
1147
+ controlOnboardingSeen: onboardingSeen,
1148
+ });
1149
+ if (!persisted) throw new Error('Could not save this Dogfood control preference to Yaver.');
1150
+ await cacheDogfoodControlPresentation('minimized-y', preferenceScope);
1151
+ if (onboardingSeen) await setDogfoodControlOnboardingSeen(true, preferenceScope);
1152
+ }
1153
+ return result;
1154
+ }
1155
+ try {
1156
+ const capability = await native.getCapability();
1157
+ const gestureSupported = capability?.supported === true;
1158
+ const shouldEnableGesture = gestureSupported && onboardingSeen && presentation === 'auto';
1159
+ const applied = await native.setEnabled(shouldEnableGesture, durationMs);
1160
+ const gestureEnabled = shouldEnableGesture && applied?.enabled === true;
1161
+ const gestureEnableFailed = shouldEnableGesture && !gestureEnabled;
1162
+ const result: DogfoodControlTriggerState = {
1163
+ configured: true,
1164
+ authorized: true,
1165
+ appId: access.appId,
1166
+ installationId: access.installationId,
1167
+ gestureSupported,
1168
+ gestureEnabled,
1169
+ fallbackVisible: allowFallback && (!gestureSupported || presentation === 'minimized-y' || gestureEnableFailed),
1170
+ presentation,
1171
+ onboardingSeen,
1172
+ reason: gestureEnableFailed
1173
+ ? 'gesture-enable-failed'
1174
+ : String(applied?.reason || capability?.reason || (gestureSupported ? 'supported' : 'unsupported')),
1175
+ platform: String(applied?.platform || capability?.platform || Platform.OS),
1176
+ };
1177
+ const token = config?.authToken || await getToken();
1178
+ if (syncOptions.requirePersistence && (!token || !access.installationId)) {
1179
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1180
+ throw new Error('A full Yaver session is required to save Dogfood settings.');
1181
+ }
1182
+ if (token && access.installationId) {
1183
+ const needsPersistence =
1184
+ access.controlPresentation !== presentation
1185
+ || access.gestureSupported !== gestureSupported
1186
+ || access.gestureCapabilityReason !== result.reason
1187
+ || (syncOptions.onboardingSeen === true && access.controlOnboardingSeen !== true);
1188
+ if (needsPersistence) {
1189
+ const persisted = await setDogfoodControlPreference({
1190
+ appId: access.appId,
1191
+ installationId: access.installationId,
1192
+ token,
1193
+ presentation,
1194
+ gestureSupported,
1195
+ gestureCapabilityReason: result.reason,
1196
+ gesturePlatform: result.platform || Platform.OS,
1197
+ controlOnboardingSeen: onboardingSeen,
1198
+ });
1199
+ if (!persisted && syncOptions.requirePersistence) {
1200
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1201
+ throw new Error('Could not save this Dogfood control preference to Yaver.');
1202
+ }
1203
+ if (persisted) {
1204
+ await cacheDogfoodControlPresentation(presentation, preferenceScope);
1205
+ if (onboardingSeen) await setDogfoodControlOnboardingSeen(true, preferenceScope);
1206
+ }
1207
+ } else {
1208
+ await cacheDogfoodControlPresentation(presentation, preferenceScope);
1209
+ if (onboardingSeen) await setDogfoodControlOnboardingSeen(true, preferenceScope);
1210
+ }
1211
+ }
1212
+ return result;
1213
+ } catch (error) {
1214
+ await native.setEnabled(false, durationMs).catch(() => undefined);
1215
+ if (syncOptions.requirePersistence) {
1216
+ throw error instanceof Error
1217
+ ? error
1218
+ : new Error('Could not save this Dogfood control preference to Yaver.');
1219
+ }
1220
+ return {
1221
+ ...base,
1222
+ authorized: true,
1223
+ appId: access.appId,
1224
+ installationId: access.installationId,
1225
+ presentation: 'minimized-y',
1226
+ onboardingSeen,
1227
+ fallbackVisible: allowFallback,
1228
+ reason: 'native-capability-check-failed',
1229
+ };
1230
+ }
1231
+ }
1232
+
1233
+ /** Complete first-run onboarding or change the later control preference.
1234
+ * Convex is authoritative and keyed by full Yaver user + app + installation;
1235
+ * the local value is only a cold-start cache. */
1236
+ static async setDogfoodControlPresentation(
1237
+ presentation: DogfoodControlPresentation,
1238
+ ): Promise<DogfoodControlTriggerState> {
1239
+ if (presentation !== 'auto' && presentation !== 'minimized-y') {
1240
+ throw new Error('Invalid Dogfood control presentation.');
1241
+ }
1242
+ return YaverFeedback.syncDogfoodControlGesture({
1243
+ presentation,
1244
+ onboardingSeen: true,
1245
+ requirePersistence: true,
1246
+ });
1247
+ }
1248
+
1249
+ /** One-tap fast reload for the compact Dogfood card. It preserves the same
1250
+ * selected render machine and bearer-authenticated P2P route as the full
1251
+ * Feedback modal, and names missing auth/machine state instead of no-oping. */
1252
+ static async requestDogfoodFastReload(): Promise<string> {
1253
+ await YaverFeedback.hydrateSession();
1254
+ if (!config?.authToken) {
1255
+ YaverFeedback.showLogin();
1256
+ throw new Error('Sign in to Yaver before requesting Fast Reload.');
1257
+ }
1258
+ if (!config.preferredDeviceId) {
1259
+ YaverFeedback.showMachinePicker();
1260
+ throw new Error('Choose a render machine before requesting Fast Reload.');
1261
+ }
1262
+ const selected = await YaverFeedback.getSelectedRemoteDevice();
1263
+ if (!selected) {
1264
+ YaverFeedback.showMachinePicker();
1265
+ throw new Error('The selected render machine is no longer available.');
1266
+ }
1267
+ if (selected.needsAuth) {
1268
+ YaverFeedback.showMachinePicker();
1269
+ throw new Error('The selected render machine needs pairing again.');
1270
+ }
1271
+ if (!selected.isOnline) {
1272
+ throw new Error('The selected render machine is offline.');
1273
+ }
1274
+ let client = YaverFeedback.getRenderP2PClient();
1275
+ if (!client && await YaverFeedback.reconnect()) {
1276
+ client = YaverFeedback.getRenderP2PClient();
1277
+ }
1278
+ if (!client) throw new Error('The render machine is not connected yet.');
1279
+ try {
1280
+ const ack = await client.reloadWithMode('fast');
1281
+ return ack.message;
1282
+ } catch (firstError) {
1283
+ if (await YaverFeedback.reconnect()) {
1284
+ const retry = YaverFeedback.getRenderP2PClient();
1285
+ if (retry) return (await retry.reloadWithMode('fast')).message;
1286
+ }
1287
+ throw firstError;
1288
+ }
1289
+ }
1290
+
1005
1291
  /** Backwards-compatible entry point for existing integrations. */
1006
1292
  static async beginDogfoodOnboarding(options: DogfoodOnboardingOptions): Promise<DogfoodFlowState> {
1007
1293
  YaverFeedback.configureDogfood(options);
@@ -1025,11 +1311,28 @@ export class YaverFeedback {
1025
1311
  publishDogfoodFlow(state);
1026
1312
  return state;
1027
1313
  }
1028
- await YaverFeedback.hydrateSession();
1029
- const token = config?.authToken || await getToken();
1030
- const account = token
1031
- ? await getDogfoodAccountAccess(appId, token)
1032
- : { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
1314
+ let token: string | null;
1315
+ let account: Awaited<ReturnType<typeof getDogfoodAccountAccess>> | {
1316
+ authenticated: false;
1317
+ ownerAuthorized: false;
1318
+ accountAuthorized: false;
1319
+ installationAuthorized: false;
1320
+ };
1321
+ try {
1322
+ await YaverFeedback.hydrateSession();
1323
+ token = config?.authToken || await getToken();
1324
+ account = token
1325
+ ? await getDogfoodAccountAccess(appId, token)
1326
+ : { authenticated: false, ownerAuthorized: false, accountAuthorized: false, installationAuthorized: false };
1327
+ } catch (cause) {
1328
+ const state: DogfoodFlowState = {
1329
+ phase: 'error',
1330
+ appId,
1331
+ error: cause instanceof Error ? cause.message : 'Yaver could not verify Dogfood access.',
1332
+ };
1333
+ publishDogfoodFlow(state);
1334
+ return state;
1335
+ }
1033
1336
  // A device key is a second factor for this installation, never a
1034
1337
  // replacement for a real Yaver account. Narrow installation sessions and
1035
1338
  // stale/invalid cached tokens both return authenticated=false here.
@@ -1039,6 +1342,15 @@ export class YaverFeedback {
1039
1342
  YaverFeedback.showLogin();
1040
1343
  return state;
1041
1344
  }
1345
+ if (!account.accountAuthorized) {
1346
+ const state: DogfoodFlowState = {
1347
+ phase: 'denied',
1348
+ appId,
1349
+ error: 'The app owner has not enabled Dogfood for this Yaver account.',
1350
+ };
1351
+ publishDogfoodFlow(state);
1352
+ return state;
1353
+ }
1042
1354
  if (!config?.preferredDeviceId) {
1043
1355
  const state: DogfoodFlowState = { phase: 'machine-required', appId };
1044
1356
  publishDogfoodFlow(state);
@@ -1162,6 +1474,7 @@ export class YaverFeedback {
1162
1474
  renderP2PClient = null;
1163
1475
  p2pAuthToken = null;
1164
1476
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
1477
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1165
1478
  }
1166
1479
 
1167
1480
  /**
@@ -1461,6 +1774,7 @@ export class YaverFeedback {
1461
1774
  DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: !!session, status });
1462
1775
  } catch { /* noop */ }
1463
1776
  await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
1777
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1464
1778
  return { status, installationId: info.installationId, session };
1465
1779
  }
1466
1780
 
@@ -1475,6 +1789,7 @@ export class YaverFeedback {
1475
1789
  const { DeviceEventEmitter } = require('react-native');
1476
1790
  DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: false, exited: true });
1477
1791
  } catch { /* noop */ }
1792
+ await YaverFeedback.syncDogfoodControlGesture().catch(() => undefined);
1478
1793
  }
1479
1794
 
1480
1795
  /** Returns the resolved relay password the SDK is currently using.
@@ -2019,6 +2334,12 @@ export class YaverFeedback {
2019
2334
  dogfoodShortcutAppStateSubscription = null;
2020
2335
  dogfoodActivationSubscription?.remove();
2021
2336
  dogfoodActivationSubscription = null;
2337
+ try {
2338
+ const native = (NativeModules as any)?.YaverDogfoodGesture;
2339
+ if (typeof native?.setEnabled === 'function') {
2340
+ void native.setEnabled(false, 900).catch(() => undefined);
2341
+ }
2342
+ } catch { /* native trigger teardown is best-effort */ }
2022
2343
  lastDogfoodActivationUrl = '';
2023
2344
  // Before `enabled = false` / `config = null` below, so an in-flight retry
2024
2345
  // can't fire against a torn-down config.
@@ -2,7 +2,10 @@ import { readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
4
  const plugin = require('../../app.plugin.js') as {
5
- __test: { patchDogfoodAppShortcut(contents: string): string };
5
+ __test: {
6
+ patchDogfoodAppShortcut(contents: string): string;
7
+ patchDogfoodSceneDelegate(contents: string): string;
8
+ };
6
9
  };
7
10
 
8
11
  describe('native Dogfood shortcut contract', () => {
@@ -25,6 +28,25 @@ public class AppDelegate: ExpoAppDelegate {
25
28
  expect(plugin.__test.patchDogfoodAppShortcut(once)).toBe(once);
26
29
  });
27
30
 
31
+ it('patches cold and warm scene-lifecycle delivery idempotently', () => {
32
+ const source = `
33
+ final class TalosSceneDelegate: UIResponder, UIWindowSceneDelegate {
34
+ func scene(
35
+ _ scene: UIScene,
36
+ willConnectTo session: UISceneSession,
37
+ options connectionOptions: UIScene.ConnectionOptions
38
+ ) {
39
+ guard scene is UIWindowScene else { return }
40
+ }
41
+ }
42
+ `;
43
+ const once = plugin.__test.patchDogfoodSceneDelegate(source);
44
+ expect(once).toContain('connectionOptions.shortcutItem');
45
+ expect(once).toContain('func windowScene(');
46
+ expect(once).toContain('markDogfoodShortcutPending()');
47
+ expect(plugin.__test.patchDogfoodSceneDelegate(once)).toBe(once);
48
+ });
49
+
28
50
  it('uses dynamic native shortcuts on both platforms', () => {
29
51
  const ios = readFileSync(join(__dirname, '../../ios/YaverHotReload.swift'), 'utf8');
30
52
  const android = readFileSync(join(__dirname, '../../android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java'), 'utf8');
@@ -34,4 +56,19 @@ public class AppDelegate: ExpoAppDelegate {
34
56
  expect(android).toContain('addDynamicShortcuts');
35
57
  expect(android).toContain('consumeDogfoodShortcut');
36
58
  });
59
+
60
+ it('observes a three-finger hold without consuming host app touches', () => {
61
+ const ios = readFileSync(join(__dirname, '../../ios/YaverDogfoodGesture.swift'), 'utf8');
62
+ const android = readFileSync(join(__dirname, '../../android/src/main/java/io/yaver/feedback/YaverDogfoodGestureModule.java'), 'utf8');
63
+ const controls = readFileSync(join(__dirname, '../DogfoodQuickControls.tsx'), 'utf8');
64
+ expect(ios).toContain('numberOfTouchesRequired = 3');
65
+ expect(ios).toContain('cancelsTouchesInView = false');
66
+ expect(ios).toContain('isVoiceOverRunning');
67
+ expect(android).toContain('event.getPointerCount() >= 3');
68
+ expect(android).toContain('method.invoke(callback, args)');
69
+ expect(android).toContain('isTouchExplorationEnabled');
70
+ expect(controls).toContain('yaver-dogfood-fast-reload');
71
+ expect(controls).toContain('yaver-dogfood-chat');
72
+ expect(controls).toContain('yaver-dogfood-minimized-control');
73
+ });
37
74
  });