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.
- package/README.md +31 -16
- package/android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java +59 -0
- package/app.plugin.js +200 -3
- package/dist/AuthOverlay.js +13 -2
- package/dist/FeedbackModal.js +70 -7
- package/dist/YaverFeedback.d.ts +34 -9
- package/dist/YaverFeedback.js +304 -20
- package/dist/__tests__/NativeDogfoodShortcut.test.d.ts +1 -0
- package/dist/__tests__/NativeDogfoodShortcut.test.js +52 -0
- package/dist/__tests__/ReportIdentity.test.js +6 -3
- package/dist/__tests__/YaverFeedback.test.js +97 -4
- package/dist/__tests__/deviceDogfood.test.js +2 -2
- package/dist/__tests__/dogfoodPolicy.test.js +1 -1
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +28 -0
- package/dist/deviceDogfood.d.ts +6 -2
- package/dist/deviceDogfood.js +6 -3
- package/dist/dogfoodPolicy.d.ts +32 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -2
- package/dist/preferences.d.ts +2 -0
- package/dist/preferences.js +24 -0
- package/ios/YaverHotReload.m +8 -0
- package/ios/YaverHotReload.swift +37 -0
- package/package.json +2 -2
- package/src/AuthOverlay.tsx +11 -2
- package/src/FeedbackModal.tsx +98 -6
- package/src/YaverFeedback.ts +271 -19
- package/src/__tests__/NativeDogfoodShortcut.test.ts +59 -0
- package/src/__tests__/ReportIdentity.test.ts +6 -4
- package/src/__tests__/YaverFeedback.test.ts +104 -5
- package/src/__tests__/deviceDogfood.test.ts +2 -2
- package/src/__tests__/dogfoodPolicy.test.ts +1 -1
- package/src/auth.ts +29 -0
- package/src/deviceDogfood.ts +8 -3
- package/src/dogfoodPolicy.ts +32 -1
- package/src/index.ts +5 -2
- package/src/preferences.ts +20 -0
package/dist/YaverFeedback.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.YaverFeedback = void 0;
|
|
4
37
|
const react_native_1 = require("react-native");
|
|
@@ -155,6 +188,60 @@ let autoStartTimer = null;
|
|
|
155
188
|
let reportLaunchInFlight = false;
|
|
156
189
|
let crashReportInFlight = false;
|
|
157
190
|
let dogfoodOnboarding = null;
|
|
191
|
+
let dogfoodFlowState = { phase: 'idle' };
|
|
192
|
+
const dogfoodFlowListeners = new Set();
|
|
193
|
+
let dogfoodShortcutAppStateSubscription = null;
|
|
194
|
+
let dogfoodActivationSubscription = null;
|
|
195
|
+
let dogfoodShortcutLaunchInFlight = false;
|
|
196
|
+
let lastDogfoodActivationUrl = '';
|
|
197
|
+
function publishDogfoodFlow(state) {
|
|
198
|
+
dogfoodFlowState = state;
|
|
199
|
+
try {
|
|
200
|
+
config?.dogfood?.onStateChange?.(state);
|
|
201
|
+
}
|
|
202
|
+
catch { /* host callback */ }
|
|
203
|
+
dogfoodFlowListeners.forEach((listener) => {
|
|
204
|
+
try {
|
|
205
|
+
listener(state);
|
|
206
|
+
}
|
|
207
|
+
catch { /* host callbacks never break the SDK */ }
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function consumeNativeDogfoodShortcut() {
|
|
211
|
+
if (dogfoodShortcutLaunchInFlight || !config?.dogfood?.appShortcut)
|
|
212
|
+
return;
|
|
213
|
+
const native = react_native_1.NativeModules?.YaverHotReload;
|
|
214
|
+
if (typeof native?.consumeDogfoodShortcut !== 'function')
|
|
215
|
+
return;
|
|
216
|
+
try {
|
|
217
|
+
if (!(await native.consumeDogfoodShortcut()))
|
|
218
|
+
return;
|
|
219
|
+
dogfoodShortcutLaunchInFlight = true;
|
|
220
|
+
// Let FeedbackModal/AuthOverlay effects mount after a cold launch.
|
|
221
|
+
const timer = setTimeout(() => {
|
|
222
|
+
void YaverFeedback.openDogfood().finally(() => { dogfoodShortcutLaunchInFlight = false; });
|
|
223
|
+
}, 350);
|
|
224
|
+
unrefTimer(timer);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
dogfoodShortcutLaunchInFlight = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function handleDogfoodActivationUrl(url) {
|
|
231
|
+
if (!url || url === lastDogfoodActivationUrl || !config?.dogfood)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
const parsed = new URL(url);
|
|
235
|
+
if (!parsed.protocol.startsWith('yaver-dogfood-') || parsed.hostname !== 'activate')
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
lastDogfoodActivationUrl = url;
|
|
242
|
+
const timer = setTimeout(() => { void YaverFeedback.openDogfood(); }, 350);
|
|
243
|
+
unrefTimer(timer);
|
|
244
|
+
}
|
|
158
245
|
/** Resolve the user's relay password by validating their auth token
|
|
159
246
|
* against Convex. Used whenever we (re)build the P2PClient so a
|
|
160
247
|
* relay-routed agentUrl carries a valid X-Relay-Password — without
|
|
@@ -446,6 +533,32 @@ class YaverFeedback {
|
|
|
446
533
|
if (config.autoLogin !== false && enabled) {
|
|
447
534
|
void YaverFeedback.hydrateSession();
|
|
448
535
|
}
|
|
536
|
+
if (config.dogfood?.appShortcut) {
|
|
537
|
+
void YaverFeedback.syncDogfoodAppShortcut();
|
|
538
|
+
void consumeNativeDogfoodShortcut();
|
|
539
|
+
try {
|
|
540
|
+
const { AppState } = require('react-native');
|
|
541
|
+
dogfoodShortcutAppStateSubscription?.remove();
|
|
542
|
+
dogfoodShortcutAppStateSubscription = AppState.addEventListener('change', (state) => {
|
|
543
|
+
if (state === 'active') {
|
|
544
|
+
void YaverFeedback.syncDogfoodAppShortcut();
|
|
545
|
+
void consumeNativeDogfoodShortcut();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
catch { /* native shortcut unavailable on web/test runtimes */ }
|
|
550
|
+
}
|
|
551
|
+
if (config.dogfood) {
|
|
552
|
+
try {
|
|
553
|
+
const { Linking } = require('react-native');
|
|
554
|
+
dogfoodActivationSubscription?.remove();
|
|
555
|
+
dogfoodActivationSubscription = Linking.addEventListener('url', ({ url }) => {
|
|
556
|
+
handleDogfoodActivationUrl(url);
|
|
557
|
+
});
|
|
558
|
+
void Linking.getInitialURL().then(handleDogfoodActivationUrl).catch(() => { });
|
|
559
|
+
}
|
|
560
|
+
catch { /* linking unavailable in non-native test runtimes */ }
|
|
561
|
+
}
|
|
449
562
|
// Create P2P client if we have a URL
|
|
450
563
|
if (config.agentUrl) {
|
|
451
564
|
p2pAuthToken = config.authToken ?? null;
|
|
@@ -640,7 +753,7 @@ class YaverFeedback {
|
|
|
640
753
|
* Sets config.agentUrl and creates P2PClient on success.
|
|
641
754
|
*/
|
|
642
755
|
static async discoverAgent() {
|
|
643
|
-
if (!config || !enabled)
|
|
756
|
+
if (!config || (!enabled && !dogfoodOnboarding))
|
|
644
757
|
return;
|
|
645
758
|
if (config.agentUrl)
|
|
646
759
|
return; // already have a URL
|
|
@@ -671,7 +784,7 @@ class YaverFeedback {
|
|
|
671
784
|
* Returns true when a new URL was adopted.
|
|
672
785
|
*/
|
|
673
786
|
static async reconnect() {
|
|
674
|
-
if (!config || !enabled)
|
|
787
|
+
if (!config || (!enabled && !dogfoodOnboarding))
|
|
675
788
|
return false;
|
|
676
789
|
if (!config.authToken || !config.convexUrl)
|
|
677
790
|
return false;
|
|
@@ -741,6 +854,7 @@ class YaverFeedback {
|
|
|
741
854
|
if (config.autoStartBlackBox !== false && !BlackBox_1.BlackBox.isStreaming) {
|
|
742
855
|
YaverFeedback.scheduleBlackBoxAutoStart();
|
|
743
856
|
}
|
|
857
|
+
await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
|
|
744
858
|
}
|
|
745
859
|
/** Returns true once the SDK has a session token it can use. */
|
|
746
860
|
static isAuthed() {
|
|
@@ -812,24 +926,186 @@ class YaverFeedback {
|
|
|
812
926
|
const { DeviceEventEmitter } = require('react-native');
|
|
813
927
|
DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
|
|
814
928
|
}
|
|
815
|
-
/**
|
|
816
|
-
*
|
|
817
|
-
|
|
818
|
-
static beginDogfoodOnboarding(options) {
|
|
929
|
+
/** Configure Dogfood once during host init. Hosts still decide whether and
|
|
930
|
+
* where to render an affordance; `openDogfood()` owns all flow mechanics. */
|
|
931
|
+
static configureDogfood(options) {
|
|
819
932
|
dogfoodOnboarding = options;
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Open Dogfood using config.dogfood + the normal app identity. Cached OAuth,
|
|
936
|
+
* machine, runner and model choices are reused. The corresponding picker is
|
|
937
|
+
* shown only when a required choice is missing.
|
|
938
|
+
*/
|
|
939
|
+
static async openDogfood(overrides) {
|
|
940
|
+
if (!config && overrides?.appId) {
|
|
941
|
+
return YaverFeedback.beginDogfoodOnboarding(overrides);
|
|
942
|
+
}
|
|
943
|
+
const configured = config?.dogfood;
|
|
944
|
+
const appId = overrides?.appId || configured?.appId || config?.bundleId;
|
|
945
|
+
if (!appId) {
|
|
946
|
+
const state = { phase: 'error', error: 'Dogfood requires an appId or FeedbackConfig.bundleId.' };
|
|
947
|
+
publishDogfoodFlow(state);
|
|
948
|
+
return state;
|
|
949
|
+
}
|
|
950
|
+
dogfoodOnboarding = {
|
|
951
|
+
appId,
|
|
952
|
+
label: overrides?.label || configured?.label || config?.projectName || appId,
|
|
953
|
+
projectName: overrides?.projectName || configured?.projectName || config?.projectName,
|
|
954
|
+
framework: overrides?.framework || configured?.framework,
|
|
955
|
+
backendUrl: overrides?.backendUrl || configured?.backendUrl,
|
|
956
|
+
secureStore: overrides?.secureStore,
|
|
957
|
+
};
|
|
958
|
+
if (configured?.canShow) {
|
|
959
|
+
// This hook is presentation policy, but it still receives the complete
|
|
960
|
+
// backend-authoritative snapshot. Passing an owner-only approximation
|
|
961
|
+
// here made legitimate approved testers fail custom `access.authorized`
|
|
962
|
+
// gates even though their exact phone key was active.
|
|
963
|
+
const access = await YaverFeedback.getDogfoodAccess();
|
|
964
|
+
try {
|
|
965
|
+
if (!(await configured.canShow(access))) {
|
|
966
|
+
const state = { phase: 'denied', appId };
|
|
967
|
+
publishDogfoodFlow(state);
|
|
968
|
+
return state;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch (cause) {
|
|
972
|
+
const state = {
|
|
973
|
+
phase: 'error',
|
|
974
|
+
appId,
|
|
975
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
976
|
+
};
|
|
977
|
+
publishDogfoodFlow(state);
|
|
978
|
+
return state;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return YaverFeedback.continueDogfoodOnboarding();
|
|
982
|
+
}
|
|
983
|
+
/** Resolve the host-facing ACL snapshot without granting authority. This is
|
|
984
|
+
* the one endpoint custom Settings screens need for visibility/status UI. */
|
|
985
|
+
static async getDogfoodAccess() {
|
|
986
|
+
const configured = config?.dogfood;
|
|
987
|
+
const appId = dogfoodOnboarding?.appId || configured?.appId || config?.bundleId;
|
|
988
|
+
if (!appId)
|
|
989
|
+
throw new Error('Dogfood requires an appId or FeedbackConfig.bundleId.');
|
|
990
|
+
let installationId;
|
|
991
|
+
let deviceState = 'unknown';
|
|
992
|
+
try {
|
|
993
|
+
const device = new deviceDogfood_1.YaverDeviceDogfood({
|
|
994
|
+
appId,
|
|
995
|
+
label: dogfoodOnboarding?.label || configured?.label || config?.projectName,
|
|
996
|
+
backendUrl: dogfoodOnboarding?.backendUrl || configured?.backendUrl,
|
|
997
|
+
secureStore: dogfoodOnboarding?.secureStore,
|
|
998
|
+
});
|
|
999
|
+
installationId = (await device.enrollmentInfo()).installationId;
|
|
1000
|
+
deviceState = await device.status();
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
// UI status must degrade to unknown; openDogfood still offers owner OAuth.
|
|
1004
|
+
}
|
|
1005
|
+
const token = config?.authToken || await (0, auth_1.getToken)();
|
|
1006
|
+
const account = token
|
|
1007
|
+
? await (0, auth_1.getDogfoodAccountAccess)(appId, token, installationId)
|
|
1008
|
+
: { authenticated: false, ownerAuthorized: false, installationAuthorized: false };
|
|
1009
|
+
const yaverAuthenticated = account.authenticated;
|
|
1010
|
+
const ownerAuthorized = account.ownerAuthorized;
|
|
1011
|
+
return {
|
|
1012
|
+
appId,
|
|
1013
|
+
yaverAuthenticated,
|
|
1014
|
+
ownerAuthorized,
|
|
1015
|
+
installationId,
|
|
1016
|
+
deviceState,
|
|
1017
|
+
authorized: yaverAuthenticated && account.installationAuthorized && deviceState === 'active',
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/** Add/remove the platform Home Screen shortcut from backend-authoritative
|
|
1021
|
+
* owner/device ACL state. Static plist shortcuts are intentionally avoided:
|
|
1022
|
+
* an unauthorized install must never advertise a hidden developer action. */
|
|
1023
|
+
static async syncDogfoodAppShortcut() {
|
|
1024
|
+
const shortcut = config?.dogfood?.appShortcut;
|
|
1025
|
+
const native = react_native_1.NativeModules?.YaverHotReload;
|
|
1026
|
+
if (!shortcut || typeof native?.setDogfoodShortcut !== 'function')
|
|
1027
|
+
return false;
|
|
1028
|
+
const access = await YaverFeedback.getDogfoodAccess();
|
|
1029
|
+
let visible = access.authorized;
|
|
1030
|
+
if (visible && config?.dogfood?.canShow) {
|
|
1031
|
+
try {
|
|
1032
|
+
visible = await config.dogfood.canShow(access);
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
visible = false;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const label = typeof shortcut === 'object' && shortcut.label
|
|
1039
|
+
? shortcut.label
|
|
1040
|
+
: `Dogfood ${config?.dogfood?.label || config?.projectName || ''}`.trim();
|
|
1041
|
+
// Product contract: both a valid full Yaver account AND this phone's
|
|
1042
|
+
// backend-approved app installation are required. Neither factor alone
|
|
1043
|
+
// advertises the developer surface.
|
|
1044
|
+
await native.setDogfoodShortcut(visible, label || 'Dogfood');
|
|
1045
|
+
return visible;
|
|
1046
|
+
}
|
|
1047
|
+
/** Backwards-compatible entry point for existing integrations. */
|
|
1048
|
+
static async beginDogfoodOnboarding(options) {
|
|
1049
|
+
YaverFeedback.configureDogfood(options);
|
|
1050
|
+
if (!config) {
|
|
1051
|
+
YaverFeedback.init({
|
|
1052
|
+
autoLogin: true,
|
|
1053
|
+
enabled: true,
|
|
1054
|
+
projectName: options.projectName || options.label,
|
|
1055
|
+
bundleId: options.appId,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return YaverFeedback.continueDogfoodOnboarding();
|
|
1059
|
+
}
|
|
1060
|
+
/** Continue after OAuth or machine selection. Public so custom host UI can
|
|
1061
|
+
* hand control back without recreating the SDK state machine. */
|
|
1062
|
+
static async continueDogfoodOnboarding() {
|
|
1063
|
+
const appId = dogfoodOnboarding?.appId;
|
|
1064
|
+
if (!dogfoodOnboarding || !appId) {
|
|
1065
|
+
const state = { phase: 'error', error: 'Dogfood is not configured.' };
|
|
1066
|
+
publishDogfoodFlow(state);
|
|
1067
|
+
return state;
|
|
1068
|
+
}
|
|
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 };
|
|
1074
|
+
// A device key is a second factor for this installation, never a
|
|
1075
|
+
// replacement for a real Yaver account. Narrow installation sessions and
|
|
1076
|
+
// stale/invalid cached tokens both return authenticated=false here.
|
|
1077
|
+
if (!account.authenticated) {
|
|
1078
|
+
const state = { phase: 'auth-required', appId };
|
|
1079
|
+
publishDogfoodFlow(state);
|
|
823
1080
|
YaverFeedback.showLogin();
|
|
824
|
-
return;
|
|
1081
|
+
return state;
|
|
1082
|
+
}
|
|
1083
|
+
if (!config?.preferredDeviceId) {
|
|
1084
|
+
const state = { phase: 'machine-required', appId };
|
|
1085
|
+
publishDogfoodFlow(state);
|
|
1086
|
+
YaverFeedback.showMachinePicker();
|
|
1087
|
+
return state;
|
|
825
1088
|
}
|
|
826
|
-
|
|
1089
|
+
const state = { phase: 'opening', appId };
|
|
1090
|
+
publishDogfoodFlow(state);
|
|
1091
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
1092
|
+
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
1093
|
+
return state;
|
|
1094
|
+
}
|
|
1095
|
+
static getDogfoodFlowState() {
|
|
1096
|
+
return dogfoodFlowState;
|
|
1097
|
+
}
|
|
1098
|
+
static onDogfoodFlowState(listener) {
|
|
1099
|
+
dogfoodFlowListeners.add(listener);
|
|
1100
|
+
listener(dogfoodFlowState);
|
|
1101
|
+
return () => dogfoodFlowListeners.delete(listener);
|
|
827
1102
|
}
|
|
828
1103
|
static getDogfoodOnboarding() {
|
|
829
1104
|
return dogfoodOnboarding;
|
|
830
1105
|
}
|
|
831
1106
|
static clearDogfoodOnboarding() {
|
|
832
1107
|
dogfoodOnboarding = null;
|
|
1108
|
+
publishDogfoodFlow({ phase: 'idle' });
|
|
833
1109
|
}
|
|
834
1110
|
/**
|
|
835
1111
|
* Update the selected remote device. Resets the cached agent URL so the
|
|
@@ -853,6 +1129,7 @@ class YaverFeedback {
|
|
|
853
1129
|
p2pClient = null;
|
|
854
1130
|
renderP2PClient = null;
|
|
855
1131
|
p2pAuthToken = null;
|
|
1132
|
+
await Promise.resolve().then(() => __importStar(require('./auth'))).then(({ saveSelectedDeviceId }) => saveSelectedDeviceId(deviceId));
|
|
856
1133
|
await YaverFeedback.discoverAgent();
|
|
857
1134
|
}
|
|
858
1135
|
/** Resolve the currently selected remote machine from the authenticated device list. */
|
|
@@ -913,6 +1190,7 @@ class YaverFeedback {
|
|
|
913
1190
|
p2pClient = null;
|
|
914
1191
|
renderP2PClient = null;
|
|
915
1192
|
p2pAuthToken = null;
|
|
1193
|
+
await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
|
|
916
1194
|
}
|
|
917
1195
|
/**
|
|
918
1196
|
* Manually trigger the feedback collection flow.
|
|
@@ -1161,12 +1439,12 @@ class YaverFeedback {
|
|
|
1161
1439
|
static getDogfoodStatus() {
|
|
1162
1440
|
return (0, dogfoodPolicy_1.resolveSDKDogfood)(config?.dogfood);
|
|
1163
1441
|
}
|
|
1164
|
-
/** One-call
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1442
|
+
/** One-call account-bound Dogfood bootstrap for third-party apps. The host
|
|
1443
|
+
* app needs no auth backend of its own: SDK OAuth supplies the full Yaver
|
|
1444
|
+
* account, then this creates/proves the installation key. Owner approval
|
|
1445
|
+
* binds that account + appId + phone key before a scoped session is minted. */
|
|
1168
1446
|
static async enableDeviceDogfood(options) {
|
|
1169
|
-
const client = new deviceDogfood_1.YaverDeviceDogfood(options);
|
|
1447
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ ...options, authToken: options.authToken || config?.authToken });
|
|
1170
1448
|
let status = await client.status();
|
|
1171
1449
|
if (status === 'unregistered' || status === 'pending' || status === 'cancelled' || status === 'revoked' || status === 'superseded') {
|
|
1172
1450
|
const enrolled = status === 'unregistered' || status === 'pending'
|
|
@@ -1180,22 +1458,23 @@ class YaverFeedback {
|
|
|
1180
1458
|
YaverFeedback.init({ autoLogin: false });
|
|
1181
1459
|
if (config) {
|
|
1182
1460
|
config.dogfood = {
|
|
1461
|
+
...config.dogfood,
|
|
1183
1462
|
enabled: status === 'active' && !!session,
|
|
1184
1463
|
appId: options.appId,
|
|
1185
1464
|
installationId: info.installationId,
|
|
1186
1465
|
installationStatus: status === 'active' && session ? 'active' : status === 'unregistered' ? 'pending' : status,
|
|
1187
1466
|
label: options.label,
|
|
1188
1467
|
};
|
|
1189
|
-
// A normal Yaver OAuth session remains authoritative for the
|
|
1190
|
-
// wizard.
|
|
1191
|
-
|
|
1192
|
-
await YaverFeedback.setAuthToken(session.token);
|
|
1468
|
+
// A normal full Yaver OAuth session remains authoritative for the
|
|
1469
|
+
// runtime wizard. The narrow installation token is intentionally never
|
|
1470
|
+
// promoted into account auth.
|
|
1193
1471
|
}
|
|
1194
1472
|
try {
|
|
1195
1473
|
const { DeviceEventEmitter } = require('react-native');
|
|
1196
1474
|
DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: !!session, status });
|
|
1197
1475
|
}
|
|
1198
1476
|
catch { /* noop */ }
|
|
1477
|
+
await YaverFeedback.syncDogfoodAppShortcut().catch(() => false);
|
|
1199
1478
|
return { status, installationId: info.installationId, session };
|
|
1200
1479
|
}
|
|
1201
1480
|
/** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
|
|
@@ -1705,6 +1984,11 @@ class YaverFeedback {
|
|
|
1705
1984
|
// init() stacked a second one on top.
|
|
1706
1985
|
commandUnsubscribe?.();
|
|
1707
1986
|
commandUnsubscribe = null;
|
|
1987
|
+
dogfoodShortcutAppStateSubscription?.remove();
|
|
1988
|
+
dogfoodShortcutAppStateSubscription = null;
|
|
1989
|
+
dogfoodActivationSubscription?.remove();
|
|
1990
|
+
dogfoodActivationSubscription = null;
|
|
1991
|
+
lastDogfoodActivationUrl = '';
|
|
1708
1992
|
// Before `enabled = false` / `config = null` below, so an in-flight retry
|
|
1709
1993
|
// can't fire against a torn-down config.
|
|
1710
1994
|
YaverFeedback.cancelBlackBoxAutoStart();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs_1 = require("fs");
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const plugin = require('../../app.plugin.js');
|
|
6
|
+
describe('native Dogfood shortcut contract', () => {
|
|
7
|
+
it('patches cold and warm iOS shortcut delivery idempotently', () => {
|
|
8
|
+
const source = `
|
|
9
|
+
public class AppDelegate: ExpoAppDelegate {
|
|
10
|
+
public override func application(
|
|
11
|
+
_ application: UIApplication,
|
|
12
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
13
|
+
) -> Bool {
|
|
14
|
+
setupYaverHotReload()
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
const once = plugin.__test.patchDogfoodAppShortcut(source);
|
|
20
|
+
expect(once).toContain('launchOptions?[.shortcutItem]');
|
|
21
|
+
expect(once).toContain('performActionFor shortcutItem');
|
|
22
|
+
expect(once).toContain('markDogfoodShortcutPending()');
|
|
23
|
+
expect(plugin.__test.patchDogfoodAppShortcut(once)).toBe(once);
|
|
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
|
+
});
|
|
43
|
+
it('uses dynamic native shortcuts on both platforms', () => {
|
|
44
|
+
const ios = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../ios/YaverHotReload.swift'), 'utf8');
|
|
45
|
+
const android = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../../android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java'), 'utf8');
|
|
46
|
+
expect(ios).toContain('UIApplication.shared.shortcutItems');
|
|
47
|
+
expect(ios).toContain('setDogfoodShortcut');
|
|
48
|
+
expect(android).toContain('ShortcutManager');
|
|
49
|
+
expect(android).toContain('addDynamicShortcuts');
|
|
50
|
+
expect(android).toContain('consumeDogfoodShortcut');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -9,8 +9,6 @@ const mockReactNative = {
|
|
|
9
9
|
NativeModules: {},
|
|
10
10
|
};
|
|
11
11
|
const mockExpoModule = { default: {} };
|
|
12
|
-
jest.mock('react-native', () => mockReactNative);
|
|
13
|
-
jest.mock('expo-constants', () => mockExpoModule, { virtual: true });
|
|
14
12
|
/** Mirrors mobile/app.json in the Talos repo — the SDK's first external consumer. */
|
|
15
13
|
const EXPO_CONFIG = {
|
|
16
14
|
name: 'Talos',
|
|
@@ -25,8 +23,13 @@ function mockExpoConstants(expoConfig, extra = {}) {
|
|
|
25
23
|
function loadResolve() {
|
|
26
24
|
let resolve;
|
|
27
25
|
jest.isolateModules(() => {
|
|
26
|
+
// Configure the mocks inside the isolated registry. Top-level virtual
|
|
27
|
+
// mocks are sticky across Jest's worker registry and lost to the real
|
|
28
|
+
// expo-constants package after a clean npm ci, making this suite depend on
|
|
29
|
+
// file order. doMock is non-hoisted and binds these exact mutable objects
|
|
30
|
+
// immediately before P2PClient is evaluated.
|
|
28
31
|
jest.doMock('react-native', () => mockReactNative);
|
|
29
|
-
jest.doMock('expo-constants', () => mockExpoModule
|
|
32
|
+
jest.doMock('expo-constants', () => mockExpoModule);
|
|
30
33
|
resolve = require('../P2PClient').resolveReportIdentity;
|
|
31
34
|
});
|
|
32
35
|
return resolve;
|
|
@@ -10,6 +10,13 @@ jest.mock('react-native', () => ({
|
|
|
10
10
|
addListener: jest.fn(() => ({ remove: jest.fn() })),
|
|
11
11
|
},
|
|
12
12
|
Platform: { OS: 'ios' },
|
|
13
|
+
NativeModules: {
|
|
14
|
+
YaverHotReload: {
|
|
15
|
+
setDogfoodShortcut: jest.fn(async () => true),
|
|
16
|
+
consumeDogfoodShortcut: jest.fn(async () => false),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
AppState: { addEventListener: jest.fn(() => ({ remove: jest.fn() })) },
|
|
13
20
|
}));
|
|
14
21
|
// Mock Discovery
|
|
15
22
|
jest.mock('../Discovery', () => ({
|
|
@@ -22,6 +29,12 @@ jest.mock('../auth', () => ({
|
|
|
22
29
|
setStrictNativeAuth: jest.fn(),
|
|
23
30
|
getToken: jest.fn(async () => null),
|
|
24
31
|
getSelectedDeviceId: jest.fn(async () => null),
|
|
32
|
+
getDogfoodAccountAccess: jest.fn(async (_appId, token) => ({
|
|
33
|
+
authenticated: token === 'owner-token',
|
|
34
|
+
ownerAuthorized: token === 'owner-token',
|
|
35
|
+
installationAuthorized: token === 'owner-token',
|
|
36
|
+
})),
|
|
37
|
+
saveSelectedDeviceId: jest.fn(async () => { }),
|
|
25
38
|
clearToken: jest.fn(async () => { }),
|
|
26
39
|
clearSelectedDeviceId: jest.fn(async () => { }),
|
|
27
40
|
listReachableDevices: jest.fn(async () => ({
|
|
@@ -46,20 +59,100 @@ beforeEach(() => {
|
|
|
46
59
|
// YaverFeedback uses module-level variables (config, enabled, p2pClient).
|
|
47
60
|
// We reset them by calling init with a known state or relying on isInitialized checks.
|
|
48
61
|
// For a clean slate, we re-init with enabled=false then verify.
|
|
62
|
+
jest.restoreAllMocks();
|
|
49
63
|
jest.clearAllMocks();
|
|
50
64
|
});
|
|
51
65
|
describe('YaverFeedback', () => {
|
|
52
66
|
describe('Dogfood onboarding', () => {
|
|
53
|
-
it('starts with Yaver OAuth when the host has no session', () => {
|
|
67
|
+
it('starts with Yaver OAuth when the host has no session', async () => {
|
|
54
68
|
YaverFeedback_1.YaverFeedback.init({ enabled: true });
|
|
55
|
-
YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
69
|
+
await YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
56
70
|
expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startLogin');
|
|
57
71
|
});
|
|
58
|
-
it('asks for a machine after an existing OAuth session', () => {
|
|
72
|
+
it('asks for a machine after an existing OAuth session', async () => {
|
|
59
73
|
YaverFeedback_1.YaverFeedback.init({ enabled: true, authToken: 'owner-token' });
|
|
60
|
-
YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
74
|
+
await YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app', label: 'Example' });
|
|
61
75
|
expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startMachinePicker');
|
|
62
76
|
});
|
|
77
|
+
it('reuses the configured app identity and selected machine', async () => {
|
|
78
|
+
YaverFeedback_1.YaverFeedback.init({
|
|
79
|
+
enabled: true,
|
|
80
|
+
authToken: 'owner-token',
|
|
81
|
+
preferredDeviceId: 'device-1',
|
|
82
|
+
projectName: 'Example',
|
|
83
|
+
bundleId: 'io.example.app',
|
|
84
|
+
dogfood: { framework: 'expo' },
|
|
85
|
+
});
|
|
86
|
+
const state = await YaverFeedback_1.YaverFeedback.openDogfood();
|
|
87
|
+
expect(state).toEqual({ phase: 'opening', appId: 'io.example.app' });
|
|
88
|
+
expect(react_native_1.DeviceEventEmitter.emit).toHaveBeenCalledWith('yaverFeedback:startReport');
|
|
89
|
+
expect(react_native_1.DeviceEventEmitter.emit).not.toHaveBeenCalledWith('yaverFeedback:startMachinePicker');
|
|
90
|
+
});
|
|
91
|
+
it('publishes flow state for custom host UI', async () => {
|
|
92
|
+
YaverFeedback_1.YaverFeedback.init({ enabled: true });
|
|
93
|
+
const states = [];
|
|
94
|
+
const unsubscribe = YaverFeedback_1.YaverFeedback.onDogfoodFlowState((state) => states.push(state.phase));
|
|
95
|
+
await YaverFeedback_1.YaverFeedback.beginDogfoodOnboarding({ appId: 'io.example.app' });
|
|
96
|
+
unsubscribe();
|
|
97
|
+
expect(states[states.length - 1]).toBe('auth-required');
|
|
98
|
+
});
|
|
99
|
+
it('lets a host ACL hide its affordance without opening auth UI', async () => {
|
|
100
|
+
YaverFeedback_1.YaverFeedback.init({
|
|
101
|
+
enabled: true,
|
|
102
|
+
bundleId: 'io.example.app',
|
|
103
|
+
dogfood: { canShow: () => false },
|
|
104
|
+
});
|
|
105
|
+
const state = await YaverFeedback_1.YaverFeedback.openDogfood();
|
|
106
|
+
expect(state).toEqual({ phase: 'denied', appId: 'io.example.app' });
|
|
107
|
+
expect(react_native_1.DeviceEventEmitter.emit).not.toHaveBeenCalledWith('yaverFeedback:startLogin');
|
|
108
|
+
});
|
|
109
|
+
it('adds the app shortcut only for a signed-in and approved phone', async () => {
|
|
110
|
+
YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
111
|
+
YaverFeedback_1.YaverFeedback.getConfig().dogfood.appShortcut = { label: 'Dogfood Example' };
|
|
112
|
+
jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
113
|
+
appId: 'io.example.app',
|
|
114
|
+
yaverAuthenticated: true,
|
|
115
|
+
ownerAuthorized: false,
|
|
116
|
+
installationId: 'phone-1',
|
|
117
|
+
deviceState: 'active',
|
|
118
|
+
authorized: true,
|
|
119
|
+
});
|
|
120
|
+
await YaverFeedback_1.YaverFeedback.syncDogfoodAppShortcut();
|
|
121
|
+
expect(react_native_1.NativeModules.YaverHotReload.setDogfoodShortcut)
|
|
122
|
+
.toHaveBeenCalledWith(true, 'Dogfood Example');
|
|
123
|
+
});
|
|
124
|
+
it('removes the shortcut when the phone is approved but Yaver is signed out', async () => {
|
|
125
|
+
YaverFeedback_1.YaverFeedback.init({ bundleId: 'io.example.app', dogfood: {} });
|
|
126
|
+
YaverFeedback_1.YaverFeedback.getConfig().dogfood.appShortcut = true;
|
|
127
|
+
jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
128
|
+
appId: 'io.example.app',
|
|
129
|
+
yaverAuthenticated: false,
|
|
130
|
+
ownerAuthorized: false,
|
|
131
|
+
installationId: 'phone-1',
|
|
132
|
+
deviceState: 'active',
|
|
133
|
+
authorized: false,
|
|
134
|
+
});
|
|
135
|
+
await YaverFeedback_1.YaverFeedback.syncDogfoodAppShortcut();
|
|
136
|
+
expect(react_native_1.NativeModules.YaverHotReload.setDogfoodShortcut)
|
|
137
|
+
.toHaveBeenCalledWith(false, 'Dogfood');
|
|
138
|
+
});
|
|
139
|
+
it('applies the host presentation ACL after backend device authorization', async () => {
|
|
140
|
+
YaverFeedback_1.YaverFeedback.init({
|
|
141
|
+
bundleId: 'io.example.app',
|
|
142
|
+
dogfood: { appShortcut: true, canShow: async () => false },
|
|
143
|
+
});
|
|
144
|
+
jest.spyOn(YaverFeedback_1.YaverFeedback, 'getDogfoodAccess').mockResolvedValueOnce({
|
|
145
|
+
appId: 'io.example.app',
|
|
146
|
+
yaverAuthenticated: true,
|
|
147
|
+
ownerAuthorized: false,
|
|
148
|
+
installationId: 'phone-1',
|
|
149
|
+
deviceState: 'active',
|
|
150
|
+
authorized: true,
|
|
151
|
+
});
|
|
152
|
+
await YaverFeedback_1.YaverFeedback.syncDogfoodAppShortcut();
|
|
153
|
+
expect(react_native_1.NativeModules.YaverHotReload.setDogfoodShortcut)
|
|
154
|
+
.toHaveBeenCalledWith(false, 'Dogfood');
|
|
155
|
+
});
|
|
63
156
|
});
|
|
64
157
|
describe('init()', () => {
|
|
65
158
|
it('sets config correctly with defaults', () => {
|
|
@@ -26,7 +26,7 @@ describe('YaverDeviceDogfood', () => {
|
|
|
26
26
|
afterEach(() => { jest.restoreAllMocks(); });
|
|
27
27
|
test('keeps identity stable and proves possession rather than trusting the UUID', async () => {
|
|
28
28
|
const store = new MemorySecureStore();
|
|
29
|
-
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
29
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', authToken: 'full-yaver-token', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
30
30
|
const first = await client.enrollmentInfo();
|
|
31
31
|
const second = await client.enrollmentInfo();
|
|
32
32
|
expect(second).toEqual(first);
|
|
@@ -49,7 +49,7 @@ describe('YaverDeviceDogfood', () => {
|
|
|
49
49
|
});
|
|
50
50
|
test('re-register rotates key and installation while preserving only the logical slot', async () => {
|
|
51
51
|
const store = new MemorySecureStore();
|
|
52
|
-
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
52
|
+
const client = new deviceDogfood_1.YaverDeviceDogfood({ appId: 'io.example.test', authToken: 'full-yaver-token', secureStore: store, backendUrl: 'https://dogfood.test' });
|
|
53
53
|
const before = await client.enrollmentInfo();
|
|
54
54
|
jest.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
|
55
55
|
if (String(input).endsWith('/dogfood/enroll/start'))
|
|
@@ -19,7 +19,7 @@ describe('resolveSDKDogfood', () => {
|
|
|
19
19
|
accountId: 'acct-1',
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
-
it('supports
|
|
22
|
+
it('supports an account-bound key-enrolled installation without an app backend', () => {
|
|
23
23
|
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationStatus: 'active' }).code)
|
|
24
24
|
.toBe('SDK_DOGFOOD_INSTALLATION_REQUIRED');
|
|
25
25
|
expect((0, dogfoodPolicy_1.resolveSDKDogfood)({ enabled: true, appId: 'io.example', installationId: 'install-1', installationStatus: 'pending' }).code)
|