yaver-feedback-react-native 0.8.3 → 0.8.6
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/dist/AuthOverlay.js +45 -18
- package/dist/FeedbackModal.js +307 -2
- package/dist/LoginScreen.js +19 -5
- package/dist/MachinePickerScreen.js +44 -6
- package/dist/P2PClient.d.ts +23 -2
- package/dist/P2PClient.js +53 -4
- package/dist/QuickActionIcon.js +27 -2
- package/dist/YaverFeedback.d.ts +13 -0
- package/dist/YaverFeedback.js +78 -34
- package/dist/__tests__/AuthDevices.test.d.ts +1 -0
- package/dist/__tests__/AuthDevices.test.js +82 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +41 -0
- package/dist/types.d.ts +21 -0
- package/package.json +2 -2
- package/src/AuthOverlay.tsx +48 -19
- package/src/FeedbackModal.tsx +349 -1
- package/src/LoginScreen.tsx +19 -5
- package/src/MachinePickerScreen.tsx +45 -6
- package/src/P2PClient.ts +61 -5
- package/src/QuickActionIcon.tsx +37 -2
- package/src/YaverFeedback.ts +83 -34
- package/src/__tests__/AuthDevices.test.ts +93 -0
- package/src/auth.ts +46 -0
- package/src/types.ts +22 -0
package/dist/P2PClient.js
CHANGED
|
@@ -92,9 +92,10 @@ function friendlyReloadError(status, body) {
|
|
|
92
92
|
* support for streaming feedback, listing builds, and triggering builds.
|
|
93
93
|
*/
|
|
94
94
|
class P2PClient {
|
|
95
|
-
constructor(baseUrl, authToken) {
|
|
95
|
+
constructor(baseUrl, authToken, relayPassword = '') {
|
|
96
96
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
97
97
|
this.authToken = authToken;
|
|
98
|
+
this.relayPassword = relayPassword;
|
|
98
99
|
}
|
|
99
100
|
/** Update the base URL (e.g. after re-discovery). */
|
|
100
101
|
setBaseUrl(url) {
|
|
@@ -104,6 +105,56 @@ class P2PClient {
|
|
|
104
105
|
setAuthToken(token) {
|
|
105
106
|
this.authToken = token;
|
|
106
107
|
}
|
|
108
|
+
/** Update the relay password (used for managed-relay baseUrls). */
|
|
109
|
+
setRelayPassword(password) {
|
|
110
|
+
this.relayPassword = password;
|
|
111
|
+
}
|
|
112
|
+
/** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
|
|
113
|
+
authHeaders(extra = {}) {
|
|
114
|
+
const h = { ...extra };
|
|
115
|
+
if (this.authToken)
|
|
116
|
+
h.Authorization = `Bearer ${this.authToken}`;
|
|
117
|
+
if (this.relayPassword)
|
|
118
|
+
h['X-Relay-Password'] = this.relayPassword;
|
|
119
|
+
return h;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start a remote browser-style sign-in for a runner (codex --device-auth
|
|
123
|
+
* / claude auth login --console). Returns a session id; callers poll
|
|
124
|
+
* getRunnerBrowserAuthStatus to surface the verification URL + one-time
|
|
125
|
+
* code. No API keys involved — the CLI writes its own auth.json once
|
|
126
|
+
* the user completes the flow in any browser.
|
|
127
|
+
*/
|
|
128
|
+
async startRunnerBrowserAuth(runner) {
|
|
129
|
+
const resp = await fetch(`${this.baseUrl}/runner-auth/browser/start`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: this.authHeaders({ 'Content-Type': 'application/json' }),
|
|
132
|
+
body: JSON.stringify({ runner }),
|
|
133
|
+
});
|
|
134
|
+
if (!resp.ok) {
|
|
135
|
+
const text = await resp.text().catch(() => '');
|
|
136
|
+
throw new Error(`startRunnerBrowserAuth(${runner}) HTTP ${resp.status}: ${text}`);
|
|
137
|
+
}
|
|
138
|
+
const data = await resp.json();
|
|
139
|
+
return data.session;
|
|
140
|
+
}
|
|
141
|
+
async getRunnerBrowserAuthStatus(sessionId) {
|
|
142
|
+
const url = `${this.baseUrl}/runner-auth/browser/status?id=${encodeURIComponent(sessionId)}`;
|
|
143
|
+
const resp = await fetch(url, { headers: this.authHeaders() });
|
|
144
|
+
if (!resp.ok) {
|
|
145
|
+
const text = await resp.text().catch(() => '');
|
|
146
|
+
throw new Error(`getRunnerBrowserAuthStatus HTTP ${resp.status}: ${text}`);
|
|
147
|
+
}
|
|
148
|
+
const data = await resp.json();
|
|
149
|
+
return data.session;
|
|
150
|
+
}
|
|
151
|
+
async cancelRunnerBrowserAuth(sessionId) {
|
|
152
|
+
const url = `${this.baseUrl}/runner-auth/browser/cancel?id=${encodeURIComponent(sessionId)}`;
|
|
153
|
+
try {
|
|
154
|
+
await fetch(url, { method: 'POST', headers: this.authHeaders() });
|
|
155
|
+
}
|
|
156
|
+
catch { /* best-effort */ }
|
|
157
|
+
}
|
|
107
158
|
/** Health check — returns true if the agent is reachable. */
|
|
108
159
|
async health() {
|
|
109
160
|
try {
|
|
@@ -563,9 +614,7 @@ class P2PClient {
|
|
|
563
614
|
async request(method, path) {
|
|
564
615
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
565
616
|
method,
|
|
566
|
-
headers:
|
|
567
|
-
Authorization: `Bearer ${this.authToken}`,
|
|
568
|
-
},
|
|
617
|
+
headers: this.authHeaders(),
|
|
569
618
|
});
|
|
570
619
|
if (!response.ok) {
|
|
571
620
|
const text = await response.text().catch(() => '');
|
package/dist/QuickActionIcon.js
CHANGED
|
@@ -115,6 +115,7 @@ const QuickActionIcon = ({ color: colorProp, backgroundColor: backgroundColorPro
|
|
|
115
115
|
const [shakenThisSession, setShakenThisSession] = (0, react_1.useState)(false);
|
|
116
116
|
const [menuOpen, setMenuOpen] = (0, react_1.useState)(false);
|
|
117
117
|
const [hostSuppressed] = (0, react_1.useState)(() => isRunningInsideYaverHost());
|
|
118
|
+
const [launching, setLaunching] = (0, react_1.useState)(false);
|
|
118
119
|
// Load the persisted disable flag once on mount. Until it resolves we
|
|
119
120
|
// render nothing — a one-frame flash of the icon before hiding would
|
|
120
121
|
// be worse than a tiny delayed appearance.
|
|
@@ -162,6 +163,24 @@ const QuickActionIcon = ({ color: colorProp, backgroundColor: backgroundColorPro
|
|
|
162
163
|
colorSub.remove();
|
|
163
164
|
};
|
|
164
165
|
}, []);
|
|
166
|
+
(0, react_1.useEffect)(() => {
|
|
167
|
+
const launchSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:reportLaunch', (event) => {
|
|
168
|
+
if (event?.state === 'starting') {
|
|
169
|
+
setLaunching(true);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
setLaunching(false);
|
|
173
|
+
});
|
|
174
|
+
const reportSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startReport', () => setLaunching(false));
|
|
175
|
+
const loginSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startLogin', () => setLaunching(false));
|
|
176
|
+
const pickerSub = react_native_1.DeviceEventEmitter.addListener('yaverFeedback:startMachinePicker', () => setLaunching(false));
|
|
177
|
+
return () => {
|
|
178
|
+
launchSub.remove();
|
|
179
|
+
reportSub.remove();
|
|
180
|
+
loginSub.remove();
|
|
181
|
+
pickerSub.remove();
|
|
182
|
+
};
|
|
183
|
+
}, []);
|
|
165
184
|
const panResponder = (0, react_1.useRef)(react_native_1.PanResponder.create({
|
|
166
185
|
onStartShouldSetPanResponder: () => true,
|
|
167
186
|
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 3 || Math.abs(g.dy) > 3,
|
|
@@ -195,9 +214,11 @@ const QuickActionIcon = ({ color: colorProp, backgroundColor: backgroundColorPro
|
|
|
195
214
|
},
|
|
196
215
|
})).current;
|
|
197
216
|
const openFeedback = (0, react_1.useCallback)(() => {
|
|
217
|
+
if (launching)
|
|
218
|
+
return;
|
|
198
219
|
setMenuOpen(false);
|
|
199
220
|
void YaverFeedback_1.YaverFeedback.startReport();
|
|
200
|
-
}, []);
|
|
221
|
+
}, [launching]);
|
|
201
222
|
const hideForever = (0, react_1.useCallback)(() => {
|
|
202
223
|
setMenuOpen(false);
|
|
203
224
|
setUserDisabled(true);
|
|
@@ -233,10 +254,14 @@ const QuickActionIcon = ({ color: colorProp, backgroundColor: backgroundColorPro
|
|
|
233
254
|
didDrag.current = false;
|
|
234
255
|
return;
|
|
235
256
|
}
|
|
257
|
+
if (launching)
|
|
258
|
+
return;
|
|
236
259
|
openFeedback();
|
|
237
260
|
}} onLongPress={() => {
|
|
238
261
|
if (didDrag.current)
|
|
239
262
|
return;
|
|
263
|
+
if (launching)
|
|
264
|
+
return;
|
|
240
265
|
setMenuOpen((m) => !m);
|
|
241
266
|
}} delayLongPress={LONG_PRESS_MS} hitSlop={6} accessibilityRole="button" accessibilityLabel="Open Yaver feedback" style={({ pressed }) => [
|
|
242
267
|
styles.icon,
|
|
@@ -247,7 +272,7 @@ const QuickActionIcon = ({ color: colorProp, backgroundColor: backgroundColorPro
|
|
|
247
272
|
backgroundColor: presetColors?.backgroundColor ?? backgroundColor,
|
|
248
273
|
borderColor: presetColors?.borderColor ?? borderColor,
|
|
249
274
|
shadowColor: presetColors?.shadowColor ?? shadowColor,
|
|
250
|
-
opacity: pressed ? 0.85 : 1,
|
|
275
|
+
opacity: launching ? 0.62 : pressed ? 0.85 : 1,
|
|
251
276
|
},
|
|
252
277
|
]}>
|
|
253
278
|
<react_native_1.Text style={[
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -65,6 +65,19 @@ export declare class YaverFeedback {
|
|
|
65
65
|
static setPreferredDevice(deviceId: string): Promise<void>;
|
|
66
66
|
/** Resolve the currently selected remote machine from the authenticated device list. */
|
|
67
67
|
static getSelectedRemoteDevice(): Promise<import("./auth").RemoteDevice | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Trigger remote device-auth for a CLI runner on the selected agent
|
|
70
|
+
* (codex login --device-auth / claude auth login --console). Returns
|
|
71
|
+
* the session so the host UI can render the verification URL + code.
|
|
72
|
+
*
|
|
73
|
+
* RN UI layer owns the modal (see FeedbackModal's runner sign-in
|
|
74
|
+
* buttons). This method just proxies into P2PClient — no browser
|
|
75
|
+
* launch, no API keys, works through the relay with an SDK token
|
|
76
|
+
* that carries the runner-auth scope.
|
|
77
|
+
*/
|
|
78
|
+
static startRunnerBrowserAuth(runner: string): Promise<import('./types').RunnerBrowserAuthSession>;
|
|
79
|
+
static getRunnerBrowserAuthStatus(sessionId: string): Promise<import('./types').RunnerBrowserAuthSession>;
|
|
80
|
+
static cancelRunnerBrowserAuth(sessionId: string): Promise<void>;
|
|
68
81
|
/**
|
|
69
82
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
70
83
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -35,6 +35,7 @@ let enabled = false;
|
|
|
35
35
|
let p2pClient = null;
|
|
36
36
|
let shakeDetector = null;
|
|
37
37
|
let p2pAuthToken = null;
|
|
38
|
+
let reportLaunchInFlight = false;
|
|
38
39
|
/** Ring buffer of captured errors. */
|
|
39
40
|
let errorBuffer = [];
|
|
40
41
|
let maxErrors = 5;
|
|
@@ -396,6 +397,32 @@ class YaverFeedback {
|
|
|
396
397
|
const all = [...devices.owned, ...devices.shared];
|
|
397
398
|
return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
|
|
398
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Trigger remote device-auth for a CLI runner on the selected agent
|
|
402
|
+
* (codex login --device-auth / claude auth login --console). Returns
|
|
403
|
+
* the session so the host UI can render the verification URL + code.
|
|
404
|
+
*
|
|
405
|
+
* RN UI layer owns the modal (see FeedbackModal's runner sign-in
|
|
406
|
+
* buttons). This method just proxies into P2PClient — no browser
|
|
407
|
+
* launch, no API keys, works through the relay with an SDK token
|
|
408
|
+
* that carries the runner-auth scope.
|
|
409
|
+
*/
|
|
410
|
+
static async startRunnerBrowserAuth(runner) {
|
|
411
|
+
if (!p2pClient) {
|
|
412
|
+
throw new Error('Not connected to any agent. Select a machine first.');
|
|
413
|
+
}
|
|
414
|
+
return p2pClient.startRunnerBrowserAuth(runner);
|
|
415
|
+
}
|
|
416
|
+
static async getRunnerBrowserAuthStatus(sessionId) {
|
|
417
|
+
if (!p2pClient)
|
|
418
|
+
throw new Error('Not connected to any agent.');
|
|
419
|
+
return p2pClient.getRunnerBrowserAuthStatus(sessionId);
|
|
420
|
+
}
|
|
421
|
+
static async cancelRunnerBrowserAuth(sessionId) {
|
|
422
|
+
if (!p2pClient)
|
|
423
|
+
return;
|
|
424
|
+
await p2pClient.cancelRunnerBrowserAuth(sessionId);
|
|
425
|
+
}
|
|
399
426
|
/**
|
|
400
427
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
401
428
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
@@ -425,47 +452,64 @@ class YaverFeedback {
|
|
|
425
452
|
if (!enabled) {
|
|
426
453
|
return;
|
|
427
454
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!config.authToken) {
|
|
431
|
-
if (config.autoLogin !== false) {
|
|
432
|
-
await YaverFeedback.hydrateSession();
|
|
433
|
-
}
|
|
434
|
-
if (!config.authToken) {
|
|
435
|
-
YaverFeedback.showLogin();
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
455
|
+
if (reportLaunchInFlight) {
|
|
456
|
+
return;
|
|
438
457
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
458
|
+
reportLaunchInFlight = true;
|
|
459
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
460
|
+
DeviceEventEmitter.emit('yaverFeedback:reportLaunch', {
|
|
461
|
+
state: 'starting',
|
|
462
|
+
at: Date.now(),
|
|
463
|
+
});
|
|
464
|
+
try {
|
|
465
|
+
// If the caller has autoLogin enabled and we have no session yet, show
|
|
466
|
+
// the in-SDK login flow instead of a failing discovery + warning spam.
|
|
467
|
+
if (!config.authToken) {
|
|
468
|
+
if (config.autoLogin !== false) {
|
|
469
|
+
await YaverFeedback.hydrateSession();
|
|
450
470
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// to pick one of their machines (handles the non-LAN case where
|
|
454
|
-
// relay discovery requires knowing which deviceId to target).
|
|
455
|
-
YaverFeedback.showMachinePicker();
|
|
471
|
+
if (!config.authToken) {
|
|
472
|
+
YaverFeedback.showLogin();
|
|
456
473
|
return;
|
|
457
474
|
}
|
|
458
|
-
else {
|
|
459
|
-
console.warn('[YaverFeedback] No agent found. Check that `yaver serve` is running on the selected machine.');
|
|
460
|
-
}
|
|
461
475
|
}
|
|
462
|
-
|
|
463
|
-
|
|
476
|
+
// Auto-discover if no agent URL was provided
|
|
477
|
+
if (!config.agentUrl) {
|
|
478
|
+
try {
|
|
479
|
+
const result = await Discovery_1.YaverDiscovery.discover({
|
|
480
|
+
convexUrl: config.convexUrl,
|
|
481
|
+
authToken: config.authToken,
|
|
482
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
483
|
+
});
|
|
484
|
+
if (result) {
|
|
485
|
+
config.agentUrl = result.url;
|
|
486
|
+
await YaverFeedback.rebuildP2PClient(result.url);
|
|
487
|
+
}
|
|
488
|
+
else if (config.autoLogin !== false && !config.preferredDeviceId) {
|
|
489
|
+
// No agent discovered and no device picked yet — prompt the user
|
|
490
|
+
// to pick one of their machines (handles the non-LAN case where
|
|
491
|
+
// relay discovery requires knowing which deviceId to target).
|
|
492
|
+
YaverFeedback.showMachinePicker();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
console.warn('[YaverFeedback] No agent found. Check that `yaver serve` is running on the selected machine.');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
console.warn('[YaverFeedback] Auto-discovery failed:', err);
|
|
501
|
+
}
|
|
464
502
|
}
|
|
503
|
+
// Emit event that the FeedbackModal listens for
|
|
504
|
+
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
reportLaunchInFlight = false;
|
|
508
|
+
DeviceEventEmitter.emit('yaverFeedback:reportLaunch', {
|
|
509
|
+
state: 'settled',
|
|
510
|
+
at: Date.now(),
|
|
511
|
+
});
|
|
465
512
|
}
|
|
466
|
-
// Emit event that the FeedbackModal listens for
|
|
467
|
-
const { DeviceEventEmitter } = require('react-native');
|
|
468
|
-
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
469
513
|
}
|
|
470
514
|
/** Returns true if the SDK has been initialized. */
|
|
471
515
|
static isInitialized() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const auth_1 = require("../auth");
|
|
4
|
+
const mockFetch = jest.fn();
|
|
5
|
+
global.fetch = mockFetch;
|
|
6
|
+
describe('auth device listing', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
mockFetch.mockReset();
|
|
10
|
+
});
|
|
11
|
+
it('keeps guest-shared devices in the shared bucket', async () => {
|
|
12
|
+
mockFetch.mockResolvedValue({
|
|
13
|
+
ok: true,
|
|
14
|
+
json: () => Promise.resolve({
|
|
15
|
+
devices: [
|
|
16
|
+
{
|
|
17
|
+
deviceId: 'own-1',
|
|
18
|
+
name: 'My Mac',
|
|
19
|
+
platform: 'macos',
|
|
20
|
+
isOnline: true,
|
|
21
|
+
isGuest: false,
|
|
22
|
+
quicHost: '10.0.0.10',
|
|
23
|
+
quicPort: 18080,
|
|
24
|
+
lastHeartbeat: 123,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
deviceId: 'guest-1',
|
|
28
|
+
name: 'yaver-test-ephemeral',
|
|
29
|
+
platform: 'linux',
|
|
30
|
+
isOnline: true,
|
|
31
|
+
isGuest: true,
|
|
32
|
+
hostName: 'Kivanc Cakmak',
|
|
33
|
+
hostEmail: 'kivanc.cakmak@icloud.com',
|
|
34
|
+
accessScope: 'shared-scoped',
|
|
35
|
+
quicHost: '157.180.114.179',
|
|
36
|
+
quicPort: 18080,
|
|
37
|
+
lastHeartbeat: 456,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
const result = await (0, auth_1.listReachableDevices)('sdk-user-token');
|
|
43
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/devices/list'), expect.objectContaining({
|
|
44
|
+
headers: { Authorization: 'Bearer sdk-user-token' },
|
|
45
|
+
}));
|
|
46
|
+
expect(result.owned).toHaveLength(1);
|
|
47
|
+
expect(result.shared).toHaveLength(1);
|
|
48
|
+
expect(result.owned[0].deviceId).toBe('own-1');
|
|
49
|
+
expect(result.shared[0]).toMatchObject({
|
|
50
|
+
deviceId: 'guest-1',
|
|
51
|
+
isGuest: true,
|
|
52
|
+
hostEmail: 'kivanc.cakmak@icloud.com',
|
|
53
|
+
accessScope: 'shared-scoped',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it('shows shared devices even when the guest owns no machines', async () => {
|
|
57
|
+
mockFetch.mockResolvedValue({
|
|
58
|
+
ok: true,
|
|
59
|
+
json: () => Promise.resolve({
|
|
60
|
+
devices: [
|
|
61
|
+
{
|
|
62
|
+
deviceId: 'guest-only',
|
|
63
|
+
name: 'yaver-test-ephemeral',
|
|
64
|
+
platform: 'linux',
|
|
65
|
+
isOnline: true,
|
|
66
|
+
isGuest: true,
|
|
67
|
+
hostName: 'Kivanc Cakmak',
|
|
68
|
+
hostEmail: 'kivanc.cakmak@icloud.com',
|
|
69
|
+
accessScope: 'shared-scoped',
|
|
70
|
+
quicHost: '157.180.114.179',
|
|
71
|
+
quicPort: 18080,
|
|
72
|
+
lastHeartbeat: 789,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const result = await (0, auth_1.listReachableDevices)('guest-only-token');
|
|
78
|
+
expect(result.owned).toEqual([]);
|
|
79
|
+
expect(result.shared).toHaveLength(1);
|
|
80
|
+
expect(result.shared[0].deviceId).toBe('guest-only');
|
|
81
|
+
});
|
|
82
|
+
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -123,6 +123,10 @@ export interface DeviceList {
|
|
|
123
123
|
owned: RemoteDevice[];
|
|
124
124
|
shared: RemoteDevice[];
|
|
125
125
|
}
|
|
126
|
+
export interface DeviceReachability {
|
|
127
|
+
reachable: boolean;
|
|
128
|
+
url?: string;
|
|
129
|
+
}
|
|
126
130
|
export interface GuestInvitation {
|
|
127
131
|
hostUserId: string;
|
|
128
132
|
hostName: string;
|
|
@@ -170,6 +174,8 @@ export interface InvitationPreview {
|
|
|
170
174
|
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
171
175
|
*/
|
|
172
176
|
export declare function listReachableDevices(token: string): Promise<DeviceList>;
|
|
177
|
+
export declare function buildDeviceCandidateUrls(device: RemoteDevice): string[];
|
|
178
|
+
export declare function probeDeviceReachability(device: RemoteDevice, timeoutMs?: number): Promise<DeviceReachability>;
|
|
173
179
|
export declare function mintGuestSdkToken(token: string, hostUserId: string, targetDeviceId: string): Promise<{
|
|
174
180
|
token: string;
|
|
175
181
|
expiresAt: number;
|
package/dist/auth.js
CHANGED
|
@@ -37,6 +37,8 @@ exports.signInWithOAuth = signInWithOAuth;
|
|
|
37
37
|
exports.signupWithEmail = signupWithEmail;
|
|
38
38
|
exports.loginWithEmail = loginWithEmail;
|
|
39
39
|
exports.listReachableDevices = listReachableDevices;
|
|
40
|
+
exports.buildDeviceCandidateUrls = buildDeviceCandidateUrls;
|
|
41
|
+
exports.probeDeviceReachability = probeDeviceReachability;
|
|
40
42
|
exports.mintGuestSdkToken = mintGuestSdkToken;
|
|
41
43
|
exports.fetchGuestHosts = fetchGuestHosts;
|
|
42
44
|
exports.findInviteByCode = findInviteByCode;
|
|
@@ -412,6 +414,45 @@ async function listReachableDevices(token) {
|
|
|
412
414
|
return { owned: [], shared: [] };
|
|
413
415
|
}
|
|
414
416
|
}
|
|
417
|
+
function buildDeviceCandidateUrls(device) {
|
|
418
|
+
const port = device.httpPort ?? device.quicPort ?? 18080;
|
|
419
|
+
const hosts = new Set();
|
|
420
|
+
if (device.quicHost)
|
|
421
|
+
hosts.add(device.quicHost);
|
|
422
|
+
for (const ip of device.localIps ?? []) {
|
|
423
|
+
if (ip)
|
|
424
|
+
hosts.add(ip);
|
|
425
|
+
}
|
|
426
|
+
return Array.from(hosts).map((host) => `http://${host}:${port}`);
|
|
427
|
+
}
|
|
428
|
+
async function probeDeviceReachability(device, timeoutMs = 2500) {
|
|
429
|
+
const candidates = buildDeviceCandidateUrls(device);
|
|
430
|
+
if (candidates.length === 0)
|
|
431
|
+
return { reachable: false };
|
|
432
|
+
const probeOne = async (baseUrl) => {
|
|
433
|
+
const controller = new AbortController();
|
|
434
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
435
|
+
try {
|
|
436
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
437
|
+
method: 'GET',
|
|
438
|
+
signal: controller.signal,
|
|
439
|
+
});
|
|
440
|
+
if (!response.ok)
|
|
441
|
+
throw new Error(`health ${response.status}`);
|
|
442
|
+
return baseUrl;
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const settled = await Promise.allSettled(candidates.map((url) => probeOne(url)));
|
|
449
|
+
for (const result of settled) {
|
|
450
|
+
if (result.status === 'fulfilled') {
|
|
451
|
+
return { reachable: true, url: result.value };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { reachable: false };
|
|
455
|
+
}
|
|
415
456
|
async function mintGuestSdkToken(token, hostUserId, targetDeviceId) {
|
|
416
457
|
const res = await fetch(`${convexSiteUrl}/guests/sdk-token`, {
|
|
417
458
|
method: 'POST',
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote browser-style sign-in session for a coding-agent CLI on the
|
|
3
|
+
* connected yaver host. Mirrors runnerBrowserAuthSession on the agent
|
|
4
|
+
* Go side. Progression: starting → awaiting_browser (openUrl + code
|
|
5
|
+
* filled) → completed | failed | cancelled.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunnerBrowserAuthSession {
|
|
8
|
+
id: string;
|
|
9
|
+
runner: string;
|
|
10
|
+
method: string;
|
|
11
|
+
status: 'starting' | 'awaiting_browser' | 'completed' | 'failed' | 'cancelled';
|
|
12
|
+
openUrl?: string;
|
|
13
|
+
code?: string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
authConfigured?: boolean;
|
|
16
|
+
authSource?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
startedAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
completedAt?: number;
|
|
21
|
+
}
|
|
1
22
|
export interface FeedbackConfig {
|
|
2
23
|
/** URL of the Yaver agent (e.g. "http://192.168.1.10:18080"). If omitted, auto-discovery is used. */
|
|
3
24
|
agentUrl?: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Visual feedback SDK for Yaver
|
|
3
|
+
"version": "0.8.6",
|
|
4
|
+
"description": "Visual feedback SDK for Yaver \u2014 bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
package/src/AuthOverlay.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { DeviceEventEmitter, Modal } from 'react-native';
|
|
3
3
|
import { YaverLoginScreen } from './LoginScreen';
|
|
4
4
|
import { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
@@ -26,6 +26,35 @@ export const AuthOverlay: React.FC = () => {
|
|
|
26
26
|
const [pickerVisible, setPickerVisible] = useState(false);
|
|
27
27
|
const [token, setToken] = useState<string | null>(null);
|
|
28
28
|
const [pendingInviteCode, setPendingInviteCode] = useState<string | null>(null);
|
|
29
|
+
const activeOverlayRef = useRef<'none' | 'login' | 'guest' | 'picker'>('none');
|
|
30
|
+
|
|
31
|
+
const openLogin = useCallback(() => {
|
|
32
|
+
activeOverlayRef.current = 'login';
|
|
33
|
+
setGuestVisible(false);
|
|
34
|
+
setPickerVisible(false);
|
|
35
|
+
setLoginVisible(true);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const openGuest = useCallback(() => {
|
|
39
|
+
activeOverlayRef.current = 'guest';
|
|
40
|
+
setLoginVisible(false);
|
|
41
|
+
setPickerVisible(false);
|
|
42
|
+
setGuestVisible(true);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const openPicker = useCallback(() => {
|
|
46
|
+
activeOverlayRef.current = 'picker';
|
|
47
|
+
setLoginVisible(false);
|
|
48
|
+
setGuestVisible(false);
|
|
49
|
+
setPickerVisible(true);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const closeAll = useCallback(() => {
|
|
53
|
+
activeOverlayRef.current = 'none';
|
|
54
|
+
setLoginVisible(false);
|
|
55
|
+
setGuestVisible(false);
|
|
56
|
+
setPickerVisible(false);
|
|
57
|
+
}, []);
|
|
29
58
|
|
|
30
59
|
useEffect(() => {
|
|
31
60
|
let mounted = true;
|
|
@@ -36,14 +65,18 @@ export const AuthOverlay: React.FC = () => {
|
|
|
36
65
|
|
|
37
66
|
const loginSub = DeviceEventEmitter.addListener(
|
|
38
67
|
'yaverFeedback:startLogin',
|
|
39
|
-
() =>
|
|
68
|
+
() => {
|
|
69
|
+
if (activeOverlayRef.current !== 'none') return;
|
|
70
|
+
openLogin();
|
|
71
|
+
},
|
|
40
72
|
);
|
|
41
73
|
const pickerSub = DeviceEventEmitter.addListener(
|
|
42
74
|
'yaverFeedback:startMachinePicker',
|
|
43
75
|
async () => {
|
|
76
|
+
if (activeOverlayRef.current !== 'none') return;
|
|
44
77
|
const cached = await getToken();
|
|
45
78
|
if (cached) setToken(cached);
|
|
46
|
-
if (cached)
|
|
79
|
+
if (cached) openPicker();
|
|
47
80
|
},
|
|
48
81
|
);
|
|
49
82
|
return () => {
|
|
@@ -51,24 +84,23 @@ export const AuthOverlay: React.FC = () => {
|
|
|
51
84
|
loginSub.remove();
|
|
52
85
|
pickerSub.remove();
|
|
53
86
|
};
|
|
54
|
-
}, []);
|
|
87
|
+
}, [openLogin, openPicker]);
|
|
55
88
|
|
|
56
89
|
const continueAfterAuth = async (newToken: string, inviteCode?: string) => {
|
|
57
90
|
setToken(newToken);
|
|
58
91
|
await YaverFeedback.setAuthToken(newToken);
|
|
59
92
|
const devices = await listReachableDevices(newToken).catch(() => ({ owned: [], shared: [] }));
|
|
60
|
-
setLoginVisible(false);
|
|
61
93
|
const cleanedInviteCode = (inviteCode ?? '').trim().toUpperCase();
|
|
62
94
|
if (cleanedInviteCode) {
|
|
63
95
|
setPendingInviteCode(cleanedInviteCode);
|
|
64
|
-
|
|
96
|
+
openGuest();
|
|
65
97
|
return;
|
|
66
98
|
}
|
|
67
99
|
if (devices.owned.length === 0 && devices.shared.length === 0) {
|
|
68
|
-
|
|
100
|
+
openGuest();
|
|
69
101
|
return;
|
|
70
102
|
}
|
|
71
|
-
|
|
103
|
+
openPicker();
|
|
72
104
|
};
|
|
73
105
|
|
|
74
106
|
const handleLoggedIn = async (newToken: string, opts?: { inviteCode?: string }) => {
|
|
@@ -77,8 +109,7 @@ export const AuthOverlay: React.FC = () => {
|
|
|
77
109
|
|
|
78
110
|
const handleDevicePicked = async (device: RemoteDevice) => {
|
|
79
111
|
await YaverFeedback.setPreferredDevice(device.deviceId);
|
|
80
|
-
|
|
81
|
-
setGuestVisible(false);
|
|
112
|
+
closeAll();
|
|
82
113
|
// Continue straight into the feedback flow the user originally triggered.
|
|
83
114
|
DeviceEventEmitter.emit('yaverFeedback:startReport');
|
|
84
115
|
};
|
|
@@ -89,11 +120,11 @@ export const AuthOverlay: React.FC = () => {
|
|
|
89
120
|
visible={loginVisible}
|
|
90
121
|
animationType="slide"
|
|
91
122
|
presentationStyle="fullScreen"
|
|
92
|
-
onRequestClose={
|
|
123
|
+
onRequestClose={closeAll}
|
|
93
124
|
>
|
|
94
125
|
<YaverLoginScreen
|
|
95
126
|
onLoggedIn={handleLoggedIn}
|
|
96
|
-
onCancel={
|
|
127
|
+
onCancel={closeAll}
|
|
97
128
|
initialInviteCode={pendingInviteCode ?? YaverFeedback.getConfig()?.guestInviteCode}
|
|
98
129
|
/>
|
|
99
130
|
</Modal>
|
|
@@ -102,14 +133,14 @@ export const AuthOverlay: React.FC = () => {
|
|
|
102
133
|
visible={pickerVisible && !!token}
|
|
103
134
|
animationType="slide"
|
|
104
135
|
presentationStyle="fullScreen"
|
|
105
|
-
onRequestClose={
|
|
136
|
+
onRequestClose={closeAll}
|
|
106
137
|
>
|
|
107
138
|
{token && (
|
|
108
139
|
<YaverMachinePickerScreen
|
|
109
140
|
token={token}
|
|
110
141
|
currentDeviceId={YaverFeedback.getConfig()?.preferredDeviceId}
|
|
111
142
|
onPick={handleDevicePicked}
|
|
112
|
-
onCancel={
|
|
143
|
+
onCancel={closeAll}
|
|
113
144
|
/>
|
|
114
145
|
)}
|
|
115
146
|
</Modal>
|
|
@@ -118,21 +149,19 @@ export const AuthOverlay: React.FC = () => {
|
|
|
118
149
|
visible={guestVisible && !!token}
|
|
119
150
|
animationType="slide"
|
|
120
151
|
presentationStyle="fullScreen"
|
|
121
|
-
onRequestClose={
|
|
152
|
+
onRequestClose={closeAll}
|
|
122
153
|
>
|
|
123
154
|
{token && (
|
|
124
155
|
<YaverGuestOnboardingScreen
|
|
125
156
|
token={token}
|
|
126
157
|
initialInviteCode={pendingInviteCode ?? YaverFeedback.getConfig()?.guestInviteCode}
|
|
127
158
|
onContinue={() => {
|
|
128
|
-
setGuestVisible(false);
|
|
129
159
|
setPendingInviteCode(null);
|
|
130
|
-
|
|
160
|
+
openPicker();
|
|
131
161
|
}}
|
|
132
162
|
onCancel={() => {
|
|
133
|
-
setGuestVisible(false);
|
|
134
163
|
setPendingInviteCode(null);
|
|
135
|
-
|
|
164
|
+
openPicker();
|
|
136
165
|
}}
|
|
137
166
|
/>
|
|
138
167
|
)}
|