yaver-feedback-react-native 0.3.0 → 0.5.0
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/LICENSE +201 -0
- package/README.md +29 -11
- package/android/src/main/java/io/yaver/feedback/YaverHotReloadModule.java +188 -0
- package/android/src/main/java/io/yaver/feedback/YaverHotReloadPackage.java +33 -0
- package/app.plugin.js +263 -5
- package/ios/YaverHotReload.m +16 -0
- package/ios/YaverHotReload.swift +112 -0
- package/package.json +24 -7
- package/src/AuthOverlay.tsx +97 -0
- package/src/BlackBox.ts +41 -2
- package/src/Discovery.ts +141 -8
- package/src/FeedbackModal.tsx +9 -2
- package/src/FloatingButton.tsx +19 -7
- package/src/LoginScreen.tsx +395 -0
- package/src/MachinePickerScreen.tsx +196 -0
- package/src/P2PClient.ts +110 -0
- package/src/YaverFeedback.ts +363 -14
- package/src/YaverUpdates.ts +334 -0
- package/src/__tests__/Discovery.test.ts +8 -2
- package/src/__tests__/YaverFeedback.test.ts +4 -1
- package/src/auth.ts +338 -0
- package/src/index.ts +36 -0
- package/src/types.ts +21 -2
package/app.plugin.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Expo config plugin for yaver-feedback-react-native.
|
|
3
3
|
*
|
|
4
|
-
* Adds
|
|
5
|
-
* - iOS
|
|
6
|
-
* -
|
|
4
|
+
* Adds:
|
|
5
|
+
* - iOS/Android permissions for camera + microphone (feedback screenshots/voice)
|
|
6
|
+
* - iOS: YaverHotReload native module for Hermes bundle hot reload
|
|
7
|
+
* - AppDelegate hook to load hot-reloaded bundles on startup
|
|
7
8
|
*
|
|
8
9
|
* Usage in app.json:
|
|
9
10
|
* { "expo": { "plugins": ["yaver-feedback-react-native"] } }
|
|
10
11
|
*/
|
|
12
|
+
// Resolve @expo/config-plugins from the host project's node_modules
|
|
13
|
+
// (not from the SDK's directory, which may be symlinked)
|
|
14
|
+
const configPluginsPath = require.resolve("@expo/config-plugins", {
|
|
15
|
+
paths: [process.cwd()],
|
|
16
|
+
});
|
|
11
17
|
const {
|
|
12
18
|
withInfoPlist,
|
|
13
19
|
withAndroidManifest,
|
|
20
|
+
withXcodeProject,
|
|
21
|
+
withAppDelegate,
|
|
22
|
+
withMainApplication,
|
|
23
|
+
withDangerousMod,
|
|
14
24
|
createRunOncePlugin,
|
|
15
|
-
} = require(
|
|
25
|
+
} = require(configPluginsPath);
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const fs = require("fs");
|
|
16
28
|
|
|
17
29
|
const pkg = require("./package.json");
|
|
18
30
|
|
|
@@ -57,9 +69,255 @@ function withYaverFeedbackAndroid(config) {
|
|
|
57
69
|
});
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Copy YaverHotReload native module files into the iOS project directory.
|
|
74
|
+
* Uses withDangerousMod to copy files during prebuild. The files are
|
|
75
|
+
* automatically picked up by Xcode since they're in the project directory.
|
|
76
|
+
*/
|
|
77
|
+
function withYaverHotReloadNativeModule(config) {
|
|
78
|
+
return withDangerousMod(config, [
|
|
79
|
+
"ios",
|
|
80
|
+
(config) => {
|
|
81
|
+
const sdkIosDir = path.resolve(__dirname, "ios");
|
|
82
|
+
const appName = config.modRequest.projectName || "SFMG";
|
|
83
|
+
const targetDir = path.join(
|
|
84
|
+
config.modRequest.platformProjectRoot,
|
|
85
|
+
appName
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const filesToCopy = ["YaverHotReload.swift", "YaverHotReload.m"];
|
|
89
|
+
for (const fileName of filesToCopy) {
|
|
90
|
+
const src = path.join(sdkIosDir, fileName);
|
|
91
|
+
const dst = path.join(targetDir, fileName);
|
|
92
|
+
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
93
|
+
fs.copyFileSync(src, dst);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return config;
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Patch AppDelegate to:
|
|
104
|
+
* 1. Return hot-reloaded bundle URL on startup (so reloaded bundle persists)
|
|
105
|
+
* 2. Handle YaverHotReloadBundle notification to recreate the RN bridge
|
|
106
|
+
* with the new bundle (enables N reloads without app restart)
|
|
107
|
+
*
|
|
108
|
+
* Uses the same pattern as Yaver's own AppDelegate: tear down old bridge,
|
|
109
|
+
* create new ExpoReactNativeFactory with overrideBundleURL, startReactNative.
|
|
110
|
+
*/
|
|
111
|
+
function withYaverAppDelegateHook(config) {
|
|
112
|
+
return withAppDelegate(config, (config) => {
|
|
113
|
+
const contents = config.modResults.contents;
|
|
114
|
+
|
|
115
|
+
// Only patch if not already patched
|
|
116
|
+
if (contents.includes("YaverHotReload")) {
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// For Swift AppDelegate (Expo SDK 50+)
|
|
121
|
+
let patched = contents;
|
|
122
|
+
|
|
123
|
+
// 1. Hook bundleURL() to return hot bundle on startup
|
|
124
|
+
if (patched.includes("func bundleURL()")) {
|
|
125
|
+
patched = patched.replace(
|
|
126
|
+
/func bundleURL\(\) -> URL\? \{/,
|
|
127
|
+
`func bundleURL() -> URL? {
|
|
128
|
+
// Yaver Feedback SDK: load hot-reloaded bundle if available
|
|
129
|
+
if let yaverBundle = YaverHotReload.bundleURL() { return yaverBundle }`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Add reload notification handler and bridge recreation logic
|
|
134
|
+
// Insert before the closing brace of the class
|
|
135
|
+
const classCloseIndex = patched.lastIndexOf("}");
|
|
136
|
+
if (classCloseIndex > 0) {
|
|
137
|
+
const reloadHandler = `
|
|
138
|
+
// MARK: - Yaver Feedback SDK Hot Reload
|
|
139
|
+
|
|
140
|
+
private var yaverIsReloading = false
|
|
141
|
+
|
|
142
|
+
private func setupYaverHotReload() {
|
|
143
|
+
NotificationCenter.default.addObserver(
|
|
144
|
+
self,
|
|
145
|
+
selector: #selector(yaverHandleHotReload(_:)),
|
|
146
|
+
name: Notification.Name("YaverHotReloadBundle"),
|
|
147
|
+
object: nil
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@objc private func yaverHandleHotReload(_ notification: Notification) {
|
|
152
|
+
guard !yaverIsReloading else { return }
|
|
153
|
+
yaverIsReloading = true
|
|
154
|
+
|
|
155
|
+
guard let bundlePath = notification.userInfo?["bundlePath"] as? String else {
|
|
156
|
+
yaverIsReloading = false
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let bundleURL = URL(fileURLWithPath: bundlePath)
|
|
161
|
+
guard FileManager.default.fileExists(atPath: bundlePath) else {
|
|
162
|
+
NSLog("[YaverHotReload] bundle not found at %@", bundlePath)
|
|
163
|
+
yaverIsReloading = false
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
NSLog("[YaverHotReload] reloading bridge with %@", bundlePath)
|
|
168
|
+
|
|
169
|
+
guard let window = self.window else {
|
|
170
|
+
yaverIsReloading = false
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Show loading placeholder
|
|
175
|
+
let placeholder = UIView(frame: window.bounds)
|
|
176
|
+
placeholder.backgroundColor = .black
|
|
177
|
+
let spinner = UIActivityIndicatorView(style: .large)
|
|
178
|
+
spinner.color = .white
|
|
179
|
+
spinner.center = placeholder.center
|
|
180
|
+
spinner.startAnimating()
|
|
181
|
+
placeholder.addSubview(spinner)
|
|
182
|
+
window.rootViewController?.view = placeholder
|
|
183
|
+
|
|
184
|
+
// Brief delay for old bridge to tear down
|
|
185
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
|
186
|
+
guard let self = self else { return }
|
|
187
|
+
|
|
188
|
+
let delegate = ReactNativeDelegate()
|
|
189
|
+
delegate.overrideBundleURL = bundleURL
|
|
190
|
+
delegate.dependencyProvider = RCTAppDependencyProvider()
|
|
191
|
+
|
|
192
|
+
let factory = ExpoReactNativeFactory(delegate: delegate)
|
|
193
|
+
self.reactNativeDelegate = delegate
|
|
194
|
+
self.reactNativeFactory = factory
|
|
195
|
+
self.bindReactNativeFactory(factory)
|
|
196
|
+
|
|
197
|
+
factory.startReactNative(
|
|
198
|
+
withModuleName: "main",
|
|
199
|
+
in: window,
|
|
200
|
+
launchOptions: nil
|
|
201
|
+
)
|
|
202
|
+
self.yaverIsReloading = false
|
|
203
|
+
NSLog("[YaverHotReload] bridge recreated successfully")
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
patched =
|
|
209
|
+
patched.slice(0, classCloseIndex) +
|
|
210
|
+
reloadHandler +
|
|
211
|
+
patched.slice(classCloseIndex);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Call setupYaverHotReload() in didFinishLaunchingWithOptions
|
|
215
|
+
if (patched.includes("super.application(application, didFinishLaunchingWithOptions:")) {
|
|
216
|
+
patched = patched.replace(
|
|
217
|
+
"super.application(application, didFinishLaunchingWithOptions:",
|
|
218
|
+
"setupYaverHotReload()\n return super.application(application, didFinishLaunchingWithOptions:"
|
|
219
|
+
);
|
|
220
|
+
// Remove the duplicate "return" if the original already had one
|
|
221
|
+
patched = patched.replace("return setupYaverHotReload()", "setupYaverHotReload()");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
config.modResults.contents = patched;
|
|
225
|
+
return config;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Copy Android native module source files and register the package.
|
|
231
|
+
* Also patches MainApplication to use hot-reloaded bundle on startup.
|
|
232
|
+
*/
|
|
233
|
+
function withYaverAndroidHotReload(config) {
|
|
234
|
+
// Copy Java source files
|
|
235
|
+
config = withDangerousMod(config, [
|
|
236
|
+
"android",
|
|
237
|
+
(config) => {
|
|
238
|
+
const sdkAndroidDir = path.resolve(__dirname, "android", "src", "main", "java", "io", "yaver", "feedback");
|
|
239
|
+
const targetDir = path.join(
|
|
240
|
+
config.modRequest.platformProjectRoot,
|
|
241
|
+
"app", "src", "main", "java", "io", "yaver", "feedback"
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(sdkAndroidDir)) {
|
|
245
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
246
|
+
for (const file of fs.readdirSync(sdkAndroidDir)) {
|
|
247
|
+
fs.copyFileSync(
|
|
248
|
+
path.join(sdkAndroidDir, file),
|
|
249
|
+
path.join(targetDir, file)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return config;
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
// Patch MainApplication to register the package and use hot bundle
|
|
258
|
+
config = withMainApplication(config, (config) => {
|
|
259
|
+
let contents = config.modResults.contents;
|
|
260
|
+
|
|
261
|
+
if (contents.includes("YaverHotReload")) {
|
|
262
|
+
return config;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Add import
|
|
266
|
+
contents = contents.replace(
|
|
267
|
+
"import com.facebook.react.ReactApplication",
|
|
268
|
+
"import com.facebook.react.ReactApplication;\nimport io.yaver.feedback.YaverHotReloadPackage;\nimport io.yaver.feedback.YaverHotReloadModule;"
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Register package in getPackages()
|
|
272
|
+
if (contents.includes("packages.add(")) {
|
|
273
|
+
// Find the last packages.add() and add ours after
|
|
274
|
+
const lastAdd = contents.lastIndexOf("packages.add(");
|
|
275
|
+
const lineEnd = contents.indexOf("\n", lastAdd);
|
|
276
|
+
contents =
|
|
277
|
+
contents.slice(0, lineEnd + 1) +
|
|
278
|
+
" packages.add(new YaverHotReloadPackage());\n" +
|
|
279
|
+
contents.slice(lineEnd + 1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Override getJSBundleFile to check for hot bundle
|
|
283
|
+
if (contents.includes("getJSMainModuleName()") && !contents.includes("getJSBundleFile")) {
|
|
284
|
+
const mainModuleIdx = contents.indexOf("getJSMainModuleName()");
|
|
285
|
+
const methodStart = contents.lastIndexOf("@Override", mainModuleIdx);
|
|
286
|
+
contents =
|
|
287
|
+
contents.slice(0, methodStart) +
|
|
288
|
+
`@Override
|
|
289
|
+
protected String getJSBundleFile() {
|
|
290
|
+
// Yaver Feedback SDK: load hot-reloaded bundle if available
|
|
291
|
+
java.io.File hotBundle = YaverHotReloadModule.getSavedBundleFile(getApplicationContext());
|
|
292
|
+
if (hotBundle != null) return hotBundle.getAbsolutePath();
|
|
293
|
+
return super.getJSBundleFile();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
` +
|
|
297
|
+
contents.slice(methodStart);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
config.modResults.contents = contents;
|
|
301
|
+
return config;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return config;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function withYaverFeedback(config, props) {
|
|
61
308
|
config = withYaverFeedbackIOS(config);
|
|
62
309
|
config = withYaverFeedbackAndroid(config);
|
|
310
|
+
|
|
311
|
+
// Hot reload native module is opt-in via enableHotReload: true
|
|
312
|
+
// Skip by default — apps running inside Yaver container get hot reload
|
|
313
|
+
// via YaverBundleLoader, standalone dev builds use DevSettings.reload()
|
|
314
|
+
const enableHotReload = props?.enableHotReload === true;
|
|
315
|
+
if (enableHotReload) {
|
|
316
|
+
config = withYaverHotReloadNativeModule(config);
|
|
317
|
+
config = withYaverAppDelegateHook(config);
|
|
318
|
+
config = withYaverAndroidHotReload(config);
|
|
319
|
+
}
|
|
320
|
+
|
|
63
321
|
return config;
|
|
64
322
|
}
|
|
65
323
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(YaverHotReload, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(loadBundle:(NSString *)urlString
|
|
6
|
+
headers:(NSDictionary *)headers
|
|
7
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
|
|
10
|
+
RCT_EXTERN_METHOD(hasBundle:(RCTPromiseResolveBlock)resolve
|
|
11
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(clearBundle:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
@end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hot reload native module for the Yaver Feedback SDK.
|
|
7
|
+
*
|
|
8
|
+
* Downloads a Hermes bytecode bundle from the agent, saves it to Documents,
|
|
9
|
+
* and posts a notification that the AppDelegate handler (injected by the Expo
|
|
10
|
+
* config plugin) catches to recreate the RN bridge with the new bundle.
|
|
11
|
+
*
|
|
12
|
+
* Supports N reloads — each reload tears down the old bridge and creates
|
|
13
|
+
* a fresh one pointing to the updated bundle file.
|
|
14
|
+
*/
|
|
15
|
+
@objc(YaverHotReload)
|
|
16
|
+
class YaverHotReload: NSObject {
|
|
17
|
+
|
|
18
|
+
static let bundleDir = "yaver-hot-reload"
|
|
19
|
+
static let bundleFile = "main.jsbundle"
|
|
20
|
+
static let reloadNotification = Notification.Name("YaverHotReloadBundle")
|
|
21
|
+
|
|
22
|
+
override static func requiresMainQueueSetup() -> Bool { return true }
|
|
23
|
+
|
|
24
|
+
/// Download a Hermes bundle from the agent and trigger a bridge reload.
|
|
25
|
+
@objc func loadBundle(_ urlString: String,
|
|
26
|
+
headers: NSDictionary?,
|
|
27
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
28
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
29
|
+
guard let bundleURL = URL(string: urlString) else {
|
|
30
|
+
reject("INVALID_URL", "Invalid bundle URL", nil); return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var request = URLRequest(url: bundleURL)
|
|
34
|
+
request.timeoutInterval = 60
|
|
35
|
+
if let headers = headers as? [String: String] {
|
|
36
|
+
for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
NSLog("[YaverHotReload] downloading bundle from %@...", urlString)
|
|
40
|
+
URLSession.shared.dataTask(with: request) { data, response, error in
|
|
41
|
+
if let error = error {
|
|
42
|
+
reject("DOWNLOAD_FAILED", error.localizedDescription, error); return
|
|
43
|
+
}
|
|
44
|
+
guard let data = data, data.count > 0 else {
|
|
45
|
+
reject("EMPTY_BUNDLE", "Empty bundle response", nil); return
|
|
46
|
+
}
|
|
47
|
+
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
|
|
48
|
+
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
49
|
+
reject("HTTP_ERROR", "Status \(code)", nil); return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Basic Hermes bytecode validation
|
|
53
|
+
if data.count >= 12 {
|
|
54
|
+
let magic: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) }
|
|
55
|
+
if magic == 0x1F1903C1 {
|
|
56
|
+
let bcVersion: UInt32 = data.withUnsafeBytes { $0.load(fromByteOffset: 8, as: UInt32.self) }
|
|
57
|
+
NSLog("[YaverHotReload] Hermes bytecode BC%d, %d bytes", bcVersion, data.count)
|
|
58
|
+
} else {
|
|
59
|
+
NSLog("[YaverHotReload] WARNING: not Hermes bytecode (magic=0x%08X)", magic)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
do {
|
|
64
|
+
let savePath = YaverHotReload.savedBundlePath()
|
|
65
|
+
let dir = savePath.deletingLastPathComponent()
|
|
66
|
+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
67
|
+
try data.write(to: savePath, options: .atomic)
|
|
68
|
+
|
|
69
|
+
NSLog("[YaverHotReload] saved %d bytes, posting reload notification", data.count)
|
|
70
|
+
resolve(["loaded": true, "size": data.count])
|
|
71
|
+
|
|
72
|
+
// Post notification — AppDelegate handler recreates the bridge
|
|
73
|
+
DispatchQueue.main.async {
|
|
74
|
+
NotificationCenter.default.post(
|
|
75
|
+
name: YaverHotReload.reloadNotification,
|
|
76
|
+
object: nil,
|
|
77
|
+
userInfo: ["bundlePath": savePath.path]
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
reject("SAVE_FAILED", error.localizedDescription, error)
|
|
82
|
+
}
|
|
83
|
+
}.resume()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@objc func hasBundle(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
87
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
88
|
+
resolve(FileManager.default.fileExists(atPath: YaverHotReload.savedBundlePath().path))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objc func clearBundle(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
92
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
93
|
+
let dir = YaverHotReload.savedBundlePath().deletingLastPathComponent()
|
|
94
|
+
try? FileManager.default.removeItem(at: dir)
|
|
95
|
+
resolve(true)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Static helpers
|
|
99
|
+
|
|
100
|
+
static func savedBundlePath() -> URL {
|
|
101
|
+
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
102
|
+
return docs
|
|
103
|
+
.appendingPathComponent(bundleDir, isDirectory: true)
|
|
104
|
+
.appendingPathComponent(bundleFile)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Returns the hot-reloaded bundle URL if one exists on disk.
|
|
108
|
+
@objc static func bundleURL() -> URL? {
|
|
109
|
+
let path = savedBundlePath()
|
|
110
|
+
return FileManager.default.fileExists(atPath: path.path) ? path : nil
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Visual feedback SDK for Yaver — shake-to-report, screen recording, voice annotations for vibe coding",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"src/",
|
|
9
|
+
"ios/",
|
|
10
|
+
"android/",
|
|
9
11
|
"app.plugin.js"
|
|
10
12
|
],
|
|
11
|
-
"license": "
|
|
13
|
+
"license": "Apache-2.0",
|
|
12
14
|
"repository": {
|
|
13
15
|
"type": "git",
|
|
14
16
|
"url": "https://github.com/kivanccakmak/yaver.io",
|
|
@@ -20,20 +22,35 @@
|
|
|
20
22
|
"@react-native-async-storage/async-storage": ">=1.17.0"
|
|
21
23
|
},
|
|
22
24
|
"peerDependenciesMeta": {
|
|
23
|
-
"@expo/config-plugins": {
|
|
24
|
-
|
|
25
|
+
"@expo/config-plugins": {
|
|
26
|
+
"optional": true
|
|
27
|
+
},
|
|
28
|
+
"expo-constants": {
|
|
29
|
+
"optional": true
|
|
30
|
+
}
|
|
25
31
|
},
|
|
26
32
|
"devDependencies": {
|
|
27
33
|
"jest": "^29.0.0",
|
|
28
34
|
"@types/jest": "^29.0.0",
|
|
35
|
+
"ts-jest": "^29.1.0",
|
|
29
36
|
"typescript": "^5.0.0"
|
|
30
37
|
},
|
|
31
38
|
"scripts": {
|
|
32
39
|
"test": "jest"
|
|
33
40
|
},
|
|
34
41
|
"jest": {
|
|
35
|
-
"preset": "
|
|
36
|
-
"
|
|
42
|
+
"preset": "ts-jest",
|
|
43
|
+
"testEnvironment": "node",
|
|
44
|
+
"testMatch": [
|
|
45
|
+
"**/src/__tests__/**/*.test.ts"
|
|
46
|
+
]
|
|
37
47
|
},
|
|
38
|
-
"keywords": [
|
|
48
|
+
"keywords": [
|
|
49
|
+
"yaver",
|
|
50
|
+
"feedback",
|
|
51
|
+
"bug-report",
|
|
52
|
+
"screen-recording",
|
|
53
|
+
"vibe-coding",
|
|
54
|
+
"react-native"
|
|
55
|
+
]
|
|
39
56
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { DeviceEventEmitter, Modal } from 'react-native';
|
|
3
|
+
import { YaverLoginScreen } from './LoginScreen';
|
|
4
|
+
import { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
5
|
+
import { YaverFeedback } from './YaverFeedback';
|
|
6
|
+
import { getToken, RemoteDevice } from './auth';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Presentation layer for the SDK's auth + machine-picker modals.
|
|
10
|
+
*
|
|
11
|
+
* Mounts automatically inside `<FeedbackModal />`, so consumers of the SDK
|
|
12
|
+
* get in-app login with no extra wiring. It listens for events emitted by
|
|
13
|
+
* `YaverFeedback.showLogin()` / `showMachinePicker()`:
|
|
14
|
+
*
|
|
15
|
+
* yaverFeedback:startLogin → show login modal
|
|
16
|
+
* yaverFeedback:startMachinePicker → show machine picker
|
|
17
|
+
*
|
|
18
|
+
* The overlay closes itself once login/pick succeeds, then re-emits
|
|
19
|
+
* `yaverFeedback:startReport` so the user continues straight into the
|
|
20
|
+
* feedback flow they originally triggered.
|
|
21
|
+
*/
|
|
22
|
+
export const AuthOverlay: React.FC = () => {
|
|
23
|
+
const [loginVisible, setLoginVisible] = useState(false);
|
|
24
|
+
const [pickerVisible, setPickerVisible] = useState(false);
|
|
25
|
+
const [token, setToken] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let mounted = true;
|
|
29
|
+
(async () => {
|
|
30
|
+
const cached = await getToken();
|
|
31
|
+
if (mounted && cached) setToken(cached);
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
const loginSub = DeviceEventEmitter.addListener(
|
|
35
|
+
'yaverFeedback:startLogin',
|
|
36
|
+
() => setLoginVisible(true),
|
|
37
|
+
);
|
|
38
|
+
const pickerSub = DeviceEventEmitter.addListener(
|
|
39
|
+
'yaverFeedback:startMachinePicker',
|
|
40
|
+
async () => {
|
|
41
|
+
const cached = await getToken();
|
|
42
|
+
if (cached) setToken(cached);
|
|
43
|
+
if (cached) setPickerVisible(true);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
return () => {
|
|
47
|
+
mounted = false;
|
|
48
|
+
loginSub.remove();
|
|
49
|
+
pickerSub.remove();
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleLoggedIn = async (newToken: string) => {
|
|
54
|
+
setToken(newToken);
|
|
55
|
+
await YaverFeedback.setAuthToken(newToken);
|
|
56
|
+
setLoginVisible(false);
|
|
57
|
+
setPickerVisible(true);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleDevicePicked = async (device: RemoteDevice) => {
|
|
61
|
+
await YaverFeedback.setPreferredDevice(device.deviceId);
|
|
62
|
+
setPickerVisible(false);
|
|
63
|
+
// Continue straight into the feedback flow the user originally triggered.
|
|
64
|
+
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<Modal
|
|
70
|
+
visible={loginVisible}
|
|
71
|
+
animationType="slide"
|
|
72
|
+
presentationStyle="fullScreen"
|
|
73
|
+
onRequestClose={() => setLoginVisible(false)}
|
|
74
|
+
>
|
|
75
|
+
<YaverLoginScreen
|
|
76
|
+
onLoggedIn={handleLoggedIn}
|
|
77
|
+
onCancel={() => setLoginVisible(false)}
|
|
78
|
+
/>
|
|
79
|
+
</Modal>
|
|
80
|
+
|
|
81
|
+
<Modal
|
|
82
|
+
visible={pickerVisible && !!token}
|
|
83
|
+
animationType="slide"
|
|
84
|
+
presentationStyle="fullScreen"
|
|
85
|
+
onRequestClose={() => setPickerVisible(false)}
|
|
86
|
+
>
|
|
87
|
+
{token && (
|
|
88
|
+
<YaverMachinePickerScreen
|
|
89
|
+
token={token}
|
|
90
|
+
onPick={handleDevicePicked}
|
|
91
|
+
onCancel={() => setPickerVisible(false)}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
</Modal>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
};
|
package/src/BlackBox.ts
CHANGED
|
@@ -7,7 +7,15 @@ import { YaverFeedback } from './YaverFeedback';
|
|
|
7
7
|
* These mirror the Go BlackBoxEvent struct on the agent side.
|
|
8
8
|
*/
|
|
9
9
|
export interface BlackBoxEvent {
|
|
10
|
-
type:
|
|
10
|
+
type:
|
|
11
|
+
| 'log'
|
|
12
|
+
| 'error'
|
|
13
|
+
| 'navigation'
|
|
14
|
+
| 'lifecycle'
|
|
15
|
+
| 'network'
|
|
16
|
+
| 'state'
|
|
17
|
+
| 'render'
|
|
18
|
+
| 'track';
|
|
11
19
|
level?: 'info' | 'warn' | 'error';
|
|
12
20
|
message: string;
|
|
13
21
|
timestamp: number;
|
|
@@ -94,7 +102,7 @@ export class BlackBox {
|
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
BlackBox.baseUrl = feedbackConfig.agentUrl.replace(/\/$/, '');
|
|
97
|
-
BlackBox.authToken = feedbackConfig.authToken;
|
|
105
|
+
BlackBox.authToken = feedbackConfig.authToken ?? null;
|
|
98
106
|
BlackBox.deviceId = config?.deviceId ?? BlackBox.generateDeviceId();
|
|
99
107
|
BlackBox.appName = config?.appName ?? '';
|
|
100
108
|
BlackBox.flushInterval = config?.flushInterval ?? 2000;
|
|
@@ -153,6 +161,37 @@ export class BlackBox {
|
|
|
153
161
|
BlackBox.push({ type: 'log', level: 'error', message, timestamp: Date.now(), source, metadata });
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
// ─── Track events ────────────────────────────────────────────────
|
|
165
|
+
//
|
|
166
|
+
// Business-event ingest. Routed into the agent's analytics ledger
|
|
167
|
+
// via the same BlackBox stream so the dev doesn't pay Mixpanel
|
|
168
|
+
// / Amplitude for "the user tapped Purchase." Zero dashboards in
|
|
169
|
+
// yaver; export to CSV / webhook into PostHog if you want charts.
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Record a business event. Fires into the analytics ledger on
|
|
173
|
+
* the agent — see GET /analytics/events.csv.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* BlackBox.track('purchase_completed', {
|
|
178
|
+
* amount: '9.99',
|
|
179
|
+
* currency: 'USD',
|
|
180
|
+
* plan: 'pro',
|
|
181
|
+
* });
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
static track(name: string, props?: Record<string, unknown>, route?: string): void {
|
|
185
|
+
if (!name) return;
|
|
186
|
+
BlackBox.push({
|
|
187
|
+
type: 'track',
|
|
188
|
+
message: name,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
route,
|
|
191
|
+
metadata: props,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
156
195
|
// ─── Errors ──────────────────────────────────────────────────────
|
|
157
196
|
|
|
158
197
|
/** Record a caught error with stack trace. Also adds to the feedback error buffer. */
|