yaver-feedback-react-native 0.9.7 → 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.
- package/README.md +39 -7
- package/android/src/main/java/io/yaver/feedback/YaverDogfoodGestureModule.java +241 -0
- package/android/src/main/java/io/yaver/feedback/YaverHotReloadPackage.java +1 -0
- package/app.plugin.js +12 -2
- package/dist/AuthOverlay.js +13 -2
- package/dist/DogfoodQuickControls.d.ts +8 -0
- package/dist/DogfoodQuickControls.js +394 -0
- package/dist/DogfoodSessionUi.d.ts +50 -0
- package/dist/DogfoodSessionUi.js +135 -0
- package/dist/FeedbackModal.js +50 -22
- package/dist/YaverFeedback.d.ts +35 -1
- package/dist/YaverFeedback.js +302 -12
- package/dist/__tests__/NativeDogfoodShortcut.test.js +14 -0
- package/dist/__tests__/YaverFeedback.test.js +148 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +41 -2
- package/dist/dogfoodPolicy.d.ts +22 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/preferences.d.ts +13 -0
- package/dist/preferences.js +84 -0
- package/ios/YaverDogfoodGesture.m +14 -0
- package/ios/YaverDogfoodGesture.swift +175 -0
- package/package.json +1 -1
- package/src/AuthOverlay.tsx +20 -3
- package/src/DogfoodQuickControls.tsx +454 -0
- package/src/DogfoodSessionUi.tsx +213 -0
- package/src/FeedbackModal.tsx +62 -34
- package/src/YaverFeedback.ts +333 -12
- package/src/__tests__/NativeDogfoodShortcut.test.ts +15 -0
- package/src/__tests__/YaverFeedback.test.ts +155 -0
- package/src/auth.ts +53 -2
- package/src/dogfoodPolicy.ts +22 -1
- package/src/index.ts +13 -1
- package/src/preferences.ts +89 -0
package/src/YaverFeedback.ts
CHANGED
|
@@ -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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
:
|
|
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.
|
|
@@ -56,4 +56,19 @@ final class TalosSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
|
56
56
|
expect(android).toContain('addDynamicShortcuts');
|
|
57
57
|
expect(android).toContain('consumeDogfoodShortcut');
|
|
58
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
|
+
});
|
|
59
74
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DeviceEventEmitter, NativeModules } from 'react-native';
|
|
2
2
|
import { YaverFeedback } from '../YaverFeedback';
|
|
3
|
+
import { getDogfoodAccountAccess, setDogfoodControlPreference } from '../auth';
|
|
3
4
|
|
|
4
5
|
// Mock react-native: DeviceEventEmitter for event dispatch + Platform so
|
|
5
6
|
// ShakeDetector.start() can branch on iOS without hitting a real RN runtime.
|
|
@@ -14,6 +15,10 @@ jest.mock('react-native', () => ({
|
|
|
14
15
|
setDogfoodShortcut: jest.fn(async () => true),
|
|
15
16
|
consumeDogfoodShortcut: jest.fn(async () => false),
|
|
16
17
|
},
|
|
18
|
+
YaverDogfoodGesture: {
|
|
19
|
+
getCapability: jest.fn(async () => ({ supported: true, enabled: false, reason: 'supported', platform: 'ios' })),
|
|
20
|
+
setEnabled: jest.fn(async (next: boolean) => ({ supported: true, enabled: next, reason: 'supported', platform: 'ios' })),
|
|
21
|
+
},
|
|
17
22
|
},
|
|
18
23
|
AppState: { addEventListener: jest.fn(() => ({ remove: jest.fn() })) },
|
|
19
24
|
}));
|
|
@@ -33,8 +38,10 @@ jest.mock('../auth', () => ({
|
|
|
33
38
|
getDogfoodAccountAccess: jest.fn(async (_appId: string, token: string) => ({
|
|
34
39
|
authenticated: token === 'owner-token',
|
|
35
40
|
ownerAuthorized: token === 'owner-token',
|
|
41
|
+
accountAuthorized: token === 'owner-token',
|
|
36
42
|
installationAuthorized: token === 'owner-token',
|
|
37
43
|
})),
|
|
44
|
+
setDogfoodControlPreference: jest.fn(async () => true),
|
|
38
45
|
saveSelectedDeviceId: jest.fn(async () => {}),
|
|
39
46
|
clearToken: jest.fn(async () => {}),
|
|
40
47
|
clearSelectedDeviceId: jest.fn(async () => {}),
|
|
@@ -103,6 +110,18 @@ describe('YaverFeedback', () => {
|
|
|
103
110
|
expect(states[states.length - 1]).toBe('auth-required');
|
|
104
111
|
});
|
|
105
112
|
|
|
113
|
+
it('returns a visible structured error when access verification fails', async () => {
|
|
114
|
+
YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
|
|
115
|
+
(getDogfoodAccountAccess as jest.Mock).mockRejectedValueOnce(new Error('Access service unavailable'));
|
|
116
|
+
const state = await YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app' });
|
|
117
|
+
expect(state).toEqual({
|
|
118
|
+
phase: 'error',
|
|
119
|
+
appId: 'io.example.app',
|
|
120
|
+
error: 'Access service unavailable',
|
|
121
|
+
});
|
|
122
|
+
expect(DeviceEventEmitter.emit).not.toHaveBeenCalledWith('yaverFeedback:startReport');
|
|
123
|
+
});
|
|
124
|
+
|
|
106
125
|
it('lets a host ACL hide its affordance without opening auth UI', async () => {
|
|
107
126
|
YaverFeedback.init({
|
|
108
127
|
enabled: true,
|
|
@@ -121,6 +140,7 @@ describe('YaverFeedback', () => {
|
|
|
121
140
|
appId: 'io.example.app',
|
|
122
141
|
yaverAuthenticated: true,
|
|
123
142
|
ownerAuthorized: false,
|
|
143
|
+
accountAuthorized: true,
|
|
124
144
|
installationId: 'phone-1',
|
|
125
145
|
deviceState: 'active',
|
|
126
146
|
authorized: true,
|
|
@@ -137,6 +157,7 @@ describe('YaverFeedback', () => {
|
|
|
137
157
|
appId: 'io.example.app',
|
|
138
158
|
yaverAuthenticated: false,
|
|
139
159
|
ownerAuthorized: false,
|
|
160
|
+
accountAuthorized: true,
|
|
140
161
|
installationId: 'phone-1',
|
|
141
162
|
deviceState: 'active',
|
|
142
163
|
authorized: false,
|
|
@@ -155,6 +176,7 @@ describe('YaverFeedback', () => {
|
|
|
155
176
|
appId: 'io.example.app',
|
|
156
177
|
yaverAuthenticated: true,
|
|
157
178
|
ownerAuthorized: false,
|
|
179
|
+
accountAuthorized: true,
|
|
158
180
|
installationId: 'phone-1',
|
|
159
181
|
deviceState: 'active',
|
|
160
182
|
authorized: true,
|
|
@@ -163,6 +185,139 @@ describe('YaverFeedback', () => {
|
|
|
163
185
|
expect((NativeModules as any).YaverHotReload.setDogfoodShortcut)
|
|
164
186
|
.toHaveBeenCalledWith(false, 'Dogfood');
|
|
165
187
|
});
|
|
188
|
+
|
|
189
|
+
it('keeps the Y visible until the authorized phone has completed onboarding', async () => {
|
|
190
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
191
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
192
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
193
|
+
appId: 'io.example.app',
|
|
194
|
+
yaverAuthenticated: true,
|
|
195
|
+
ownerAuthorized: false,
|
|
196
|
+
accountAuthorized: true,
|
|
197
|
+
installationId: 'phone-1',
|
|
198
|
+
deviceState: 'active',
|
|
199
|
+
authorized: true,
|
|
200
|
+
});
|
|
201
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
202
|
+
expect(state).toMatchObject({
|
|
203
|
+
onboardingSeen: false,
|
|
204
|
+
presentation: 'minimized-y',
|
|
205
|
+
gestureSupported: true,
|
|
206
|
+
gestureEnabled: false,
|
|
207
|
+
fallbackVisible: true,
|
|
208
|
+
});
|
|
209
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 900);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('uses the invisible three-finger hold only after onboarding selects it', async () => {
|
|
213
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
214
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
215
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
216
|
+
appId: 'io.example.app',
|
|
217
|
+
yaverAuthenticated: true,
|
|
218
|
+
ownerAuthorized: false,
|
|
219
|
+
accountAuthorized: true,
|
|
220
|
+
installationId: 'phone-1',
|
|
221
|
+
deviceState: 'active',
|
|
222
|
+
authorized: true,
|
|
223
|
+
controlPresentation: 'auto',
|
|
224
|
+
controlOnboardingSeen: true,
|
|
225
|
+
});
|
|
226
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
227
|
+
expect(state).toMatchObject({
|
|
228
|
+
onboardingSeen: true,
|
|
229
|
+
presentation: 'auto',
|
|
230
|
+
gestureSupported: true,
|
|
231
|
+
gestureEnabled: true,
|
|
232
|
+
fallbackVisible: false,
|
|
233
|
+
});
|
|
234
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(true, 900);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('keeps the Y when native capability reports support but enabling the gesture fails', async () => {
|
|
238
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
239
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
240
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
241
|
+
appId: 'io.example.app',
|
|
242
|
+
yaverAuthenticated: true,
|
|
243
|
+
ownerAuthorized: false,
|
|
244
|
+
accountAuthorized: true,
|
|
245
|
+
installationId: 'phone-1',
|
|
246
|
+
deviceState: 'active',
|
|
247
|
+
authorized: true,
|
|
248
|
+
controlPresentation: 'auto',
|
|
249
|
+
controlOnboardingSeen: true,
|
|
250
|
+
});
|
|
251
|
+
(NativeModules as any).YaverDogfoodGesture.setEnabled.mockResolvedValueOnce({
|
|
252
|
+
supported: true,
|
|
253
|
+
enabled: false,
|
|
254
|
+
reason: 'supported',
|
|
255
|
+
platform: 'ios',
|
|
256
|
+
});
|
|
257
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
258
|
+
expect(state).toMatchObject({
|
|
259
|
+
gestureSupported: true,
|
|
260
|
+
gestureEnabled: false,
|
|
261
|
+
fallbackVisible: true,
|
|
262
|
+
reason: 'gesture-enable-failed',
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('persists first-run completion for the exact account app installation', async () => {
|
|
267
|
+
YaverFeedback.init({ authToken: 'owner-token', bundleId: 'io.example.app', dogfood: {} });
|
|
268
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
269
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
270
|
+
appId: 'io.example.app',
|
|
271
|
+
yaverAuthenticated: true,
|
|
272
|
+
ownerAuthorized: true,
|
|
273
|
+
accountAuthorized: true,
|
|
274
|
+
installationId: 'phone-1',
|
|
275
|
+
deviceState: 'active',
|
|
276
|
+
authorized: true,
|
|
277
|
+
controlOnboardingSeen: false,
|
|
278
|
+
});
|
|
279
|
+
const state = await YaverFeedback.setDogfoodControlPresentation('minimized-y');
|
|
280
|
+
expect(state).toMatchObject({ onboardingSeen: true, presentation: 'minimized-y', fallbackVisible: true });
|
|
281
|
+
expect(setDogfoodControlPreference).toHaveBeenCalledWith(expect.objectContaining({
|
|
282
|
+
appId: 'io.example.app',
|
|
283
|
+
installationId: 'phone-1',
|
|
284
|
+
token: 'owner-token',
|
|
285
|
+
presentation: 'minimized-y',
|
|
286
|
+
controlOnboardingSeen: true,
|
|
287
|
+
}));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('falls back to the minimized Y when accessibility owns multi-touch', async () => {
|
|
291
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
292
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = { durationMs: 1200 };
|
|
293
|
+
jest.spyOn(YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
294
|
+
appId: 'io.example.app',
|
|
295
|
+
yaverAuthenticated: true,
|
|
296
|
+
ownerAuthorized: false,
|
|
297
|
+
accountAuthorized: true,
|
|
298
|
+
installationId: 'phone-1',
|
|
299
|
+
deviceState: 'active',
|
|
300
|
+
authorized: true,
|
|
301
|
+
});
|
|
302
|
+
(NativeModules as any).YaverDogfoodGesture.getCapability.mockResolvedValueOnce({
|
|
303
|
+
supported: false,
|
|
304
|
+
enabled: false,
|
|
305
|
+
reason: 'accessibility-touch-exploration',
|
|
306
|
+
platform: 'ios',
|
|
307
|
+
});
|
|
308
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
309
|
+
expect(state).toMatchObject({ gestureSupported: false, gestureEnabled: false, fallbackVisible: true });
|
|
310
|
+
expect((NativeModules as any).YaverDogfoodGesture.setEnabled).toHaveBeenCalledWith(false, 1200);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('suppresses guest controls inside the Yaver split-view container', async () => {
|
|
314
|
+
YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
315
|
+
YaverFeedback.getConfig()!.dogfood!.controlGesture = true;
|
|
316
|
+
(NativeModules as any).YaverInfo = { isYaver: true };
|
|
317
|
+
const state = await YaverFeedback.syncDogfoodControlGesture();
|
|
318
|
+
delete (NativeModules as any).YaverInfo;
|
|
319
|
+
expect(state).toMatchObject({ gestureEnabled: false, fallbackVisible: false, reason: 'yaver-host-owns-controls' });
|
|
320
|
+
});
|
|
166
321
|
});
|
|
167
322
|
|
|
168
323
|
describe('init()', () => {
|