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/README.md
CHANGED
|
@@ -113,31 +113,46 @@ still require the user's normal Yaver authentication.
|
|
|
113
113
|
|
|
114
114
|
### Device-enrolled Dogfood for apps without their own OAuth/backend
|
|
115
115
|
|
|
116
|
-
Mount `FeedbackModal
|
|
117
|
-
Settings
|
|
116
|
+
Mount `FeedbackModal` once and configure the ACL-backed dynamic app shortcut.
|
|
117
|
+
No permanent Settings row or shake gesture is required:
|
|
118
118
|
|
|
119
119
|
```tsx
|
|
120
|
-
YaverFeedback.
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
YaverFeedback.init({
|
|
121
|
+
enabled: true,
|
|
122
|
+
trigger: 'manual',
|
|
123
|
+
quickIcon: 'off',
|
|
124
|
+
disableShakeGesture: true,
|
|
125
|
+
bundleId: 'io.example.app',
|
|
123
126
|
projectName: 'example',
|
|
124
|
-
|
|
127
|
+
dogfood: {
|
|
128
|
+
label: 'Example',
|
|
129
|
+
framework: 'expo',
|
|
130
|
+
appShortcut: { label: 'Dogfood Example' },
|
|
131
|
+
// Optional host presentation ACL. Backend account + phone-key approval
|
|
132
|
+
// remains mandatory even when this returns true.
|
|
133
|
+
canShow: (access) => appUser.isAdmin && access.authorized,
|
|
134
|
+
},
|
|
125
135
|
});
|
|
126
136
|
```
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
For first-time setup, a signed-in user opens the app from Yaver's Dogfood app
|
|
139
|
+
catalog. The public deep link contains no token. The SDK runs stateful Yaver
|
|
140
|
+
OAuth, machine, coding runner/model, project, and Browser/Hermes/WebRTC setup.
|
|
141
|
+
After the owner approves that exact account + app ID + phone key, iOS and
|
|
142
|
+
Android add the long-press shortcut. Signing out, revocation, or host ACL denial
|
|
143
|
+
removes it on the next foreground sync.
|
|
144
|
+
|
|
145
|
+
The app creates a random installation ID and Ed25519 key inside SecureStore.
|
|
146
|
+
The ID is a public handle, not a credential: enrollment and each short session
|
|
147
|
+
require a server challenge signed by the private key. An owner approves,
|
|
148
|
+
cancels, or revokes from Yaver mobile, CLI, or MCP. Re-registering rotates the
|
|
149
|
+
key/ID, preserves only the logical local slot, and atomically supersedes that
|
|
150
|
+
slot's prior active generation when the replacement is approved, without
|
|
151
|
+
disabling another phone.
|
|
137
152
|
|
|
138
153
|
Install `expo-secure-store` and `expo-crypto` in Expo hosts. Bare React Native
|
|
139
154
|
hosts may provide a `secureStore` implementation. Runtime/build controls still
|
|
140
|
-
require full Yaver OAuth; the account-
|
|
155
|
+
require full Yaver OAuth; the account-bound installation token defaults to
|
|
141
156
|
`feedback` + `blackbox` and cannot be rotated into a long-lived owner token.
|
|
142
157
|
|
|
143
158
|
### Embeddable Dogfood runtime
|
|
@@ -2,7 +2,12 @@ package io.yaver.feedback;
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity;
|
|
4
4
|
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
5
6
|
import android.content.SharedPreferences;
|
|
7
|
+
import android.content.pm.ShortcutInfo;
|
|
8
|
+
import android.content.pm.ShortcutManager;
|
|
9
|
+
import android.graphics.drawable.Icon;
|
|
10
|
+
import android.os.Build;
|
|
6
11
|
import android.os.Handler;
|
|
7
12
|
import android.os.Looper;
|
|
8
13
|
import android.util.Log;
|
|
@@ -25,6 +30,7 @@ import java.io.InputStream;
|
|
|
25
30
|
import java.net.HttpURLConnection;
|
|
26
31
|
import java.net.URL;
|
|
27
32
|
import java.util.concurrent.Executors;
|
|
33
|
+
import java.util.Collections;
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* Hot reload native module for the Yaver Feedback SDK (Android).
|
|
@@ -45,6 +51,8 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
|
|
|
45
51
|
private static final String PREFS_KEY_BOOT_ATTEMPTS = "boot_attempts";
|
|
46
52
|
private static final String PREFS_KEY_BUNDLE_MTIME = "bundle_mtime";
|
|
47
53
|
private static final int MAX_BOOT_ATTEMPTS = 3;
|
|
54
|
+
private static final String DOGFOOD_SHORTCUT_ID = "io.yaver.feedback.dogfood";
|
|
55
|
+
private static final String DOGFOOD_SHORTCUT_EXTRA = "io.yaver.feedback.DOGFOOD";
|
|
48
56
|
|
|
49
57
|
public YaverHotReloadModule(ReactApplicationContext context) {
|
|
50
58
|
super(context);
|
|
@@ -153,6 +161,57 @@ public class YaverHotReloadModule extends ReactContextBaseJavaModule {
|
|
|
153
161
|
promise.resolve(true);
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
/** Dynamic shortcut: only JS, after backend ACL resolution, may add it. */
|
|
165
|
+
@ReactMethod
|
|
166
|
+
public void setDogfoodShortcut(boolean enabled, String label, Promise promise) {
|
|
167
|
+
try {
|
|
168
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
|
169
|
+
promise.resolve(false);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
Context context = getReactApplicationContext();
|
|
173
|
+
ShortcutManager manager = context.getSystemService(ShortcutManager.class);
|
|
174
|
+
if (manager == null) {
|
|
175
|
+
promise.resolve(false);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
manager.removeDynamicShortcuts(Collections.singletonList(DOGFOOD_SHORTCUT_ID));
|
|
179
|
+
if (enabled) {
|
|
180
|
+
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
|
181
|
+
if (intent == null) throw new IllegalStateException("App launch intent unavailable");
|
|
182
|
+
intent.setAction(DOGFOOD_SHORTCUT_EXTRA);
|
|
183
|
+
intent.putExtra(DOGFOOD_SHORTCUT_EXTRA, true);
|
|
184
|
+
ShortcutInfo shortcut = new ShortcutInfo.Builder(context, DOGFOOD_SHORTCUT_ID)
|
|
185
|
+
.setShortLabel(label == null || label.trim().isEmpty() ? "Dogfood" : label)
|
|
186
|
+
.setLongLabel(label == null || label.trim().isEmpty() ? "Dogfood" : label)
|
|
187
|
+
.setIcon(Icon.createWithResource(context, android.R.drawable.ic_media_play))
|
|
188
|
+
.setIntent(intent)
|
|
189
|
+
.build();
|
|
190
|
+
manager.addDynamicShortcuts(Collections.singletonList(shortcut));
|
|
191
|
+
}
|
|
192
|
+
promise.resolve(enabled);
|
|
193
|
+
} catch (Exception error) {
|
|
194
|
+
promise.reject("DOGFOOD_SHORTCUT_FAILED", error.getMessage(), error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@ReactMethod
|
|
199
|
+
public void consumeDogfoodShortcut(Promise promise) {
|
|
200
|
+
Activity activity = getCurrentActivity();
|
|
201
|
+
if (activity == null || activity.getIntent() == null) {
|
|
202
|
+
promise.resolve(false);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
Intent intent = activity.getIntent();
|
|
206
|
+
boolean pending = DOGFOOD_SHORTCUT_EXTRA.equals(intent.getAction())
|
|
207
|
+
|| intent.getBooleanExtra(DOGFOOD_SHORTCUT_EXTRA, false);
|
|
208
|
+
if (pending) {
|
|
209
|
+
intent.removeExtra(DOGFOOD_SHORTCUT_EXTRA);
|
|
210
|
+
intent.setAction(Intent.ACTION_MAIN);
|
|
211
|
+
}
|
|
212
|
+
promise.resolve(pending);
|
|
213
|
+
}
|
|
214
|
+
|
|
156
215
|
/**
|
|
157
216
|
* Recreate the React Native context with the new bundle.
|
|
158
217
|
*/
|
package/app.plugin.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
withXcodeProject,
|
|
21
21
|
withAppDelegate,
|
|
22
22
|
withMainApplication,
|
|
23
|
+
withMainActivity,
|
|
23
24
|
withDangerousMod,
|
|
24
25
|
createRunOncePlugin,
|
|
25
26
|
} = require(configPluginsPath);
|
|
@@ -38,6 +39,14 @@ function withYaverFeedbackIOS(config) {
|
|
|
38
39
|
config.modResults.NSMicrophoneUsageDescription =
|
|
39
40
|
"Used for voice annotations in feedback reports during development";
|
|
40
41
|
}
|
|
42
|
+
const bundleId = config.ios?.bundleIdentifier;
|
|
43
|
+
if (bundleId) {
|
|
44
|
+
const scheme = `yaver-dogfood-${bundleId.toLowerCase().replace(/[^a-z0-9.-]/g, "-")}`;
|
|
45
|
+
const urlTypes = config.modResults.CFBundleURLTypes || [];
|
|
46
|
+
const exists = urlTypes.some((entry) => Array.isArray(entry.CFBundleURLSchemes) && entry.CFBundleURLSchemes.includes(scheme));
|
|
47
|
+
if (!exists) urlTypes.push({ CFBundleURLName: `${bundleId}.yaver-dogfood`, CFBundleURLSchemes: [scheme] });
|
|
48
|
+
config.modResults.CFBundleURLTypes = urlTypes;
|
|
49
|
+
}
|
|
41
50
|
return config;
|
|
42
51
|
});
|
|
43
52
|
}
|
|
@@ -78,6 +87,28 @@ function withYaverFeedbackAndroid(config) {
|
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
const packageName = config.android?.package;
|
|
91
|
+
const mainActivity = manifest.application?.[0]?.activity?.find((activity) =>
|
|
92
|
+
activity.$?.["android:name"]?.endsWith("MainActivity")
|
|
93
|
+
);
|
|
94
|
+
if (packageName && mainActivity) {
|
|
95
|
+
const scheme = `yaver-dogfood-${packageName.toLowerCase().replace(/[^a-z0-9.-]/g, "-")}`;
|
|
96
|
+
mainActivity["intent-filter"] = mainActivity["intent-filter"] || [];
|
|
97
|
+
const exists = mainActivity["intent-filter"].some((filter) =>
|
|
98
|
+
filter.data?.some((data) => data.$?.["android:scheme"] === scheme)
|
|
99
|
+
);
|
|
100
|
+
if (!exists) {
|
|
101
|
+
mainActivity["intent-filter"].push({
|
|
102
|
+
action: [{ $: { "android:name": "android.intent.action.VIEW" } }],
|
|
103
|
+
category: [
|
|
104
|
+
{ $: { "android:name": "android.intent.category.DEFAULT" } },
|
|
105
|
+
{ $: { "android:name": "android.intent.category.BROWSABLE" } },
|
|
106
|
+
],
|
|
107
|
+
data: [{ $: { "android:scheme": scheme, "android:host": "activate" } }],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
81
112
|
return config;
|
|
82
113
|
});
|
|
83
114
|
}
|
|
@@ -183,8 +214,10 @@ function withYaverAppDelegateHook(config) {
|
|
|
183
214
|
return withAppDelegate(config, (config) => {
|
|
184
215
|
const contents = config.modResults.contents;
|
|
185
216
|
|
|
186
|
-
//
|
|
217
|
+
// Existing consumers may already have the hot-reload hook from an older
|
|
218
|
+
// SDK. Still apply newer, independently-versioned native contracts.
|
|
187
219
|
if (contents.includes("YaverHotReload")) {
|
|
220
|
+
config.modResults.contents = patchDogfoodAppShortcut(contents);
|
|
188
221
|
return config;
|
|
189
222
|
}
|
|
190
223
|
|
|
@@ -326,11 +359,136 @@ function withYaverAppDelegateHook(config) {
|
|
|
326
359
|
patched = patched.replace("return setupYaverHotReload()", "setupYaverHotReload()");
|
|
327
360
|
}
|
|
328
361
|
|
|
329
|
-
config.modResults.contents = patched;
|
|
362
|
+
config.modResults.contents = patchDogfoodAppShortcut(patched);
|
|
330
363
|
return config;
|
|
331
364
|
});
|
|
332
365
|
}
|
|
333
366
|
|
|
367
|
+
/** Wire the dynamic iOS Home Screen shortcut into the SDK native module.
|
|
368
|
+
* The shortcut itself is created at runtime only after backend ACL success;
|
|
369
|
+
* this hook merely consumes a user-selected action on warm/cold launch. */
|
|
370
|
+
function patchDogfoodAppShortcut(contents) {
|
|
371
|
+
if (contents.includes("Yaver Feedback SDK Dogfood Shortcut")) return contents;
|
|
372
|
+
let patched = contents;
|
|
373
|
+
|
|
374
|
+
// Cold launch: UIKit supplies the shortcut in launchOptions before RN/JS.
|
|
375
|
+
const setupAnchor = " setupYaverHotReload()";
|
|
376
|
+
if (patched.includes(setupAnchor)) {
|
|
377
|
+
patched = patched.replace(
|
|
378
|
+
setupAnchor,
|
|
379
|
+
` if let yaverShortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem,
|
|
380
|
+
yaverShortcut.type == YaverHotReload.dogfoodShortcutType {
|
|
381
|
+
YaverHotReload.markDogfoodShortcutPending()
|
|
382
|
+
}
|
|
383
|
+
${setupAnchor}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const classCloseIndex = findAppDelegateClassClose(patched);
|
|
388
|
+
if (classCloseIndex > 0) {
|
|
389
|
+
const handler = `
|
|
390
|
+
// MARK: - Yaver Feedback SDK Dogfood Shortcut
|
|
391
|
+
|
|
392
|
+
public override func application(
|
|
393
|
+
_ application: UIApplication,
|
|
394
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
395
|
+
completionHandler: @escaping (Bool) -> Void
|
|
396
|
+
) {
|
|
397
|
+
if shortcutItem.type == YaverHotReload.dogfoodShortcutType {
|
|
398
|
+
YaverHotReload.markDogfoodShortcutPending()
|
|
399
|
+
completionHandler(true)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
super.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
patched = patched.slice(0, classCloseIndex) + handler + patched.slice(classCloseIndex);
|
|
406
|
+
}
|
|
407
|
+
return patched;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Scene-based iOS apps receive Home Screen quick actions through their
|
|
411
|
+
* UIWindowSceneDelegate, not AppDelegate. A CarPlay scene manifest is enough
|
|
412
|
+
* to opt an otherwise ordinary Expo app into that lifecycle, so patch the
|
|
413
|
+
* phone scene delegate when a host has one. */
|
|
414
|
+
function withYaverSceneDelegateShortcut(config) {
|
|
415
|
+
return withDangerousMod(config, [
|
|
416
|
+
"ios",
|
|
417
|
+
(config) => {
|
|
418
|
+
const appName = config.modRequest.projectName;
|
|
419
|
+
if (!appName) return config;
|
|
420
|
+
const sourceDir = path.join(config.modRequest.platformProjectRoot, appName);
|
|
421
|
+
if (!fs.existsSync(sourceDir)) return config;
|
|
422
|
+
|
|
423
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
424
|
+
if (!entry.isFile() || !entry.name.endsWith("SceneDelegate.swift")) continue;
|
|
425
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
426
|
+
const source = fs.readFileSync(sourcePath, "utf8");
|
|
427
|
+
if (!source.includes("UIWindowSceneDelegate")) continue;
|
|
428
|
+
const patched = patchDogfoodSceneDelegate(source);
|
|
429
|
+
if (patched !== source) fs.writeFileSync(sourcePath, patched);
|
|
430
|
+
}
|
|
431
|
+
return config;
|
|
432
|
+
},
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function patchDogfoodSceneDelegate(contents) {
|
|
437
|
+
if (contents.includes("Yaver Feedback SDK Dogfood Scene Shortcut")) return contents;
|
|
438
|
+
|
|
439
|
+
let patched = insertAtEndOfMethod(
|
|
440
|
+
contents,
|
|
441
|
+
"options connectionOptions: UIScene.ConnectionOptions",
|
|
442
|
+
`
|
|
443
|
+
// Yaver Feedback SDK Dogfood Scene Shortcut: cold launch.
|
|
444
|
+
if let yaverShortcut = connectionOptions.shortcutItem,
|
|
445
|
+
yaverShortcut.type == YaverHotReload.dogfoodShortcutType {
|
|
446
|
+
YaverHotReload.markDogfoodShortcutPending()
|
|
447
|
+
}
|
|
448
|
+
`
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// A custom scene delegate may already forward quick actions to the patched
|
|
452
|
+
// AppDelegate. Keep that host-owned behavior instead of declaring a second
|
|
453
|
+
// method with the same Swift selector.
|
|
454
|
+
if (patched.includes("performActionFor shortcutItem")) return patched;
|
|
455
|
+
|
|
456
|
+
const classCloseIndex = findWindowSceneDelegateClassClose(patched);
|
|
457
|
+
if (classCloseIndex < 0) return patched;
|
|
458
|
+
const handler = `
|
|
459
|
+
// MARK: - Yaver Feedback SDK Dogfood Scene Shortcut
|
|
460
|
+
|
|
461
|
+
func windowScene(
|
|
462
|
+
_ windowScene: UIWindowScene,
|
|
463
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
464
|
+
completionHandler: @escaping (Bool) -> Void
|
|
465
|
+
) {
|
|
466
|
+
guard shortcutItem.type == YaverHotReload.dogfoodShortcutType else {
|
|
467
|
+
completionHandler(false)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
YaverHotReload.markDogfoodShortcutPending()
|
|
471
|
+
completionHandler(true)
|
|
472
|
+
}
|
|
473
|
+
`;
|
|
474
|
+
return patched.slice(0, classCloseIndex) + handler + patched.slice(classCloseIndex);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function findWindowSceneDelegateClassClose(contents) {
|
|
478
|
+
const headerMatch = contents.match(/class\s+\w+\s*:[^{]*UIWindowSceneDelegate[^{]*\{/);
|
|
479
|
+
if (!headerMatch) return -1;
|
|
480
|
+
const bodyStart = headerMatch.index + headerMatch[0].length - 1;
|
|
481
|
+
let depth = 0;
|
|
482
|
+
for (let i = bodyStart; i < contents.length; i++) {
|
|
483
|
+
if (contents[i] === "{") depth++;
|
|
484
|
+
else if (contents[i] === "}") {
|
|
485
|
+
depth--;
|
|
486
|
+
if (depth === 0) return i;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return -1;
|
|
490
|
+
}
|
|
491
|
+
|
|
334
492
|
// Locate the closing brace of `class AppDelegate: ...` in an Expo
|
|
335
493
|
// Swift AppDelegate file. Returns the index of the matching `}` for
|
|
336
494
|
// the AppDelegate class's opening `{`, or -1 if not found. We need
|
|
@@ -404,6 +562,33 @@ function withYaverAndroidHotReload(config) {
|
|
|
404
562
|
return config;
|
|
405
563
|
});
|
|
406
564
|
|
|
565
|
+
// ReactActivity forwards warm-launch intents to RN but does not update
|
|
566
|
+
// Activity.getIntent(). The native module consumes the explicit shortcut
|
|
567
|
+
// extra from getIntent(), so retain the latest intent for warm launches.
|
|
568
|
+
config = withMainActivity(config, (config) => {
|
|
569
|
+
let contents = config.modResults.contents;
|
|
570
|
+
if (contents.includes("yaverDogfoodShortcutIntent")) return config;
|
|
571
|
+
const classClose = contents.lastIndexOf("}");
|
|
572
|
+
if (classClose < 0) return config;
|
|
573
|
+
const method = config.modResults.language === "kt"
|
|
574
|
+
? `
|
|
575
|
+
// Yaver Feedback SDK: retain dynamic Dogfood shortcut warm-launch intent.
|
|
576
|
+
override fun onNewIntent(yaverDogfoodShortcutIntent: android.content.Intent) {
|
|
577
|
+
setIntent(yaverDogfoodShortcutIntent)
|
|
578
|
+
super.onNewIntent(yaverDogfoodShortcutIntent)
|
|
579
|
+
}
|
|
580
|
+
`
|
|
581
|
+
: `
|
|
582
|
+
// Yaver Feedback SDK: retain dynamic Dogfood shortcut warm-launch intent.
|
|
583
|
+
@Override public void onNewIntent(android.content.Intent yaverDogfoodShortcutIntent) {
|
|
584
|
+
setIntent(yaverDogfoodShortcutIntent);
|
|
585
|
+
super.onNewIntent(yaverDogfoodShortcutIntent);
|
|
586
|
+
}
|
|
587
|
+
`;
|
|
588
|
+
config.modResults.contents = contents.slice(0, classClose) + method + contents.slice(classClose);
|
|
589
|
+
return config;
|
|
590
|
+
});
|
|
591
|
+
|
|
407
592
|
return config;
|
|
408
593
|
}
|
|
409
594
|
|
|
@@ -608,14 +793,26 @@ function withYaverFeedback(config, props) {
|
|
|
608
793
|
if (enableHotReload) {
|
|
609
794
|
config = withYaverHotReloadNativeModule(config);
|
|
610
795
|
config = withYaverAppDelegateHook(config);
|
|
796
|
+
config = withYaverSceneDelegateShortcut(config);
|
|
611
797
|
config = withYaverAndroidHotReload(config);
|
|
612
798
|
}
|
|
613
799
|
|
|
614
800
|
return config;
|
|
615
801
|
}
|
|
616
802
|
|
|
617
|
-
|
|
803
|
+
const yaverFeedbackPlugin = createRunOncePlugin(
|
|
618
804
|
withYaverFeedback,
|
|
619
805
|
pkg.name,
|
|
620
806
|
pkg.version
|
|
621
807
|
);
|
|
808
|
+
|
|
809
|
+
// Pure transforms are exposed for contract tests only. Keeping the test at the
|
|
810
|
+
// config-plugin seam catches template drift before a consumer discovers it in
|
|
811
|
+
// an archive build.
|
|
812
|
+
yaverFeedbackPlugin.__test = {
|
|
813
|
+
patchDogfoodAppShortcut,
|
|
814
|
+
patchDogfoodSceneDelegate,
|
|
815
|
+
findAppDelegateClassClose,
|
|
816
|
+
findWindowSceneDelegateClassClose,
|
|
817
|
+
};
|
|
818
|
+
module.exports = yaverFeedbackPlugin;
|
package/dist/AuthOverlay.js
CHANGED
|
@@ -89,12 +89,23 @@ const AuthOverlay = () => {
|
|
|
89
89
|
const handleLoggedIn = async (newToken) => {
|
|
90
90
|
setToken(newToken);
|
|
91
91
|
await YaverFeedback_1.YaverFeedback.setAuthToken(newToken);
|
|
92
|
-
|
|
92
|
+
if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
|
|
93
|
+
closeAll();
|
|
94
|
+
await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
openPicker();
|
|
98
|
+
}
|
|
93
99
|
};
|
|
94
100
|
const handleDevicePicked = async (device) => {
|
|
95
101
|
await YaverFeedback_1.YaverFeedback.setPreferredDevice(device.deviceId);
|
|
96
102
|
closeAll();
|
|
97
|
-
|
|
103
|
+
if (YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()) {
|
|
104
|
+
await YaverFeedback_1.YaverFeedback.continueDogfoodOnboarding();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
react_native_1.DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
108
|
+
}
|
|
98
109
|
};
|
|
99
110
|
return (<>
|
|
100
111
|
<react_native_1.Modal visible={loginVisible} animationType="slide" presentationStyle="fullScreen" onRequestClose={closeAll}>
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -236,8 +236,17 @@ const FeedbackModal = () => {
|
|
|
236
236
|
|| projects[0]
|
|
237
237
|
|| null;
|
|
238
238
|
setDogfoodProject((current) => current && projects.some((item) => item.path === current.path) ? current : preferred);
|
|
239
|
-
if (preferred)
|
|
240
|
-
|
|
239
|
+
if (preferred) {
|
|
240
|
+
const framework = preferred.framework || onboarding.framework || 'expo';
|
|
241
|
+
const capabilities = await client.getDogfoodRemoteRuntimeCapabilities(preferred.path, framework).catch(() => null);
|
|
242
|
+
const nativeRuntimeAvailable = !!capabilities?.targets.some((target) => target.enabled && target.id !== 'browser-window');
|
|
243
|
+
if (mountedRef.current)
|
|
244
|
+
setDogfoodNativeAvailable(nativeRuntimeAvailable);
|
|
245
|
+
const savedLane = await (0, preferences_1.getPreferredDogfoodLane)(onboarding.appId);
|
|
246
|
+
const savedSupported = (0, DogfoodRuntime_1.dogfoodLaneOptions)(framework, { nativeRuntimeAvailable })
|
|
247
|
+
.some((option) => option.lane === savedLane && option.supported);
|
|
248
|
+
setDogfoodLane(savedLane && savedSupported ? savedLane : (0, DogfoodRuntime_1.defaultDogfoodLane)(framework));
|
|
249
|
+
}
|
|
241
250
|
}
|
|
242
251
|
catch (cause) {
|
|
243
252
|
if (mountedRef.current)
|
|
@@ -468,8 +477,12 @@ const FeedbackModal = () => {
|
|
|
468
477
|
(0, react_1.useEffect)(() => {
|
|
469
478
|
mountedRef.current = true;
|
|
470
479
|
const sub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
|
|
471
|
-
|
|
472
|
-
|
|
480
|
+
const onboarding = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding();
|
|
481
|
+
// Explicit Dogfood remains usable when passive capture/shake is off.
|
|
482
|
+
// The host still owns visibility; server OAuth/device signatures own
|
|
483
|
+
// authority. Keeping this event path independent avoids toggling the
|
|
484
|
+
// user's feedback preference merely to open Developer Mode.
|
|
485
|
+
if (YaverFeedback_1.YaverFeedback.isEnabled() || onboarding) {
|
|
473
486
|
const directDogfood = YaverFeedback_1.YaverFeedback.getDogfoodStatus().active;
|
|
474
487
|
setDogfoodActive(directDogfood);
|
|
475
488
|
setVisible(true);
|
|
@@ -1003,6 +1016,14 @@ const FeedbackModal = () => {
|
|
|
1003
1016
|
const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
|
|
1004
1017
|
const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
|
|
1005
1018
|
const needsAuthRunnerCount = runnerCards.filter((row) => row.installed && !row.authConfigured && !row.ready).length;
|
|
1019
|
+
const selectedDogfoodRunner = preferredRunner
|
|
1020
|
+
? runnerCards.find((row) => row.id === preferredRunner) ?? null
|
|
1021
|
+
: null;
|
|
1022
|
+
const dogfoodRunnerReady = !!selectedDogfoodRunner
|
|
1023
|
+
&& (selectedDogfoodRunner.ready || selectedDogfoodRunner.authConfigured);
|
|
1024
|
+
const dogfoodModelReady = !selectedDogfoodRunner?.models?.length
|
|
1025
|
+
|| !!preferredModel && selectedDogfoodRunner.models.some((model) => model.id === preferredModel);
|
|
1026
|
+
const dogfoodStartBlocked = !dogfoodProject || !dogfoodRunnerReady || !dogfoodModelReady;
|
|
1006
1027
|
// Once the user fires off a vibe task, swap the entire modal body
|
|
1007
1028
|
// for the live chat screen. The chat manages its own SSE
|
|
1008
1029
|
// subscription, multi-turn follow-ups, and Reload button. Closing
|
|
@@ -1102,9 +1123,40 @@ const FeedbackModal = () => {
|
|
|
1102
1123
|
<react_native_1.Text style={[styles.dogfoodChoiceText, dogfoodProject?.path === project.path && styles.dogfoodChoiceTextSelected]}>{project.name}</react_native_1.Text>
|
|
1103
1124
|
</react_native_1.Pressable>))}
|
|
1104
1125
|
</react_native_1.ScrollView>
|
|
1126
|
+
<react_native_1.Text style={styles.dogfoodStepLabel}>Coding agent</react_native_1.Text>
|
|
1127
|
+
<react_native_1.View style={styles.dogfoodChoiceRow}>
|
|
1128
|
+
{runnerCards.filter((row) => row.ready || row.authConfigured).map((row) => (<react_native_1.Pressable key={row.id} onPress={() => {
|
|
1129
|
+
const nextModel = row.models?.find((model) => model.isDefault)?.id || row.models?.[0]?.id || '';
|
|
1130
|
+
setPreferredRunnerState(row.id);
|
|
1131
|
+
setPreferredModelState(nextModel);
|
|
1132
|
+
void (0, preferences_1.setPreferredRunner)(row.id);
|
|
1133
|
+
void (0, preferences_1.setPreferredModel)(nextModel || null);
|
|
1134
|
+
}} style={[styles.dogfoodChoice, preferredRunner === row.id && styles.dogfoodChoiceSelected]} accessibilityRole="button" accessibilityState={{ selected: preferredRunner === row.id }} accessibilityLabel={`Use ${row.name} for Dogfood`}>
|
|
1135
|
+
<react_native_1.Text style={[styles.dogfoodChoiceText, preferredRunner === row.id && styles.dogfoodChoiceTextSelected]}>{row.name}</react_native_1.Text>
|
|
1136
|
+
</react_native_1.Pressable>))}
|
|
1137
|
+
</react_native_1.View>
|
|
1138
|
+
{readyRunnerCount === 0 ? (<react_native_1.Text style={styles.dogfoodWizardHint}>Sign in or configure a coding agent under Coding Agents below.</react_native_1.Text>) : null}
|
|
1139
|
+
{selectedDogfoodRunner?.models?.length ? (<>
|
|
1140
|
+
<react_native_1.Text style={styles.dogfoodStepLabel}>Model</react_native_1.Text>
|
|
1141
|
+
<react_native_1.ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dogfoodChoiceRow}>
|
|
1142
|
+
{selectedDogfoodRunner.models.map((model) => (<react_native_1.Pressable key={model.id} onPress={() => {
|
|
1143
|
+
setPreferredModelState(model.id);
|
|
1144
|
+
void (0, preferences_1.setPreferredModel)(model.id);
|
|
1145
|
+
}} style={[styles.dogfoodChoice, preferredModel === model.id && styles.dogfoodChoiceSelected]} accessibilityRole="button" accessibilityState={{ selected: preferredModel === model.id }} accessibilityLabel={`Use ${model.name || model.id} model for Dogfood`}>
|
|
1146
|
+
<react_native_1.Text style={[styles.dogfoodChoiceText, preferredModel === model.id && styles.dogfoodChoiceTextSelected]}>{model.name || model.id}</react_native_1.Text>
|
|
1147
|
+
</react_native_1.Pressable>))}
|
|
1148
|
+
</react_native_1.ScrollView>
|
|
1149
|
+
</>) : null}
|
|
1105
1150
|
<react_native_1.Text style={styles.dogfoodStepLabel}>Runtime lane</react_native_1.Text>
|
|
1106
1151
|
<react_native_1.View style={styles.dogfoodChoiceRow}>
|
|
1107
|
-
{(0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable }).map((option) => (<react_native_1.Pressable key={option.lane} onPress={() =>
|
|
1152
|
+
{(0, DogfoodRuntime_1.dogfoodLaneOptions)(dogfoodProject?.framework || YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.framework || 'expo', { nativeRuntimeAvailable: dogfoodNativeAvailable }).map((option) => (<react_native_1.Pressable key={option.lane} onPress={() => {
|
|
1153
|
+
if (!option.supported)
|
|
1154
|
+
return;
|
|
1155
|
+
setDogfoodLane(option.lane);
|
|
1156
|
+
const appId = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.appId;
|
|
1157
|
+
if (appId)
|
|
1158
|
+
void (0, preferences_1.setPreferredDogfoodLane)(appId, option.lane);
|
|
1159
|
+
}} style={[
|
|
1108
1160
|
styles.dogfoodChoice,
|
|
1109
1161
|
dogfoodLane === option.lane && styles.dogfoodChoiceSelected,
|
|
1110
1162
|
!option.supported && styles.actionBtnDisabled,
|
|
@@ -1113,9 +1165,9 @@ const FeedbackModal = () => {
|
|
|
1113
1165
|
</react_native_1.Pressable>))}
|
|
1114
1166
|
</react_native_1.View>
|
|
1115
1167
|
<react_native_1.Text style={styles.dogfoodWizardHint}>
|
|
1116
|
-
|
|
1168
|
+
{[preferredRunner || 'Choose a coding agent', preferredModel].filter(Boolean).join(' · ')}
|
|
1117
1169
|
</react_native_1.Text>
|
|
1118
|
-
<ActionRow label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'} tint="#818cf8" onPress={() => void startDogfoodRuntime()} disabled={
|
|
1170
|
+
<ActionRow label={dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase) ? dogfoodRuntime.message : 'Start Dogfood'} tint="#818cf8" onPress={() => void startDogfoodRuntime()} disabled={dogfoodStartBlocked || !!(dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase))} busy={!!dogfoodRuntime && !['idle', 'ready', 'failed', 'stopped'].includes(dogfoodRuntime.phase)}/>
|
|
1119
1171
|
{dogfoodRuntime ? (<react_native_1.View style={styles.dogfoodConsole}>
|
|
1120
1172
|
<react_native_1.Text style={styles.dogfoodConsoleStatus}>{dogfoodRuntime.message}</react_native_1.Text>
|
|
1121
1173
|
{dogfoodRuntime.logs.slice(-80).map((line, index) => (<react_native_1.Text key={`${line.at}-${index}`} selectable style={styles.dogfoodConsoleLine}>{line.text}</react_native_1.Text>))}
|
|
@@ -1258,6 +1310,15 @@ const FeedbackModal = () => {
|
|
|
1258
1310
|
</react_native_1.Pressable>
|
|
1259
1311
|
</react_native_1.View>)}
|
|
1260
1312
|
|
|
1313
|
+
{YaverFeedback_1.YaverFeedback.isAuthed() ? (<react_native_1.Pressable onPress={() => {
|
|
1314
|
+
void YaverFeedback_1.YaverFeedback.signOut().then(() => {
|
|
1315
|
+
handleClose();
|
|
1316
|
+
YaverFeedback_1.YaverFeedback.showLogin();
|
|
1317
|
+
});
|
|
1318
|
+
}} style={({ pressed }) => [styles.yaverSignOutBtn, pressed && styles.buttonPressed]} accessibilityRole="button" accessibilityLabel="Sign out of Yaver">
|
|
1319
|
+
<react_native_1.Text style={styles.yaverSignOutText}>Sign out of Yaver</react_native_1.Text>
|
|
1320
|
+
</react_native_1.Pressable>) : null}
|
|
1321
|
+
|
|
1261
1322
|
<react_native_1.View style={styles.iconSelector}>
|
|
1262
1323
|
<react_native_1.Text style={styles.iconSelectorTitle}>Quick Icon Color</react_native_1.Text>
|
|
1263
1324
|
<react_native_1.Text style={styles.iconSelectorText}>
|
|
@@ -1414,6 +1475,8 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1414
1475
|
dogfoodConsoleError: { color: '#fca5a5', fontSize: 11, lineHeight: 16, marginTop: 5 },
|
|
1415
1476
|
dogfoodOpenPreview: { alignSelf: 'flex-start', borderRadius: 9, paddingHorizontal: 11, paddingVertical: 8, marginTop: 6, backgroundColor: '#6555df' },
|
|
1416
1477
|
dogfoodOpenPreviewText: { color: '#fff', fontSize: 12, fontWeight: '800' },
|
|
1478
|
+
yaverSignOutBtn: { alignSelf: 'flex-start', paddingHorizontal: 4, paddingVertical: 8 },
|
|
1479
|
+
yaverSignOutText: { color: '#b42318', fontSize: 13, fontWeight: '700' },
|
|
1417
1480
|
reloadRow: {
|
|
1418
1481
|
gap: 4,
|
|
1419
1482
|
},
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { FeedbackConfig, CapturedError } from './types';
|
|
|
2
2
|
import { P2PClient } from './P2PClient';
|
|
3
3
|
import { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult } from './storeShots';
|
|
4
4
|
import { QuickIconColorPreset } from './preferences';
|
|
5
|
-
import { type SDKDogfoodStatus } from './dogfoodPolicy';
|
|
5
|
+
import { type SDKDogfoodStatus, type DogfoodAccessSnapshot } from './dogfoodPolicy';
|
|
6
6
|
import { type DeviceDogfoodOptions, type DeviceDogfoodSession, type DeviceDogfoodState } from './deviceDogfood';
|
|
7
7
|
export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
|
|
8
8
|
/** Hint used to preselect the matching project returned by the owner machine. */
|
|
@@ -10,6 +10,12 @@ export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
|
|
|
10
10
|
/** Framework fallback when the machine has not classified the project yet. */
|
|
11
11
|
framework?: string;
|
|
12
12
|
}
|
|
13
|
+
export type DogfoodFlowPhase = 'idle' | 'denied' | 'auth-required' | 'machine-required' | 'opening' | 'error';
|
|
14
|
+
export interface DogfoodFlowState {
|
|
15
|
+
phase: DogfoodFlowPhase;
|
|
16
|
+
appId?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
13
19
|
/**
|
|
14
20
|
* Main entry point for the Yaver Feedback SDK.
|
|
15
21
|
* Call `YaverFeedback.init()` once at app startup.
|
|
@@ -108,10 +114,29 @@ export declare class YaverFeedback {
|
|
|
108
114
|
* an active session; no-ops otherwise.
|
|
109
115
|
*/
|
|
110
116
|
static showMachinePicker(): void;
|
|
111
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
/** Configure Dogfood once during host init. Hosts still decide whether and
|
|
118
|
+
* where to render an affordance; `openDogfood()` owns all flow mechanics. */
|
|
119
|
+
static configureDogfood(options: DogfoodOnboardingOptions): void;
|
|
120
|
+
/**
|
|
121
|
+
* Open Dogfood using config.dogfood + the normal app identity. Cached OAuth,
|
|
122
|
+
* machine, runner and model choices are reused. The corresponding picker is
|
|
123
|
+
* shown only when a required choice is missing.
|
|
124
|
+
*/
|
|
125
|
+
static openDogfood(overrides?: Partial<DogfoodOnboardingOptions>): Promise<DogfoodFlowState>;
|
|
126
|
+
/** Resolve the host-facing ACL snapshot without granting authority. This is
|
|
127
|
+
* the one endpoint custom Settings screens need for visibility/status UI. */
|
|
128
|
+
static getDogfoodAccess(): Promise<DogfoodAccessSnapshot>;
|
|
129
|
+
/** Add/remove the platform Home Screen shortcut from backend-authoritative
|
|
130
|
+
* owner/device ACL state. Static plist shortcuts are intentionally avoided:
|
|
131
|
+
* an unauthorized install must never advertise a hidden developer action. */
|
|
132
|
+
static syncDogfoodAppShortcut(): Promise<boolean>;
|
|
133
|
+
/** Backwards-compatible entry point for existing integrations. */
|
|
134
|
+
static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): Promise<DogfoodFlowState>;
|
|
135
|
+
/** Continue after OAuth or machine selection. Public so custom host UI can
|
|
136
|
+
* hand control back without recreating the SDK state machine. */
|
|
137
|
+
static continueDogfoodOnboarding(): Promise<DogfoodFlowState>;
|
|
138
|
+
static getDogfoodFlowState(): DogfoodFlowState;
|
|
139
|
+
static onDogfoodFlowState(listener: (state: DogfoodFlowState) => void): () => void;
|
|
115
140
|
static getDogfoodOnboarding(): DogfoodOnboardingOptions | null;
|
|
116
141
|
static clearDogfoodOnboarding(): void;
|
|
117
142
|
/**
|
|
@@ -215,10 +240,10 @@ export declare class YaverFeedback {
|
|
|
215
240
|
static getConfig(): FeedbackConfig | null;
|
|
216
241
|
/** Current third-party SDK mode. Fails closed unless enabled + account match. */
|
|
217
242
|
static getDogfoodStatus(): SDKDogfoodStatus;
|
|
218
|
-
/** One-call
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
243
|
+
/** One-call account-bound Dogfood bootstrap for third-party apps. The host
|
|
244
|
+
* app needs no auth backend of its own: SDK OAuth supplies the full Yaver
|
|
245
|
+
* account, then this creates/proves the installation key. Owner approval
|
|
246
|
+
* binds that account + appId + phone key before a scoped session is minted. */
|
|
222
247
|
static enableDeviceDogfood(options: DeviceDogfoodOptions): Promise<{
|
|
223
248
|
status: DeviceDogfoodState;
|
|
224
249
|
installationId: string;
|