yaver-feedback-react-native 0.4.0 → 0.5.2
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/package.json +22 -7
- package/src/AuthOverlay.tsx +97 -0
- package/src/BlackBox.ts +41 -2
- package/src/Discovery.ts +26 -13
- package/src/FeedbackModal.tsx +9 -2
- package/src/LoginScreen.tsx +395 -0
- package/src/MachinePickerScreen.tsx +196 -0
- package/src/P2PClient.ts +110 -0
- package/src/ShakeDetector.ts +95 -35
- package/src/YaverFeedback.ts +257 -9
- 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/src/ShakeDetector.ts
CHANGED
|
@@ -1,57 +1,117 @@
|
|
|
1
1
|
import { DeviceEventEmitter, Platform } from 'react-native';
|
|
2
2
|
|
|
3
|
-
const SHAKE_THRESHOLD = 1.5; // g-force threshold
|
|
4
3
|
const SHAKE_TIMEOUT_MS = 1000; // minimum time between shakes
|
|
5
|
-
const
|
|
4
|
+
const ACCEL_THRESHOLD_G = 1.8; // peak g-force that qualifies as a shake event
|
|
5
|
+
const ACCEL_MIN_HITS = 3; // peaks within the window before we fire
|
|
6
|
+
const ACCEL_WINDOW_MS = 800; // rolling window
|
|
7
|
+
const ACCEL_SAMPLE_INTERVAL_MS = 80; // ≈12Hz — cheap but catches shakes
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Detects device shake gestures.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Two detection paths run in parallel so shake works in both Debug and
|
|
13
|
+
* Release / TestFlight builds:
|
|
14
|
+
*
|
|
15
|
+
* 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
|
|
16
|
+
* on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
|
|
17
|
+
* on iOS, which is why TestFlight builds never saw a shake prior to
|
|
18
|
+
* SDK 0.5.1.
|
|
19
|
+
* 2. Accelerometer-based fallback via `expo-sensors` (optional peer
|
|
20
|
+
* dep). When the host app has `expo-sensors` installed we subscribe
|
|
21
|
+
* to the accelerometer and fire on a burst of above-threshold peaks.
|
|
22
|
+
* This path works identically in Debug, Release, and TestFlight.
|
|
23
|
+
*
|
|
24
|
+
* Both paths share a 1-second debounce so a single shake never fires the
|
|
25
|
+
* callback twice.
|
|
12
26
|
*/
|
|
13
27
|
export class ShakeDetector {
|
|
14
|
-
private
|
|
28
|
+
private devMenuSub: { remove(): void } | null = null;
|
|
29
|
+
private accelSub: { remove(): void } | null = null;
|
|
15
30
|
private lastShakeTime = 0;
|
|
31
|
+
private peakTimestamps: number[] = [];
|
|
16
32
|
|
|
17
|
-
/**
|
|
18
|
-
* Start listening for shake gestures.
|
|
19
|
-
* @param onShake - Callback invoked when a shake is detected.
|
|
20
|
-
*/
|
|
21
33
|
start(onShake: () => void): void {
|
|
22
34
|
this.stop();
|
|
35
|
+
this.subscribeDevMenu(onShake);
|
|
36
|
+
this.subscribeAccelerometer(onShake);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
if (now - this.lastShakeTime > SHAKE_TIMEOUT_MS) {
|
|
30
|
-
this.lastShakeTime = now;
|
|
31
|
-
onShake();
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return;
|
|
39
|
+
stop(): void {
|
|
40
|
+
if (this.devMenuSub) {
|
|
41
|
+
this.devMenuSub.remove();
|
|
42
|
+
this.devMenuSub = null;
|
|
35
43
|
}
|
|
44
|
+
if (this.accelSub) {
|
|
45
|
+
this.accelSub.remove();
|
|
46
|
+
this.accelSub = null;
|
|
47
|
+
}
|
|
48
|
+
this.peakTimestamps = [];
|
|
49
|
+
}
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
private fire(onShake: () => void): void {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (now - this.lastShakeTime <= SHAKE_TIMEOUT_MS) return;
|
|
54
|
+
this.lastShakeTime = now;
|
|
55
|
+
this.peakTimestamps = [];
|
|
56
|
+
onShake();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dev-menu / platform-native event listener. On iOS this is the
|
|
61
|
+
* `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
|
|
62
|
+
* the `ShakeEvent` name a handful of third-party shake libraries emit.
|
|
63
|
+
*/
|
|
64
|
+
private subscribeDevMenu(onShake: () => void): void {
|
|
65
|
+
const eventName = Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
|
|
66
|
+
this.devMenuSub = DeviceEventEmitter.addListener(eventName, () => {
|
|
67
|
+
this.fire(onShake);
|
|
47
68
|
});
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Accelerometer-based detection. Uses `expo-sensors` when available —
|
|
73
|
+
* the host app's own dependency, not the SDK's, so apps that don't
|
|
74
|
+
* want the extra native surface get the Dev-menu path only.
|
|
75
|
+
*/
|
|
76
|
+
private subscribeAccelerometer(onShake: () => void): void {
|
|
77
|
+
let Accelerometer: {
|
|
78
|
+
setUpdateInterval: (ms: number) => void;
|
|
79
|
+
addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
|
|
80
|
+
remove(): void;
|
|
81
|
+
};
|
|
82
|
+
} | null = null;
|
|
83
|
+
try {
|
|
84
|
+
// Optional peer dep — if it isn't installed, we simply skip this path.
|
|
85
|
+
Accelerometer = require('expo-sensors').Accelerometer;
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!Accelerometer) return;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
Accelerometer.setUpdateInterval(ACCEL_SAMPLE_INTERVAL_MS);
|
|
93
|
+
} catch {
|
|
94
|
+
// Some platforms reject zero-value intervals; fall through with defaults
|
|
55
95
|
}
|
|
96
|
+
|
|
97
|
+
this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
|
|
98
|
+
// Magnitude of acceleration vector (in g). Subtract 1 so a stationary
|
|
99
|
+
// device reports ~0 rather than the 1g of gravity.
|
|
100
|
+
const mag = Math.sqrt(x * x + y * y + z * z);
|
|
101
|
+
if (mag - 1 < ACCEL_THRESHOLD_G - 1) return;
|
|
102
|
+
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
this.peakTimestamps.push(now);
|
|
105
|
+
// Drop peaks that fell out of the rolling window.
|
|
106
|
+
while (
|
|
107
|
+
this.peakTimestamps.length > 0 &&
|
|
108
|
+
now - this.peakTimestamps[0] > ACCEL_WINDOW_MS
|
|
109
|
+
) {
|
|
110
|
+
this.peakTimestamps.shift();
|
|
111
|
+
}
|
|
112
|
+
if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
|
|
113
|
+
this.fire(onShake);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
56
116
|
}
|
|
57
117
|
}
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { YaverDiscovery } from './Discovery';
|
|
|
3
3
|
import { BlackBox } from './BlackBox';
|
|
4
4
|
import { P2PClient } from './P2PClient';
|
|
5
5
|
import { ShakeDetector } from './ShakeDetector';
|
|
6
|
+
import {
|
|
7
|
+
configureAuthEndpoints,
|
|
8
|
+
getToken,
|
|
9
|
+
getSelectedDeviceId,
|
|
10
|
+
clearToken,
|
|
11
|
+
clearSelectedDeviceId,
|
|
12
|
+
DEFAULT_CONVEX_SITE_URL,
|
|
13
|
+
} from './auth';
|
|
6
14
|
|
|
7
15
|
let config: FeedbackConfig | null = null;
|
|
8
16
|
let enabled = false;
|
|
@@ -16,6 +24,13 @@ let maxErrors = 5;
|
|
|
16
24
|
/** Track whether BlackBox was running before disable (to restart on enable). */
|
|
17
25
|
let blackBoxWasStreaming = false;
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Flag evaluation cache — 30s TTL per `userId|key`. Prevents a
|
|
29
|
+
* tight render loop from hammering /flags/eval when the dev calls
|
|
30
|
+
* `YaverFeedback.getFlag()` every frame.
|
|
31
|
+
*/
|
|
32
|
+
const flagCache: Map<string, { value: unknown; at: number }> = new Map();
|
|
33
|
+
|
|
19
34
|
/**
|
|
20
35
|
* Main entry point for the Yaver Feedback SDK.
|
|
21
36
|
* Call `YaverFeedback.init()` once at app startup.
|
|
@@ -34,9 +49,23 @@ export class YaverFeedback {
|
|
|
34
49
|
maxRecordingDuration: 120,
|
|
35
50
|
feedbackMode: 'batch',
|
|
36
51
|
agentCommentaryLevel: 0,
|
|
52
|
+
autoLogin: true,
|
|
37
53
|
...cfg,
|
|
38
54
|
};
|
|
39
55
|
|
|
56
|
+
// Route the in-SDK login screen to prod yaver.io by default; callers may
|
|
57
|
+
// override for staging via authConvexSiteUrl / authWebBaseUrl.
|
|
58
|
+
configureAuthEndpoints({
|
|
59
|
+
convexSiteUrl: cfg.authConvexSiteUrl,
|
|
60
|
+
webBaseUrl: cfg.authWebBaseUrl,
|
|
61
|
+
});
|
|
62
|
+
// If no explicit convexUrl was set but we have an auth site URL, use it
|
|
63
|
+
// so Discovery.discoverFromConvex() has somewhere to look up the user's
|
|
64
|
+
// machines (works for both LAN-direct and off-LAN relay paths).
|
|
65
|
+
if (!config.convexUrl) {
|
|
66
|
+
config.convexUrl = cfg.authConvexSiteUrl ?? DEFAULT_CONVEX_SITE_URL;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
// Default: enabled only in dev mode
|
|
41
70
|
if (cfg.enabled !== undefined) {
|
|
42
71
|
enabled = cfg.enabled;
|
|
@@ -44,13 +73,20 @@ export class YaverFeedback {
|
|
|
44
73
|
enabled = typeof __DEV__ !== 'undefined' ? __DEV__ : false;
|
|
45
74
|
}
|
|
46
75
|
|
|
76
|
+
// Hydrate cached auth token + preferred device from AsyncStorage so the
|
|
77
|
+
// SDK reconnects silently on subsequent launches. If autoLogin is false
|
|
78
|
+
// the caller is responsible for providing authToken themselves.
|
|
79
|
+
if (config.autoLogin !== false && enabled) {
|
|
80
|
+
void YaverFeedback.hydrateSession();
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
// Create P2P client if we have a URL
|
|
48
84
|
if (config.agentUrl) {
|
|
49
|
-
p2pClient = new P2PClient(config.agentUrl, config.authToken);
|
|
85
|
+
p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '');
|
|
50
86
|
} else {
|
|
51
87
|
p2pClient = null;
|
|
52
88
|
// Auto-discover agent in the background when convexUrl or LAN is available
|
|
53
|
-
if (enabled) {
|
|
89
|
+
if (enabled && (config.authToken || config.preferredDeviceId)) {
|
|
54
90
|
YaverFeedback.discoverAgent();
|
|
55
91
|
}
|
|
56
92
|
}
|
|
@@ -117,6 +153,7 @@ export class YaverFeedback {
|
|
|
117
153
|
static async discoverAgent(): Promise<void> {
|
|
118
154
|
if (!config || !enabled) return;
|
|
119
155
|
if (config.agentUrl) return; // already have a URL
|
|
156
|
+
if (!config.authToken) return; // need auth before discovery can succeed
|
|
120
157
|
|
|
121
158
|
try {
|
|
122
159
|
const result = await YaverDiscovery.discover({
|
|
@@ -126,13 +163,109 @@ export class YaverFeedback {
|
|
|
126
163
|
});
|
|
127
164
|
if (result && config) {
|
|
128
165
|
config.agentUrl = result.url;
|
|
129
|
-
p2pClient = new P2PClient(result.url, config.authToken);
|
|
166
|
+
p2pClient = new P2PClient(result.url, config.authToken ?? '');
|
|
130
167
|
}
|
|
131
168
|
} catch {
|
|
132
169
|
// Discovery failed — FloatingButton will show disconnected, user can retry
|
|
133
170
|
}
|
|
134
171
|
}
|
|
135
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
175
|
+
* by the in-SDK login + machine-picker screens). When present the SDK can
|
|
176
|
+
* reconnect silently on launch without re-prompting the user. Safe to call
|
|
177
|
+
* multiple times — it only overrides values the caller did not already set.
|
|
178
|
+
*/
|
|
179
|
+
static async hydrateSession(): Promise<void> {
|
|
180
|
+
if (!config) return;
|
|
181
|
+
try {
|
|
182
|
+
if (!config.authToken) {
|
|
183
|
+
const cached = await getToken();
|
|
184
|
+
if (cached) {
|
|
185
|
+
config.authToken = cached;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!config.preferredDeviceId) {
|
|
189
|
+
const cachedDevice = await getSelectedDeviceId();
|
|
190
|
+
if (cachedDevice) {
|
|
191
|
+
config.preferredDeviceId = cachedDevice;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (config.authToken && !config.agentUrl) {
|
|
195
|
+
await YaverFeedback.discoverAgent();
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// hydration best-effort
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update the signed-in session token (e.g. after the in-SDK login screen
|
|
204
|
+
* succeeds). Rebuilds the P2P client and kicks off agent discovery.
|
|
205
|
+
*/
|
|
206
|
+
static async setAuthToken(token: string): Promise<void> {
|
|
207
|
+
if (!config) return;
|
|
208
|
+
config.authToken = token;
|
|
209
|
+
if (config.agentUrl) {
|
|
210
|
+
p2pClient = new P2PClient(config.agentUrl, token);
|
|
211
|
+
} else {
|
|
212
|
+
await YaverFeedback.discoverAgent();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Returns true once the SDK has a session token it can use. */
|
|
217
|
+
static isAuthed(): boolean {
|
|
218
|
+
return Boolean(config?.authToken);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Request the embedded FeedbackModal to show the login screen. Works by
|
|
223
|
+
* emitting an event the modal listens for — avoids forcing the host app
|
|
224
|
+
* to mount a second navigator.
|
|
225
|
+
*/
|
|
226
|
+
static showLogin(): void {
|
|
227
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
228
|
+
DeviceEventEmitter.emit('yaverFeedback:startLogin');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Request the embedded FeedbackModal to show the machine picker. Requires
|
|
233
|
+
* an active session; no-ops otherwise.
|
|
234
|
+
*/
|
|
235
|
+
static showMachinePicker(): void {
|
|
236
|
+
if (!YaverFeedback.isAuthed()) return;
|
|
237
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
238
|
+
DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Update the selected remote device. Resets the cached agent URL so the
|
|
243
|
+
* next `startReport()` (or FloatingButton press) rediscovers against the
|
|
244
|
+
* newly-selected machine.
|
|
245
|
+
*/
|
|
246
|
+
static async setPreferredDevice(deviceId: string): Promise<void> {
|
|
247
|
+
if (!config) return;
|
|
248
|
+
config.preferredDeviceId = deviceId;
|
|
249
|
+
config.agentUrl = undefined;
|
|
250
|
+
p2pClient = null;
|
|
251
|
+
await YaverFeedback.discoverAgent();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
256
|
+
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
257
|
+
*/
|
|
258
|
+
static async signOut(): Promise<void> {
|
|
259
|
+
await clearToken();
|
|
260
|
+
await clearSelectedDeviceId();
|
|
261
|
+
if (config) {
|
|
262
|
+
config.authToken = undefined;
|
|
263
|
+
config.preferredDeviceId = undefined;
|
|
264
|
+
config.agentUrl = undefined;
|
|
265
|
+
}
|
|
266
|
+
p2pClient = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
136
269
|
/**
|
|
137
270
|
* Manually trigger the feedback collection flow.
|
|
138
271
|
* Opens the feedback modal if the SDK is initialized and enabled.
|
|
@@ -148,6 +281,18 @@ export class YaverFeedback {
|
|
|
148
281
|
return;
|
|
149
282
|
}
|
|
150
283
|
|
|
284
|
+
// If the caller has autoLogin enabled and we have no session yet, show
|
|
285
|
+
// the in-SDK login flow instead of a failing discovery + warning spam.
|
|
286
|
+
if (!config.authToken) {
|
|
287
|
+
if (config.autoLogin !== false) {
|
|
288
|
+
await YaverFeedback.hydrateSession();
|
|
289
|
+
}
|
|
290
|
+
if (!config.authToken) {
|
|
291
|
+
YaverFeedback.showLogin();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
151
296
|
// Auto-discover if no agent URL was provided
|
|
152
297
|
if (!config.agentUrl) {
|
|
153
298
|
try {
|
|
@@ -158,9 +303,15 @@ export class YaverFeedback {
|
|
|
158
303
|
});
|
|
159
304
|
if (result) {
|
|
160
305
|
config.agentUrl = result.url;
|
|
161
|
-
p2pClient = new P2PClient(result.url, config.authToken);
|
|
306
|
+
p2pClient = new P2PClient(result.url, config.authToken ?? '');
|
|
307
|
+
} else if (config.autoLogin !== false && !config.preferredDeviceId) {
|
|
308
|
+
// No agent discovered and no device picked yet — prompt the user
|
|
309
|
+
// to pick one of their machines (handles the non-LAN case where
|
|
310
|
+
// relay discovery requires knowing which deviceId to target).
|
|
311
|
+
YaverFeedback.showMachinePicker();
|
|
312
|
+
return;
|
|
162
313
|
} else {
|
|
163
|
-
console.warn('[YaverFeedback] No agent found.
|
|
314
|
+
console.warn('[YaverFeedback] No agent found. Check that `yaver serve` is running on the selected machine.');
|
|
164
315
|
}
|
|
165
316
|
} catch (err) {
|
|
166
317
|
console.warn('[YaverFeedback] Auto-discovery failed:', err);
|
|
@@ -314,6 +465,103 @@ export class YaverFeedback {
|
|
|
314
465
|
return config?.agentCommentaryLevel ?? 0;
|
|
315
466
|
}
|
|
316
467
|
|
|
468
|
+
// ─── One-stop SaaS replacement methods ─────────────────────────
|
|
469
|
+
//
|
|
470
|
+
// These are the three solo-dev SaaS-replacement entry points
|
|
471
|
+
// wired into YaverFeedback so there's exactly one import path
|
|
472
|
+
// for the dev's app code: track / getFlag / checkUpdate.
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Record a business event. Routes through BlackBox so the agent
|
|
476
|
+
* persists it to the analytics ledger (no dashboards — export
|
|
477
|
+
* via CSV or webhook into PostHog).
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```ts
|
|
481
|
+
* YaverFeedback.track('purchase_completed', { amount: '9.99' });
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
static track(name: string, props?: Record<string, unknown>, route?: string): void {
|
|
485
|
+
if (!enabled) return;
|
|
486
|
+
BlackBox.track(name, props, route);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Evaluate a single feature flag for a user. Results are cached
|
|
491
|
+
* for 30 seconds inside YaverFeedback so a tight loop evaluating
|
|
492
|
+
* the same key doesn't hammer the agent.
|
|
493
|
+
*
|
|
494
|
+
* @param key — flag key (must exist on the agent)
|
|
495
|
+
* @param defaultValue — returned if the flag is missing / offline
|
|
496
|
+
* @param userId — stable user identifier for rollout bucketing
|
|
497
|
+
*/
|
|
498
|
+
static async getFlag<T = boolean | string>(
|
|
499
|
+
key: string,
|
|
500
|
+
defaultValue: T,
|
|
501
|
+
userId: string = 'anonymous',
|
|
502
|
+
): Promise<T> {
|
|
503
|
+
if (!enabled || !p2pClient) return defaultValue;
|
|
504
|
+
const cacheKey = `${userId}|${key}`;
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
const cached = flagCache.get(cacheKey);
|
|
507
|
+
if (cached && now - cached.at < 30_000) {
|
|
508
|
+
return (cached.value as T) ?? defaultValue;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const val = await p2pClient.flagsEvaluateOne<T>(key, userId);
|
|
512
|
+
flagCache.set(cacheKey, { value: val ?? defaultValue, at: now });
|
|
513
|
+
return (val as T) ?? defaultValue;
|
|
514
|
+
} catch {
|
|
515
|
+
return defaultValue;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Bulk evaluate every flag for a user. Cached on the same 30s
|
|
521
|
+
* window as getFlag — use this when boot needs a handful of
|
|
522
|
+
* flags in one roundtrip.
|
|
523
|
+
*/
|
|
524
|
+
static async getFlags(
|
|
525
|
+
userId: string = 'anonymous',
|
|
526
|
+
): Promise<Record<string, unknown>> {
|
|
527
|
+
if (!enabled || !p2pClient) return {};
|
|
528
|
+
const cacheKey = `all|${userId}`;
|
|
529
|
+
const now = Date.now();
|
|
530
|
+
const cached = flagCache.get(cacheKey);
|
|
531
|
+
if (cached && now - cached.at < 30_000) {
|
|
532
|
+
return (cached.value as Record<string, unknown>) ?? {};
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const flags = await p2pClient.flagsEvaluate(userId);
|
|
536
|
+
flagCache.set(cacheKey, { value: flags, at: now });
|
|
537
|
+
return flags;
|
|
538
|
+
} catch {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Ask what bundle this device should run. Returns the latest
|
|
545
|
+
* release manifest in the configured channel plus a rollout
|
|
546
|
+
* gate. The dev can then compare against what's currently
|
|
547
|
+
* running and prompt the user to reload.
|
|
548
|
+
*
|
|
549
|
+
* On-disk bundle swap is platform-specific — see
|
|
550
|
+
* `YaverFeedback.onUpdateAvailable` if you want a hook.
|
|
551
|
+
*/
|
|
552
|
+
static async checkUpdate(
|
|
553
|
+
channel: string = 'production',
|
|
554
|
+
deviceId?: string,
|
|
555
|
+
): Promise<Awaited<ReturnType<P2PClient['releasesLatest']>>> {
|
|
556
|
+
if (!enabled || !p2pClient) return null;
|
|
557
|
+
return p2pClient.releasesLatest(channel, deviceId);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Clear the in-memory flag cache. Useful for tests or after sign-out. */
|
|
561
|
+
static clearFlagCache(): void {
|
|
562
|
+
flagCache.clear();
|
|
563
|
+
}
|
|
564
|
+
|
|
317
565
|
/**
|
|
318
566
|
* Reporting-only mode: auto-capture screenshot + errors and send
|
|
319
567
|
* to the agent's /feedback endpoint. No modal UI — just shake and go.
|
|
@@ -334,7 +582,7 @@ export class YaverFeedback {
|
|
|
334
582
|
});
|
|
335
583
|
if (result) {
|
|
336
584
|
config.agentUrl = result.url;
|
|
337
|
-
p2pClient = new P2PClient(result.url, config.authToken);
|
|
585
|
+
p2pClient = new P2PClient(result.url, config.authToken ?? '');
|
|
338
586
|
}
|
|
339
587
|
} catch {}
|
|
340
588
|
}
|
|
@@ -375,7 +623,7 @@ export class YaverFeedback {
|
|
|
375
623
|
errors: errorBuffer.length > 0 ? [...errorBuffer] : undefined,
|
|
376
624
|
};
|
|
377
625
|
|
|
378
|
-
await uploadFeedback(config.agentUrl, config.authToken, bundle);
|
|
626
|
+
await uploadFeedback(config.agentUrl, config.authToken ?? '', bundle);
|
|
379
627
|
console.log('[YaverFeedback] Auto-report sent');
|
|
380
628
|
} catch (err) {
|
|
381
629
|
console.warn('[YaverFeedback] Auto-report failed:', err);
|
|
@@ -397,7 +645,7 @@ export class YaverFeedback {
|
|
|
397
645
|
private static defaultReload(): void {
|
|
398
646
|
if (!config?.agentUrl) return;
|
|
399
647
|
const bundleUrl = `${config.agentUrl}/dev/native-bundle`;
|
|
400
|
-
const headers = { Authorization: `Bearer ${config.authToken}` };
|
|
648
|
+
const headers = { Authorization: `Bearer ${config.authToken ?? ''}` };
|
|
401
649
|
YaverFeedback.loadBundleAndReload(bundleUrl, headers);
|
|
402
650
|
}
|
|
403
651
|
|
|
@@ -411,7 +659,7 @@ export class YaverFeedback {
|
|
|
411
659
|
const fullUrl = bundleUrl.startsWith('http')
|
|
412
660
|
? bundleUrl
|
|
413
661
|
: `${config.agentUrl}${bundleUrl}`;
|
|
414
|
-
const headers = { Authorization: `Bearer ${config.authToken}` };
|
|
662
|
+
const headers = { Authorization: `Bearer ${config.authToken ?? ''}` };
|
|
415
663
|
YaverFeedback.loadBundleAndReload(fullUrl, headers);
|
|
416
664
|
}
|
|
417
665
|
|