yaver-feedback-react-native 0.7.0 → 0.7.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/dist/Discovery.d.ts +40 -27
- package/dist/Discovery.js +164 -70
- package/dist/FeedbackModal.js +44 -7
- package/dist/MachinePickerScreen.js +5 -1
- package/dist/YaverFeedback.d.ts +10 -0
- package/dist/YaverFeedback.js +30 -0
- package/dist/auth.d.ts +15 -1
- package/dist/auth.js +42 -4
- package/dist/deviceDedup.d.ts +44 -0
- package/dist/deviceDedup.js +184 -0
- package/package.json +1 -1
- package/src/Discovery.ts +188 -101
- package/src/FeedbackModal.tsx +43 -7
- package/src/MachinePickerScreen.tsx +6 -1
- package/src/YaverFeedback.ts +27 -0
- package/src/auth.ts +52 -4
- package/src/deviceDedup.ts +184 -0
package/dist/auth.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
|
|
16
16
|
* package; do not import this module from a browser bundle.
|
|
17
17
|
*/
|
|
18
|
-
export declare const DEFAULT_CONVEX_SITE_URL = "https://
|
|
18
|
+
export declare const DEFAULT_CONVEX_SITE_URL = "https://perceptive-minnow-557.eu-west-1.convex.site";
|
|
19
19
|
export declare const DEFAULT_WEB_BASE_URL = "https://yaver.io";
|
|
20
20
|
/** Override the Convex site URL + web base (staging vs prod). */
|
|
21
21
|
export declare function configureAuthEndpoints(opts: {
|
|
@@ -105,7 +105,17 @@ export interface RemoteDevice {
|
|
|
105
105
|
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
106
106
|
quicHost: string;
|
|
107
107
|
quicPort: number;
|
|
108
|
+
/** Agent HTTP port — preferred over quicPort when present. */
|
|
109
|
+
httpPort?: number;
|
|
108
110
|
publicKey?: string;
|
|
111
|
+
/** Hardware identifier — used for dedup across re-pair events. */
|
|
112
|
+
hwid?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Every LAN IP the agent reported in its last heartbeat. Useful on
|
|
115
|
+
* multi-homed hosts — probing all of them in parallel is the same
|
|
116
|
+
* trick the Yaver mobile app uses to "just work" on the same Wi-Fi.
|
|
117
|
+
*/
|
|
118
|
+
localIps?: string[];
|
|
109
119
|
}
|
|
110
120
|
export interface DeviceList {
|
|
111
121
|
owned: RemoteDevice[];
|
|
@@ -114,5 +124,9 @@ export interface DeviceList {
|
|
|
114
124
|
/**
|
|
115
125
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
116
126
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
127
|
+
*
|
|
128
|
+
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
129
|
+
* rows per physical machine after a re-pair or hostname change, and the
|
|
130
|
+
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
117
131
|
*/
|
|
118
132
|
export declare function listReachableDevices(token: string): Promise<DeviceList>;
|
package/dist/auth.js
CHANGED
|
@@ -63,7 +63,12 @@ catch {
|
|
|
63
63
|
const TOKEN_KEY = 'yaver_feedback_auth_token';
|
|
64
64
|
const USER_KEY = 'yaver_feedback_user';
|
|
65
65
|
const DEVICE_KEY = 'yaver_feedback_selected_device';
|
|
66
|
-
|
|
66
|
+
// Source of truth: mobile/src/lib/constants.ts → CONVEX_SITE_URL.
|
|
67
|
+
// The yaver-io Convex deployment was migrated from shocking-echidna-394
|
|
68
|
+
// to perceptive-minnow-557; sessions minted against the old deployment
|
|
69
|
+
// don't validate on agents that point at the new one, producing a 403
|
|
70
|
+
// "invalid token" from the agent's authSDK middleware.
|
|
71
|
+
exports.DEFAULT_CONVEX_SITE_URL = 'https://perceptive-minnow-557.eu-west-1.convex.site';
|
|
67
72
|
exports.DEFAULT_WEB_BASE_URL = 'https://yaver.io';
|
|
68
73
|
let convexSiteUrl = exports.DEFAULT_CONVEX_SITE_URL;
|
|
69
74
|
let webBaseUrl = exports.DEFAULT_WEB_BASE_URL;
|
|
@@ -346,6 +351,10 @@ async function loginWithEmail(email, password) {
|
|
|
346
351
|
/**
|
|
347
352
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
348
353
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
354
|
+
*
|
|
355
|
+
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
356
|
+
* rows per physical machine after a re-pair or hostname change, and the
|
|
357
|
+
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
349
358
|
*/
|
|
350
359
|
async function listReachableDevices(token) {
|
|
351
360
|
try {
|
|
@@ -355,10 +364,39 @@ async function listReachableDevices(token) {
|
|
|
355
364
|
if (!res.ok)
|
|
356
365
|
return { owned: [], shared: [] };
|
|
357
366
|
const data = await res.json();
|
|
358
|
-
const
|
|
367
|
+
const raw = (data.devices ?? []);
|
|
368
|
+
// Normalise Convex field names → SDK's RemoteDevice shape. The
|
|
369
|
+
// backend returns `localIps`, sometimes the mobile-side mapping
|
|
370
|
+
// surfaces `lanIps` — accept either so the field survives.
|
|
371
|
+
const normalised = raw.map((d) => ({
|
|
372
|
+
deviceId: d.deviceId ?? d.id,
|
|
373
|
+
name: d.name ?? '',
|
|
374
|
+
platform: d.platform ?? d.os ?? '',
|
|
375
|
+
isOnline: !!d.isOnline,
|
|
376
|
+
needsAuth: !!d.needsAuth,
|
|
377
|
+
runnerDown: !!d.runnerDown,
|
|
378
|
+
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
379
|
+
isGuest: !!d.isGuest,
|
|
380
|
+
hostName: d.hostName,
|
|
381
|
+
hostEmail: d.hostEmail,
|
|
382
|
+
accessScope: d.accessScope ?? 'owner',
|
|
383
|
+
quicHost: d.quicHost ?? d.host ?? '',
|
|
384
|
+
quicPort: d.quicPort ?? 0,
|
|
385
|
+
httpPort: d.httpPort ?? d.quicPort,
|
|
386
|
+
publicKey: d.publicKey,
|
|
387
|
+
hwid: d.hardwareId ?? d.hwid,
|
|
388
|
+
localIps: Array.isArray(d.localIps)
|
|
389
|
+
? d.localIps
|
|
390
|
+
: Array.isArray(d.lanIps)
|
|
391
|
+
? d.lanIps
|
|
392
|
+
: undefined,
|
|
393
|
+
}));
|
|
394
|
+
// Lazy require so Jest + tree-shakers don't choke on a circular import.
|
|
395
|
+
const { collapseRemoteDevices } = require('./deviceDedup');
|
|
396
|
+
const deduped = collapseRemoteDevices(normalised);
|
|
359
397
|
return {
|
|
360
|
-
owned:
|
|
361
|
-
shared:
|
|
398
|
+
owned: deduped.filter((d) => !d.isGuest),
|
|
399
|
+
shared: deduped.filter((d) => d.isGuest),
|
|
362
400
|
};
|
|
363
401
|
}
|
|
364
402
|
catch {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device deduplication + online-signal merging, ported from the Yaver
|
|
3
|
+
* mobile app's DeviceContext.collapseAliasDevices.
|
|
4
|
+
*
|
|
5
|
+
* Convex stores one row per pair (device, re-install). After a re-pair
|
|
6
|
+
* or hostname change the list can contain 2-3 rows for the same
|
|
7
|
+
* physical machine, with different hwid/publicKey values. The picker
|
|
8
|
+
* then shows duplicates and the user can't tell which one is live.
|
|
9
|
+
*
|
|
10
|
+
* This file collapses rows in three passes:
|
|
11
|
+
* 1. Identity key (hwid → publicKey → "host:os:name" → id → name)
|
|
12
|
+
* 2. Alias key (os + normalized-hostname) — catches re-pairs that
|
|
13
|
+
* mint a new hwid
|
|
14
|
+
* 3. Endpoint key (host:port)
|
|
15
|
+
*
|
|
16
|
+
* When collapsing, `mergeDeviceEntries` prefers: authenticated over
|
|
17
|
+
* needsAuth, online over offline, freshest lastHeartbeat.
|
|
18
|
+
*/
|
|
19
|
+
import type { RemoteDevice } from './auth';
|
|
20
|
+
/**
|
|
21
|
+
* Collapse a Convex device list so each physical machine appears once.
|
|
22
|
+
* Safe on an empty list; idempotent on an already-deduped list.
|
|
23
|
+
*/
|
|
24
|
+
export declare function collapseRemoteDevices(devices: RemoteDevice[]): RemoteDevice[];
|
|
25
|
+
/**
|
|
26
|
+
* Threshold for "online" based on heartbeat age, in milliseconds.
|
|
27
|
+
* Matches the mobile app (HEARTBEAT_STALE_MS = 90 s). The SDK used
|
|
28
|
+
* 60 s, which flashed yellow on single missed beats.
|
|
29
|
+
*/
|
|
30
|
+
export declare const HEARTBEAT_STALE_MS = 90000;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a freshness flag consistent with the mobile app. A device is
|
|
33
|
+
* "fresh" when it was online per Convex AND its heartbeat is within
|
|
34
|
+
* `HEARTBEAT_STALE_MS`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isDeviceFresh(d: RemoteDevice): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Pick the best candidate for an auto-connect attempt. Preference:
|
|
39
|
+
* 1. matches the preferred deviceId when supplied + still fresh
|
|
40
|
+
* 2. fresh (online + recent heartbeat) + has a quicHost
|
|
41
|
+
* 3. online + has a quicHost
|
|
42
|
+
* 4. first with a quicHost
|
|
43
|
+
*/
|
|
44
|
+
export declare function pickTargetDevice(devices: RemoteDevice[], preferredDeviceId?: string): RemoteDevice | null;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Device deduplication + online-signal merging, ported from the Yaver
|
|
4
|
+
* mobile app's DeviceContext.collapseAliasDevices.
|
|
5
|
+
*
|
|
6
|
+
* Convex stores one row per pair (device, re-install). After a re-pair
|
|
7
|
+
* or hostname change the list can contain 2-3 rows for the same
|
|
8
|
+
* physical machine, with different hwid/publicKey values. The picker
|
|
9
|
+
* then shows duplicates and the user can't tell which one is live.
|
|
10
|
+
*
|
|
11
|
+
* This file collapses rows in three passes:
|
|
12
|
+
* 1. Identity key (hwid → publicKey → "host:os:name" → id → name)
|
|
13
|
+
* 2. Alias key (os + normalized-hostname) — catches re-pairs that
|
|
14
|
+
* mint a new hwid
|
|
15
|
+
* 3. Endpoint key (host:port)
|
|
16
|
+
*
|
|
17
|
+
* When collapsing, `mergeDeviceEntries` prefers: authenticated over
|
|
18
|
+
* needsAuth, online over offline, freshest lastHeartbeat.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.HEARTBEAT_STALE_MS = void 0;
|
|
22
|
+
exports.collapseRemoteDevices = collapseRemoteDevices;
|
|
23
|
+
exports.isDeviceFresh = isDeviceFresh;
|
|
24
|
+
exports.pickTargetDevice = pickTargetDevice;
|
|
25
|
+
function normalizedName(name) {
|
|
26
|
+
return String(name || '').trim().toLowerCase().replace(/\.local$/i, '');
|
|
27
|
+
}
|
|
28
|
+
function normalizedHost(host) {
|
|
29
|
+
return String(host || '').trim().toLowerCase().replace(/\.local$/i, '');
|
|
30
|
+
}
|
|
31
|
+
function identityKey(d) {
|
|
32
|
+
if (d.hwid)
|
|
33
|
+
return `hwid:${d.hwid}`;
|
|
34
|
+
if (d.publicKey)
|
|
35
|
+
return `pub:${d.publicKey}`;
|
|
36
|
+
if (d.isGuest) {
|
|
37
|
+
const scope = d.hostEmail || d.hostName || 'guest';
|
|
38
|
+
return `guest:${scope}:${d.deviceId || d.name}`;
|
|
39
|
+
}
|
|
40
|
+
const n = normalizedName(d.name);
|
|
41
|
+
const os = String(d.platform || '').trim().toLowerCase();
|
|
42
|
+
if (n && os)
|
|
43
|
+
return `host:${os}:${n}`;
|
|
44
|
+
if (d.deviceId)
|
|
45
|
+
return `id:${d.deviceId}`;
|
|
46
|
+
return `name:${d.name}`;
|
|
47
|
+
}
|
|
48
|
+
function aliasKey(d) {
|
|
49
|
+
if (d.isGuest)
|
|
50
|
+
return null;
|
|
51
|
+
const n = normalizedName(d.name);
|
|
52
|
+
const os = String(d.platform || '').trim().toLowerCase();
|
|
53
|
+
if (!n || !os)
|
|
54
|
+
return null;
|
|
55
|
+
return `${os}:${n}`;
|
|
56
|
+
}
|
|
57
|
+
function endpointKey(d) {
|
|
58
|
+
if (d.isGuest)
|
|
59
|
+
return null;
|
|
60
|
+
const h = normalizedHost(d.quicHost);
|
|
61
|
+
if (!h)
|
|
62
|
+
return null;
|
|
63
|
+
return `${h}:${d.quicPort || 0}`;
|
|
64
|
+
}
|
|
65
|
+
function mergeEntries(existing, incoming) {
|
|
66
|
+
const incomingWins = (!!existing.needsAuth && !incoming.needsAuth) ||
|
|
67
|
+
(incoming.lastHeartbeat || 0) > (existing.lastHeartbeat || 0) ||
|
|
68
|
+
(!!incoming.isOnline && !existing.isOnline);
|
|
69
|
+
const base = incomingWins ? incoming : existing;
|
|
70
|
+
const other = incomingWins ? existing : incoming;
|
|
71
|
+
return {
|
|
72
|
+
...other,
|
|
73
|
+
...base,
|
|
74
|
+
quicHost: base.quicHost || other.quicHost,
|
|
75
|
+
quicPort: base.quicPort || other.quicPort,
|
|
76
|
+
isOnline: base.isOnline || other.isOnline,
|
|
77
|
+
runnerDown: base.runnerDown && other.runnerDown,
|
|
78
|
+
publicKey: base.publicKey || other.publicKey,
|
|
79
|
+
lastHeartbeat: Math.max(existing.lastHeartbeat || 0, incoming.lastHeartbeat || 0),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// When two rows share the same alias key (hostname + OS) but differ on
|
|
83
|
+
// hwid/publicKey, pick the active one over the stale needs-auth leftover.
|
|
84
|
+
function pickActiveOverStaleNeedsAuth(a, b) {
|
|
85
|
+
const aDead = a.needsAuth && !a.isOnline;
|
|
86
|
+
const bDead = b.needsAuth && !b.isOnline;
|
|
87
|
+
const aLive = !a.needsAuth && a.isOnline;
|
|
88
|
+
const bLive = !b.needsAuth && b.isOnline;
|
|
89
|
+
if (aDead && bLive)
|
|
90
|
+
return b;
|
|
91
|
+
if (bDead && aLive)
|
|
92
|
+
return a;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Collapse a Convex device list so each physical machine appears once.
|
|
97
|
+
* Safe on an empty list; idempotent on an already-deduped list.
|
|
98
|
+
*/
|
|
99
|
+
function collapseRemoteDevices(devices) {
|
|
100
|
+
if (!Array.isArray(devices) || devices.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
const byIdentity = new Map();
|
|
103
|
+
for (const d of devices) {
|
|
104
|
+
const k = identityKey(d);
|
|
105
|
+
const prev = byIdentity.get(k);
|
|
106
|
+
byIdentity.set(k, prev ? mergeEntries(prev, d) : d);
|
|
107
|
+
}
|
|
108
|
+
const byAlias = new Map();
|
|
109
|
+
for (const d of byIdentity.values()) {
|
|
110
|
+
const k = aliasKey(d);
|
|
111
|
+
if (!k) {
|
|
112
|
+
byAlias.set(`id:${d.deviceId}`, d);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const prev = byAlias.get(k);
|
|
116
|
+
if (!prev) {
|
|
117
|
+
byAlias.set(k, d);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const strongIdentityConflict = !!prev.publicKey && !!d.publicKey && prev.publicKey !== d.publicKey;
|
|
121
|
+
if (strongIdentityConflict) {
|
|
122
|
+
const winner = pickActiveOverStaleNeedsAuth(prev, d);
|
|
123
|
+
if (winner) {
|
|
124
|
+
byAlias.set(k, winner);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
byAlias.set(k, mergeEntries(prev, d));
|
|
129
|
+
}
|
|
130
|
+
const byEndpoint = new Map();
|
|
131
|
+
for (const d of byAlias.values()) {
|
|
132
|
+
const k = endpointKey(d);
|
|
133
|
+
if (!k) {
|
|
134
|
+
byEndpoint.set(`id:${d.deviceId}`, d);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const prev = byEndpoint.get(k);
|
|
138
|
+
byEndpoint.set(k, prev ? mergeEntries(prev, d) : d);
|
|
139
|
+
}
|
|
140
|
+
return [...byEndpoint.values()];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Threshold for "online" based on heartbeat age, in milliseconds.
|
|
144
|
+
* Matches the mobile app (HEARTBEAT_STALE_MS = 90 s). The SDK used
|
|
145
|
+
* 60 s, which flashed yellow on single missed beats.
|
|
146
|
+
*/
|
|
147
|
+
exports.HEARTBEAT_STALE_MS = 90000;
|
|
148
|
+
/**
|
|
149
|
+
* Returns a freshness flag consistent with the mobile app. A device is
|
|
150
|
+
* "fresh" when it was online per Convex AND its heartbeat is within
|
|
151
|
+
* `HEARTBEAT_STALE_MS`.
|
|
152
|
+
*/
|
|
153
|
+
function isDeviceFresh(d) {
|
|
154
|
+
if (!d.isOnline)
|
|
155
|
+
return false;
|
|
156
|
+
if (!d.lastHeartbeat)
|
|
157
|
+
return true;
|
|
158
|
+
return Date.now() - d.lastHeartbeat < exports.HEARTBEAT_STALE_MS;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Pick the best candidate for an auto-connect attempt. Preference:
|
|
162
|
+
* 1. matches the preferred deviceId when supplied + still fresh
|
|
163
|
+
* 2. fresh (online + recent heartbeat) + has a quicHost
|
|
164
|
+
* 3. online + has a quicHost
|
|
165
|
+
* 4. first with a quicHost
|
|
166
|
+
*/
|
|
167
|
+
function pickTargetDevice(devices, preferredDeviceId) {
|
|
168
|
+
if (!devices.length)
|
|
169
|
+
return null;
|
|
170
|
+
if (preferredDeviceId) {
|
|
171
|
+
const preferred = devices.find((d) => d.deviceId === preferredDeviceId && d.quicHost);
|
|
172
|
+
if (preferred && isDeviceFresh(preferred))
|
|
173
|
+
return preferred;
|
|
174
|
+
if (preferred)
|
|
175
|
+
return preferred;
|
|
176
|
+
}
|
|
177
|
+
const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
|
|
178
|
+
if (fresh)
|
|
179
|
+
return fresh;
|
|
180
|
+
const online = devices.find((d) => d.isOnline && d.quicHost);
|
|
181
|
+
if (online)
|
|
182
|
+
return online;
|
|
183
|
+
return devices.find((d) => d.quicHost) || devices[0] || null;
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|