yaver-feedback-react-native 0.8.1 → 0.8.3
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 +2 -2
- package/dist/AuthOverlay.js +33 -3
- package/dist/Discovery.js +2 -0
- package/dist/FeedbackModal.js +438 -215
- package/dist/GuestOnboardingScreen.d.ts +8 -0
- package/dist/GuestOnboardingScreen.js +282 -0
- package/dist/LoginScreen.d.ts +5 -1
- package/dist/LoginScreen.js +5 -2
- package/dist/MachinePickerScreen.js +1 -1
- package/dist/P2PClient.d.ts +22 -3
- package/dist/P2PClient.js +45 -2
- package/dist/QuickActionIcon.js +16 -4
- package/dist/YaverFeedback.d.ts +7 -0
- package/dist/YaverFeedback.js +68 -4
- package/dist/__tests__/P2PClient.test.js +30 -0
- package/dist/__tests__/YaverFeedback.test.js +39 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.js +75 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +10 -5
- package/dist/preferences.d.ts +11 -0
- package/dist/preferences.js +82 -0
- package/dist/types.d.ts +7 -0
- package/package.json +6 -4
- package/src/AuthOverlay.tsx +47 -2
- package/src/Discovery.ts +2 -0
- package/src/FeedbackModal.tsx +507 -258
- package/src/GuestOnboardingScreen.tsx +307 -0
- package/src/LoginScreen.tsx +21 -2
- package/src/MachinePickerScreen.tsx +3 -1
- package/src/P2PClient.ts +69 -3
- package/src/QuickActionIcon.tsx +25 -5
- package/src/YaverFeedback.ts +79 -4
- package/src/__tests__/P2PClient.test.ts +40 -0
- package/src/__tests__/YaverFeedback.test.ts +42 -0
- package/src/auth.ts +137 -0
- package/src/index.ts +14 -4
- package/src/preferences.ts +96 -0
- package/src/types.ts +7 -0
package/src/YaverFeedback.ts
CHANGED
|
@@ -9,13 +9,18 @@ import {
|
|
|
9
9
|
setStrictNativeAuth,
|
|
10
10
|
getToken,
|
|
11
11
|
getSelectedDeviceId,
|
|
12
|
+
listReachableDevices,
|
|
13
|
+
mintGuestSdkToken,
|
|
12
14
|
clearToken,
|
|
13
15
|
clearSelectedDeviceId,
|
|
14
16
|
DEFAULT_CONVEX_SITE_URL,
|
|
15
17
|
} from './auth';
|
|
16
18
|
import {
|
|
17
19
|
getQuickIconDisabled,
|
|
20
|
+
getQuickIconColorPreset,
|
|
18
21
|
setQuickIconDisabled,
|
|
22
|
+
setQuickIconColorPreset,
|
|
23
|
+
QuickIconColorPreset,
|
|
19
24
|
} from './preferences';
|
|
20
25
|
|
|
21
26
|
// Is this JS runtime the Yaver mobile app's super-host bridge? The
|
|
@@ -45,6 +50,7 @@ let config: FeedbackConfig | null = null;
|
|
|
45
50
|
let enabled = false;
|
|
46
51
|
let p2pClient: P2PClient | null = null;
|
|
47
52
|
let shakeDetector: ShakeDetector | null = null;
|
|
53
|
+
let p2pAuthToken: string | null = null;
|
|
48
54
|
|
|
49
55
|
/** Ring buffer of captured errors. */
|
|
50
56
|
let errorBuffer: CapturedError[] = [];
|
|
@@ -73,6 +79,44 @@ const flagCache: Map<string, { value: unknown; at: number }> = new Map();
|
|
|
73
79
|
* Call `YaverFeedback.init()` once at app startup.
|
|
74
80
|
*/
|
|
75
81
|
export class YaverFeedback {
|
|
82
|
+
private static async resolveP2PAuthToken(): Promise<string | null> {
|
|
83
|
+
if (!config?.authToken) return null;
|
|
84
|
+
if (!config.preferredDeviceId) return config.authToken;
|
|
85
|
+
const devices = await listReachableDevices(config.authToken);
|
|
86
|
+
const all = [...devices.owned, ...devices.shared];
|
|
87
|
+
const selected = all.find((device) => device.deviceId === config?.preferredDeviceId);
|
|
88
|
+
if (!selected || !selected.isGuest || selected.accessScope !== 'shared-scoped') {
|
|
89
|
+
return config.authToken;
|
|
90
|
+
}
|
|
91
|
+
if (!selected.hostUserId) {
|
|
92
|
+
return config.authToken;
|
|
93
|
+
}
|
|
94
|
+
const delegated = await mintGuestSdkToken(
|
|
95
|
+
config.authToken,
|
|
96
|
+
selected.hostUserId,
|
|
97
|
+
selected.deviceId,
|
|
98
|
+
);
|
|
99
|
+
return delegated.token;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static async rebuildP2PClient(agentUrl?: string): Promise<void> {
|
|
103
|
+
if (!config) return;
|
|
104
|
+
const effectiveUrl = agentUrl ?? config.agentUrl;
|
|
105
|
+
if (!effectiveUrl) {
|
|
106
|
+
p2pClient = null;
|
|
107
|
+
p2pAuthToken = null;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const token = await YaverFeedback.resolveP2PAuthToken();
|
|
111
|
+
if (!token) {
|
|
112
|
+
p2pClient = null;
|
|
113
|
+
p2pAuthToken = null;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
p2pAuthToken = token;
|
|
117
|
+
p2pClient = new P2PClient(effectiveUrl, token);
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
/**
|
|
77
121
|
* Initialize the feedback SDK with the given configuration.
|
|
78
122
|
* Typically called in your app's root component or entry file.
|
|
@@ -126,7 +170,11 @@ export class YaverFeedback {
|
|
|
126
170
|
|
|
127
171
|
// Create P2P client if we have a URL
|
|
128
172
|
if (config.agentUrl) {
|
|
173
|
+
p2pAuthToken = config.authToken ?? null;
|
|
129
174
|
p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '');
|
|
175
|
+
if (config.authToken) {
|
|
176
|
+
void YaverFeedback.rebuildP2PClient(config.agentUrl);
|
|
177
|
+
}
|
|
130
178
|
} else {
|
|
131
179
|
p2pClient = null;
|
|
132
180
|
// Auto-discover agent in the background when convexUrl or LAN is available
|
|
@@ -247,7 +295,7 @@ export class YaverFeedback {
|
|
|
247
295
|
});
|
|
248
296
|
if (result && config) {
|
|
249
297
|
config.agentUrl = result.url;
|
|
250
|
-
|
|
298
|
+
await YaverFeedback.rebuildP2PClient(result.url);
|
|
251
299
|
}
|
|
252
300
|
} catch {
|
|
253
301
|
// Discovery failed — FloatingButton will show disconnected, user can retry
|
|
@@ -274,7 +322,7 @@ export class YaverFeedback {
|
|
|
274
322
|
});
|
|
275
323
|
if (!result) return false;
|
|
276
324
|
config.agentUrl = result.url;
|
|
277
|
-
|
|
325
|
+
await YaverFeedback.rebuildP2PClient(result.url);
|
|
278
326
|
return true;
|
|
279
327
|
} catch {
|
|
280
328
|
return false;
|
|
@@ -318,7 +366,7 @@ export class YaverFeedback {
|
|
|
318
366
|
if (!config) return;
|
|
319
367
|
config.authToken = token;
|
|
320
368
|
if (config.agentUrl) {
|
|
321
|
-
|
|
369
|
+
await YaverFeedback.rebuildP2PClient(config.agentUrl);
|
|
322
370
|
} else {
|
|
323
371
|
await YaverFeedback.discoverAgent();
|
|
324
372
|
}
|
|
@@ -359,9 +407,19 @@ export class YaverFeedback {
|
|
|
359
407
|
config.preferredDeviceId = deviceId;
|
|
360
408
|
config.agentUrl = undefined;
|
|
361
409
|
p2pClient = null;
|
|
410
|
+
p2pAuthToken = null;
|
|
362
411
|
await YaverFeedback.discoverAgent();
|
|
363
412
|
}
|
|
364
413
|
|
|
414
|
+
/** Resolve the currently selected remote machine from the authenticated device list. */
|
|
415
|
+
static async getSelectedRemoteDevice() {
|
|
416
|
+
if (!config?.authToken || !config.preferredDeviceId) return null;
|
|
417
|
+
const preferredDeviceId = config.preferredDeviceId;
|
|
418
|
+
const devices = await listReachableDevices(config.authToken);
|
|
419
|
+
const all = [...devices.owned, ...devices.shared];
|
|
420
|
+
return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
|
|
421
|
+
}
|
|
422
|
+
|
|
365
423
|
/**
|
|
366
424
|
* Sign out: clear cached token + device, tear down the P2P client. The
|
|
367
425
|
* SDK stays enabled; the next feedback trigger will re-prompt for login.
|
|
@@ -375,6 +433,7 @@ export class YaverFeedback {
|
|
|
375
433
|
config.agentUrl = undefined;
|
|
376
434
|
}
|
|
377
435
|
p2pClient = null;
|
|
436
|
+
p2pAuthToken = null;
|
|
378
437
|
}
|
|
379
438
|
|
|
380
439
|
/**
|
|
@@ -414,7 +473,7 @@ export class YaverFeedback {
|
|
|
414
473
|
});
|
|
415
474
|
if (result) {
|
|
416
475
|
config.agentUrl = result.url;
|
|
417
|
-
|
|
476
|
+
await YaverFeedback.rebuildP2PClient(result.url);
|
|
418
477
|
} else if (config.autoLogin !== false && !config.preferredDeviceId) {
|
|
419
478
|
// No agent discovered and no device picked yet — prompt the user
|
|
420
479
|
// to pick one of their machines (handles the non-LAN case where
|
|
@@ -859,6 +918,22 @@ export class YaverFeedback {
|
|
|
859
918
|
return getQuickIconDisabled();
|
|
860
919
|
}
|
|
861
920
|
|
|
921
|
+
static async setQuickIconColorPreset(
|
|
922
|
+
preset: QuickIconColorPreset | null,
|
|
923
|
+
): Promise<void> {
|
|
924
|
+
await setQuickIconColorPreset(preset);
|
|
925
|
+
try {
|
|
926
|
+
const { DeviceEventEmitter } = require('react-native');
|
|
927
|
+
DeviceEventEmitter.emit('yaverFeedback:quickIconColorChange', { preset });
|
|
928
|
+
} catch {
|
|
929
|
+
// emitter unavailable — preference is still persisted
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
static async getQuickIconColorPreset(): Promise<QuickIconColorPreset | null> {
|
|
934
|
+
return getQuickIconColorPreset();
|
|
935
|
+
}
|
|
936
|
+
|
|
862
937
|
/** Clear the persisted "user hid the icon" flag. */
|
|
863
938
|
static async resetQuickIconPreference(): Promise<void> {
|
|
864
939
|
await YaverFeedback.setQuickIconVisible(true);
|
|
@@ -215,4 +215,44 @@ describe('P2PClient', () => {
|
|
|
215
215
|
expect(result).toEqual(builds);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
describe('reloadApp()', () => {
|
|
220
|
+
it('returns an acknowledgement for dev reloads', async () => {
|
|
221
|
+
mockFetch.mockResolvedValue({
|
|
222
|
+
ok: true,
|
|
223
|
+
json: () => Promise.resolve({ ok: true, changeClass: 'js_only' }),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const client = new P2PClient('http://localhost:18080', 'tok');
|
|
227
|
+
const result = await client.reloadApp('dev');
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual(
|
|
230
|
+
expect.objectContaining({
|
|
231
|
+
ok: true,
|
|
232
|
+
mode: 'dev',
|
|
233
|
+
acknowledged: true,
|
|
234
|
+
message: 'Hot reload request accepted.',
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns an acknowledgement for bundle reloads', async () => {
|
|
240
|
+
mockFetch.mockResolvedValue({
|
|
241
|
+
ok: true,
|
|
242
|
+
json: () => Promise.resolve({ ok: true }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const client = new P2PClient('http://localhost:18080', 'tok');
|
|
246
|
+
const result = await client.reloadApp('bundle');
|
|
247
|
+
|
|
248
|
+
expect(result).toEqual(
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
ok: true,
|
|
251
|
+
mode: 'bundle',
|
|
252
|
+
acknowledged: true,
|
|
253
|
+
message: 'Reload request acknowledged. Agent is rebuilding the bundle.',
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
218
258
|
});
|
|
@@ -17,6 +17,34 @@ jest.mock('../Discovery', () => ({
|
|
|
17
17
|
},
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
jest.mock('../auth', () => ({
|
|
21
|
+
configureAuthEndpoints: jest.fn(),
|
|
22
|
+
setStrictNativeAuth: jest.fn(),
|
|
23
|
+
getToken: jest.fn(async () => null),
|
|
24
|
+
getSelectedDeviceId: jest.fn(async () => null),
|
|
25
|
+
clearToken: jest.fn(async () => {}),
|
|
26
|
+
clearSelectedDeviceId: jest.fn(async () => {}),
|
|
27
|
+
listReachableDevices: jest.fn(async () => ({
|
|
28
|
+
owned: [
|
|
29
|
+
{
|
|
30
|
+
deviceId: 'device-1',
|
|
31
|
+
name: 'Dev Mac',
|
|
32
|
+
platform: 'darwin',
|
|
33
|
+
isOnline: true,
|
|
34
|
+
needsAuth: false,
|
|
35
|
+
runnerDown: false,
|
|
36
|
+
lastHeartbeat: Date.now(),
|
|
37
|
+
isGuest: false,
|
|
38
|
+
accessScope: 'owner',
|
|
39
|
+
quicHost: '127.0.0.1',
|
|
40
|
+
quicPort: 18080,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
shared: [],
|
|
44
|
+
})),
|
|
45
|
+
DEFAULT_CONVEX_SITE_URL: 'https://example.convex.site',
|
|
46
|
+
}));
|
|
47
|
+
|
|
20
48
|
// Reset module-level state between tests by re-requiring
|
|
21
49
|
beforeEach(() => {
|
|
22
50
|
// YaverFeedback uses module-level variables (config, enabled, p2pClient).
|
|
@@ -126,6 +154,20 @@ describe('YaverFeedback', () => {
|
|
|
126
154
|
});
|
|
127
155
|
});
|
|
128
156
|
|
|
157
|
+
describe('getSelectedRemoteDevice()', () => {
|
|
158
|
+
it('returns the selected device from the reachable device list', async () => {
|
|
159
|
+
YaverFeedback.init({
|
|
160
|
+
authToken: 'tok',
|
|
161
|
+
preferredDeviceId: 'device-1',
|
|
162
|
+
enabled: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const device = await YaverFeedback.getSelectedRemoteDevice();
|
|
166
|
+
expect(device?.deviceId).toBe('device-1');
|
|
167
|
+
expect(device?.name).toBe('Dev Mac');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
129
171
|
describe('startReport()', () => {
|
|
130
172
|
it('does nothing when not enabled', async () => {
|
|
131
173
|
YaverFeedback.init({ authToken: 'tok', enabled: false });
|
package/src/auth.ts
CHANGED
|
@@ -410,8 +410,10 @@ export interface RemoteDevice {
|
|
|
410
410
|
runnerDown: boolean;
|
|
411
411
|
lastHeartbeat: number;
|
|
412
412
|
isGuest: boolean;
|
|
413
|
+
hostUserId?: string;
|
|
413
414
|
hostName?: string;
|
|
414
415
|
hostEmail?: string;
|
|
416
|
+
hostUserIdString?: string;
|
|
415
417
|
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
416
418
|
quicHost: string;
|
|
417
419
|
quicPort: number;
|
|
@@ -433,6 +435,49 @@ export interface DeviceList {
|
|
|
433
435
|
shared: RemoteDevice[];
|
|
434
436
|
}
|
|
435
437
|
|
|
438
|
+
export interface GuestInvitation {
|
|
439
|
+
hostUserId: string;
|
|
440
|
+
hostName: string;
|
|
441
|
+
hostEmail: string;
|
|
442
|
+
hostUserIdString?: string;
|
|
443
|
+
createdAt: number;
|
|
444
|
+
expiresAt: number;
|
|
445
|
+
inviteCode?: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface ActiveGuestHost {
|
|
449
|
+
hostUserId: string;
|
|
450
|
+
hostName: string;
|
|
451
|
+
hostEmail: string;
|
|
452
|
+
grantedAt: number;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export interface GuestHostsResponse {
|
|
456
|
+
pending: GuestInvitation[];
|
|
457
|
+
active: ActiveGuestHost[];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface InvitationHostDevice {
|
|
461
|
+
deviceId: string;
|
|
462
|
+
name: string;
|
|
463
|
+
platform: string;
|
|
464
|
+
lastHeartbeat?: number;
|
|
465
|
+
proposed: boolean;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface InvitationPreview {
|
|
469
|
+
inviteCode: string;
|
|
470
|
+
hostUserId: string;
|
|
471
|
+
hostName: string;
|
|
472
|
+
hostEmail: string;
|
|
473
|
+
hostUserIdString?: string;
|
|
474
|
+
proposedDeviceIds?: string[];
|
|
475
|
+
hostDevices: InvitationHostDevice[];
|
|
476
|
+
invitedByUserId?: boolean;
|
|
477
|
+
expiresAt: number;
|
|
478
|
+
createdAt: number;
|
|
479
|
+
}
|
|
480
|
+
|
|
436
481
|
/**
|
|
437
482
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
438
483
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
@@ -463,8 +508,10 @@ export async function listReachableDevices(
|
|
|
463
508
|
runnerDown: !!d.runnerDown,
|
|
464
509
|
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
465
510
|
isGuest: !!d.isGuest,
|
|
511
|
+
hostUserId: d.hostUserId,
|
|
466
512
|
hostName: d.hostName,
|
|
467
513
|
hostEmail: d.hostEmail,
|
|
514
|
+
hostUserIdString: d.hostUserIdString,
|
|
468
515
|
accessScope: d.accessScope ?? 'owner',
|
|
469
516
|
quicHost: d.quicHost ?? d.host ?? '',
|
|
470
517
|
quicPort: d.quicPort ?? 0,
|
|
@@ -488,3 +535,93 @@ export async function listReachableDevices(
|
|
|
488
535
|
return { owned: [], shared: [] };
|
|
489
536
|
}
|
|
490
537
|
}
|
|
538
|
+
|
|
539
|
+
export async function mintGuestSdkToken(
|
|
540
|
+
token: string,
|
|
541
|
+
hostUserId: string,
|
|
542
|
+
targetDeviceId: string,
|
|
543
|
+
): Promise<{ token: string; expiresAt: number; allowedProjects?: string[] }> {
|
|
544
|
+
const res = await fetch(`${convexSiteUrl}/guests/sdk-token`, {
|
|
545
|
+
method: 'POST',
|
|
546
|
+
headers: {
|
|
547
|
+
Authorization: `Bearer ${token}`,
|
|
548
|
+
'Content-Type': 'application/json',
|
|
549
|
+
},
|
|
550
|
+
body: JSON.stringify({ hostUserId, targetDeviceId }),
|
|
551
|
+
});
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const data = await res.json().catch(() => ({}));
|
|
554
|
+
throw new Error(data.error || 'Failed to mint delegated SDK token');
|
|
555
|
+
}
|
|
556
|
+
return res.json();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function fetchGuestHosts(token: string): Promise<GuestHostsResponse> {
|
|
560
|
+
const res = await fetch(`${convexSiteUrl}/guests/hosts`, {
|
|
561
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
562
|
+
});
|
|
563
|
+
if (!res.ok) {
|
|
564
|
+
const data = await res.json().catch(() => ({}));
|
|
565
|
+
throw new Error(data.error || 'Failed to fetch guest hosts');
|
|
566
|
+
}
|
|
567
|
+
return res.json();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function findInviteByCode(
|
|
571
|
+
token: string,
|
|
572
|
+
code: string,
|
|
573
|
+
): Promise<InvitationPreview | null> {
|
|
574
|
+
const cleaned = code.toUpperCase().trim();
|
|
575
|
+
const res = await fetch(
|
|
576
|
+
`${convexSiteUrl}/guests/find-by-code?code=${encodeURIComponent(cleaned)}`,
|
|
577
|
+
{ headers: { Authorization: `Bearer ${token}` } },
|
|
578
|
+
);
|
|
579
|
+
if (res.status === 404) return null;
|
|
580
|
+
if (!res.ok) {
|
|
581
|
+
const data = await res.json().catch(() => ({}));
|
|
582
|
+
throw new Error(data.error || 'Failed to load invite');
|
|
583
|
+
}
|
|
584
|
+
return res.json();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function acceptGuestByCode(
|
|
588
|
+
token: string,
|
|
589
|
+
code: string,
|
|
590
|
+
approvedDeviceIds?: string[],
|
|
591
|
+
): Promise<{ hostName: string; hostEmail: string }> {
|
|
592
|
+
const res = await fetch(`${convexSiteUrl}/guests/accept-code`, {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: {
|
|
595
|
+
Authorization: `Bearer ${token}`,
|
|
596
|
+
'Content-Type': 'application/json',
|
|
597
|
+
},
|
|
598
|
+
body: JSON.stringify({
|
|
599
|
+
code: code.toUpperCase().trim(),
|
|
600
|
+
approvedDeviceIds,
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
if (!res.ok) {
|
|
604
|
+
const data = await res.json().catch(() => ({}));
|
|
605
|
+
throw new Error(data.error || 'Invalid invite code');
|
|
606
|
+
}
|
|
607
|
+
return res.json();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function acceptGuestInvitation(
|
|
611
|
+
token: string,
|
|
612
|
+
hostUserId: string,
|
|
613
|
+
approvedDeviceIds?: string[],
|
|
614
|
+
): Promise<void> {
|
|
615
|
+
const res = await fetch(`${convexSiteUrl}/guests/accept`, {
|
|
616
|
+
method: 'POST',
|
|
617
|
+
headers: {
|
|
618
|
+
Authorization: `Bearer ${token}`,
|
|
619
|
+
'Content-Type': 'application/json',
|
|
620
|
+
},
|
|
621
|
+
body: JSON.stringify({ hostUserId, approvedDeviceIds }),
|
|
622
|
+
});
|
|
623
|
+
if (!res.ok) {
|
|
624
|
+
const data = await res.json().catch(() => ({}));
|
|
625
|
+
throw new Error(data.error || 'Failed to accept invitation');
|
|
626
|
+
}
|
|
627
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* yaver-feedback-react-native — Visual feedback SDK for Yaver.
|
|
3
3
|
*
|
|
4
|
-
* Shake-to-report surface with
|
|
4
|
+
* Shake-to-report surface with three launch actions:
|
|
5
5
|
* 1. Hot Reload — instant JS reload
|
|
6
6
|
* 2. Vibing — open a vibing session on the agent
|
|
7
|
-
* 3. Screenshot
|
|
8
|
-
*
|
|
9
|
-
* 4. Screen Recording — start, then stop + upload
|
|
7
|
+
* 3. Screenshot & Fix — capture the current screen and trigger
|
|
8
|
+
* the fix loop
|
|
10
9
|
*
|
|
11
10
|
* The small quick-access icon stays hidden until the first shake by
|
|
12
11
|
* default on mobile, then remains available unless the user hides it.
|
|
@@ -41,6 +40,8 @@ export { YaverLoginScreen } from './LoginScreen';
|
|
|
41
40
|
export type { YaverLoginScreenProps } from './LoginScreen';
|
|
42
41
|
export { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
43
42
|
export type { YaverMachinePickerProps } from './MachinePickerScreen';
|
|
43
|
+
export { YaverGuestOnboardingScreen } from './GuestOnboardingScreen';
|
|
44
|
+
export type { YaverGuestOnboardingScreenProps } from './GuestOnboardingScreen';
|
|
44
45
|
export { PairDeviceModal } from './PairDeviceModal';
|
|
45
46
|
export type { PairDeviceModalProps } from './PairDeviceModal';
|
|
46
47
|
export { AuthOverlay } from './AuthOverlay';
|
|
@@ -73,6 +74,10 @@ export {
|
|
|
73
74
|
signupWithEmail,
|
|
74
75
|
loginWithEmail,
|
|
75
76
|
listReachableDevices,
|
|
77
|
+
fetchGuestHosts,
|
|
78
|
+
findInviteByCode,
|
|
79
|
+
acceptGuestByCode,
|
|
80
|
+
acceptGuestInvitation,
|
|
76
81
|
DEFAULT_CONVEX_SITE_URL,
|
|
77
82
|
DEFAULT_WEB_BASE_URL,
|
|
78
83
|
DEFAULT_OAUTH_REDIRECT,
|
|
@@ -82,6 +87,11 @@ export type {
|
|
|
82
87
|
User,
|
|
83
88
|
RemoteDevice,
|
|
84
89
|
DeviceList,
|
|
90
|
+
GuestInvitation,
|
|
91
|
+
ActiveGuestHost,
|
|
92
|
+
GuestHostsResponse,
|
|
93
|
+
InvitationHostDevice,
|
|
94
|
+
InvitationPreview,
|
|
85
95
|
} from './auth';
|
|
86
96
|
export {
|
|
87
97
|
captureScreenshot,
|
package/src/preferences.ts
CHANGED
|
@@ -25,6 +25,69 @@ try {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const QUICK_ICON_DISABLED_KEY = 'yaver_feedback_quickicon_disabled';
|
|
28
|
+
const QUICK_ICON_COLOR_KEY = 'yaver_feedback_quickicon_color';
|
|
29
|
+
|
|
30
|
+
export type QuickIconColorPreset =
|
|
31
|
+
| 'orange'
|
|
32
|
+
| 'lime'
|
|
33
|
+
| 'cyan'
|
|
34
|
+
| 'pink'
|
|
35
|
+
| 'yellow'
|
|
36
|
+
| 'slate';
|
|
37
|
+
|
|
38
|
+
export const QUICK_ICON_COLOR_PRESETS: Record<
|
|
39
|
+
QuickIconColorPreset,
|
|
40
|
+
{
|
|
41
|
+
label: string;
|
|
42
|
+
backgroundColor: string;
|
|
43
|
+
foregroundColor: string;
|
|
44
|
+
borderColor: string;
|
|
45
|
+
shadowColor: string;
|
|
46
|
+
}
|
|
47
|
+
> = {
|
|
48
|
+
orange: {
|
|
49
|
+
label: 'Orange',
|
|
50
|
+
backgroundColor: '#ff6b2c',
|
|
51
|
+
foregroundColor: '#111111',
|
|
52
|
+
borderColor: 'rgba(255,255,255,0.92)',
|
|
53
|
+
shadowColor: '#000000',
|
|
54
|
+
},
|
|
55
|
+
lime: {
|
|
56
|
+
label: 'Lime',
|
|
57
|
+
backgroundColor: '#a3e635',
|
|
58
|
+
foregroundColor: '#111111',
|
|
59
|
+
borderColor: 'rgba(255,255,255,0.85)',
|
|
60
|
+
shadowColor: '#365314',
|
|
61
|
+
},
|
|
62
|
+
cyan: {
|
|
63
|
+
label: 'Cyan',
|
|
64
|
+
backgroundColor: '#22d3ee',
|
|
65
|
+
foregroundColor: '#082f49',
|
|
66
|
+
borderColor: 'rgba(255,255,255,0.82)',
|
|
67
|
+
shadowColor: '#083344',
|
|
68
|
+
},
|
|
69
|
+
pink: {
|
|
70
|
+
label: 'Pink',
|
|
71
|
+
backgroundColor: '#fb7185',
|
|
72
|
+
foregroundColor: '#fff1f2',
|
|
73
|
+
borderColor: 'rgba(255,255,255,0.78)',
|
|
74
|
+
shadowColor: '#4c0519',
|
|
75
|
+
},
|
|
76
|
+
yellow: {
|
|
77
|
+
label: 'Yellow',
|
|
78
|
+
backgroundColor: '#facc15',
|
|
79
|
+
foregroundColor: '#1c1917',
|
|
80
|
+
borderColor: 'rgba(255,255,255,0.88)',
|
|
81
|
+
shadowColor: '#713f12',
|
|
82
|
+
},
|
|
83
|
+
slate: {
|
|
84
|
+
label: 'Slate',
|
|
85
|
+
backgroundColor: '#475569',
|
|
86
|
+
foregroundColor: '#f8fafc',
|
|
87
|
+
borderColor: 'rgba(255,255,255,0.68)',
|
|
88
|
+
shadowColor: '#020617',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
28
91
|
|
|
29
92
|
/** True if the user has long-pressed the icon and chosen "Hide". */
|
|
30
93
|
export async function getQuickIconDisabled(): Promise<boolean> {
|
|
@@ -53,3 +116,36 @@ export async function setQuickIconDisabled(disabled: boolean): Promise<void> {
|
|
|
53
116
|
export async function clearQuickIconDisabled(): Promise<void> {
|
|
54
117
|
await setQuickIconDisabled(false);
|
|
55
118
|
}
|
|
119
|
+
|
|
120
|
+
export async function getQuickIconColorPreset(): Promise<QuickIconColorPreset | null> {
|
|
121
|
+
if (!AsyncStorage) return null;
|
|
122
|
+
try {
|
|
123
|
+
const v = await AsyncStorage.getItem(QUICK_ICON_COLOR_KEY);
|
|
124
|
+
if (!v) return null;
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(QUICK_ICON_COLOR_PRESETS, v)) {
|
|
126
|
+
return v as QuickIconColorPreset;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function setQuickIconColorPreset(
|
|
135
|
+
preset: QuickIconColorPreset | null,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
if (!AsyncStorage) return;
|
|
138
|
+
try {
|
|
139
|
+
if (!preset) {
|
|
140
|
+
await AsyncStorage.removeItem(QUICK_ICON_COLOR_KEY);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await AsyncStorage.setItem(QUICK_ICON_COLOR_KEY, preset);
|
|
144
|
+
} catch {
|
|
145
|
+
// best-effort
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function clearQuickIconColorPreset(): Promise<void> {
|
|
150
|
+
await setQuickIconColorPreset(null);
|
|
151
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -206,6 +206,13 @@ export interface FeedbackConfig {
|
|
|
206
206
|
* Default: false (preserve historical behavior).
|
|
207
207
|
*/
|
|
208
208
|
strictNativeAuth?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Optional host invite code to prefill into the in-SDK guest onboarding
|
|
211
|
+
* flow. Useful when your app receives the code from a deep link, QR flow,
|
|
212
|
+
* or an out-of-band host handoff and you want the user to redeem it
|
|
213
|
+
* without typing.
|
|
214
|
+
*/
|
|
215
|
+
guestInviteCode?: string;
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
export interface FeedbackBundle {
|