yaver-feedback-react-native 0.7.7 → 0.7.8
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/MachinePickerScreen.js +18 -0
- package/dist/PairDeviceModal.d.ts +28 -0
- package/dist/PairDeviceModal.js +198 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/MachinePickerScreen.tsx +22 -0
- package/src/PairDeviceModal.tsx +228 -0
- package/src/index.ts +2 -0
|
@@ -37,6 +37,7 @@ exports.YaverMachinePickerScreen = void 0;
|
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const auth_1 = require("./auth");
|
|
40
|
+
const PairDeviceModal_1 = require("./PairDeviceModal");
|
|
40
41
|
/**
|
|
41
42
|
* List of remote dev machines the signed-in user can reach. Split into
|
|
42
43
|
* - Owned machines (user is the host)
|
|
@@ -51,6 +52,7 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
51
52
|
const [refreshing, setRefreshing] = (0, react_1.useState)(false);
|
|
52
53
|
const [error, setError] = (0, react_1.useState)(null);
|
|
53
54
|
const [list, setList] = (0, react_1.useState)({ owned: [], shared: [] });
|
|
55
|
+
const [pairingDevice, setPairingDevice] = (0, react_1.useState)(null);
|
|
54
56
|
const load = (0, react_1.useCallback)(async (silent = false) => {
|
|
55
57
|
if (!silent)
|
|
56
58
|
setLoading(true);
|
|
@@ -74,6 +76,15 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
74
76
|
void load();
|
|
75
77
|
}, [load]);
|
|
76
78
|
const handlePick = async (device) => {
|
|
79
|
+
// Needs-auth device — show the in-SDK pair modal instead of
|
|
80
|
+
// treating the tap as a "pick". The user enters the 6-char code
|
|
81
|
+
// from their Mac terminal; the SDK POSTs it to /auth/pair/submit
|
|
82
|
+
// on the agent directly. Once the device flips out of bootstrap
|
|
83
|
+
// mode, the next load() picks up the fresh state.
|
|
84
|
+
if (device.isOnline && device.needsAuth) {
|
|
85
|
+
setPairingDevice(device);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
77
88
|
await (0, auth_1.saveSelectedDeviceId)(device.deviceId);
|
|
78
89
|
onPick(device);
|
|
79
90
|
};
|
|
@@ -149,6 +160,13 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
149
160
|
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
150
161
|
</>)}
|
|
151
162
|
</react_native_1.ScrollView>
|
|
163
|
+
|
|
164
|
+
<PairDeviceModal_1.PairDeviceModal device={pairingDevice} onClose={() => setPairingDevice(null)} onPaired={() => {
|
|
165
|
+
// Give the agent a moment to flip bootstrap → owner mode,
|
|
166
|
+
// then reload the list so the now-authenticated device shows
|
|
167
|
+
// up with a green dot and can be selected normally.
|
|
168
|
+
setTimeout(() => void load(true), 1500);
|
|
169
|
+
}}/>
|
|
152
170
|
</react_native_1.SafeAreaView>);
|
|
153
171
|
};
|
|
154
172
|
exports.YaverMachinePickerScreen = YaverMachinePickerScreen;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { RemoteDevice } from './auth';
|
|
3
|
+
/**
|
|
4
|
+
* In-SDK remote-pair modal. Shown when the user taps a device in the
|
|
5
|
+
* machine picker that's in `needsAuth` state.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. User types the 6-char bootstrap passkey printed in the
|
|
9
|
+
* `yaver serve` terminal on the Mac (also shown in Yaver mobile
|
|
10
|
+
* app when pairing interactively).
|
|
11
|
+
* 2. SDK POSTs to `http://<device.quicHost>:<device.httpPort>/auth/pair/submit?code=XXXXXX`
|
|
12
|
+
* with the user's Convex session token (same one the SDK already
|
|
13
|
+
* has after Apple / Google / email sign-in).
|
|
14
|
+
* 3. Agent validates the token against Convex, persists it, flips
|
|
15
|
+
* out of bootstrap mode — within a couple of seconds it'll
|
|
16
|
+
* report `needsAuth=false` in /devices/list.
|
|
17
|
+
*
|
|
18
|
+
* This avoids making the user bounce to the Yaver mobile app just to
|
|
19
|
+
* adopt a machine. Works for owners and shared-scope guests since the
|
|
20
|
+
* pair endpoint accepts any valid Convex session that matches the
|
|
21
|
+
* expected account type.
|
|
22
|
+
*/
|
|
23
|
+
export interface PairDeviceModalProps {
|
|
24
|
+
device: RemoteDevice | null;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
onPaired?: (device: RemoteDevice) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare const PairDeviceModal: React.FC<PairDeviceModalProps>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PairDeviceModal = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const auth_1 = require("./auth");
|
|
40
|
+
const PairDeviceModal = ({ device, onClose, onPaired, }) => {
|
|
41
|
+
const [code, setCode] = (0, react_1.useState)('');
|
|
42
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
43
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
44
|
+
const [success, setSuccess] = (0, react_1.useState)(false);
|
|
45
|
+
(0, react_1.useEffect)(() => {
|
|
46
|
+
if (device) {
|
|
47
|
+
setCode('');
|
|
48
|
+
setError(null);
|
|
49
|
+
setSuccess(false);
|
|
50
|
+
}
|
|
51
|
+
}, [device?.deviceId]);
|
|
52
|
+
const handleSubmit = async () => {
|
|
53
|
+
if (!device)
|
|
54
|
+
return;
|
|
55
|
+
const trimmed = code.trim().toUpperCase();
|
|
56
|
+
if (trimmed.length !== 6) {
|
|
57
|
+
setError('Code must be 6 characters.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const token = await (0, auth_1.getToken)();
|
|
61
|
+
if (!token) {
|
|
62
|
+
setError('Not signed in.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const host = (device.quicHost || '').trim();
|
|
66
|
+
const port = device.httpPort || device.quicPort || 18080;
|
|
67
|
+
if (!host) {
|
|
68
|
+
setError('No reachable address for this machine.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setBusy(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const url = `http://${host}:${port}/auth/pair/submit?code=${encodeURIComponent(trimmed)}`;
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
token,
|
|
80
|
+
convexSiteUrl: (0, auth_1.getConvexSiteUrl)(),
|
|
81
|
+
// Backend reads userId from the session, but older agents
|
|
82
|
+
// expect it in the body. Pass empty string if unknown.
|
|
83
|
+
userId: '',
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
let msg = `Agent rejected pair (HTTP ${res.status}).`;
|
|
88
|
+
try {
|
|
89
|
+
const body = await res.json();
|
|
90
|
+
if (body?.error)
|
|
91
|
+
msg = String(body.error);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// body not JSON
|
|
95
|
+
}
|
|
96
|
+
throw new Error(msg);
|
|
97
|
+
}
|
|
98
|
+
setSuccess(true);
|
|
99
|
+
onPaired?.(device);
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
onClose();
|
|
102
|
+
}, 1200);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
setBusy(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return (<react_native_1.Modal visible={!!device} animationType="slide" transparent onRequestClose={onClose}>
|
|
112
|
+
<react_native_1.Pressable style={styles.overlay} onPress={onClose}>
|
|
113
|
+
<react_native_1.Pressable style={styles.card} onPress={(e) => e.stopPropagation()}>
|
|
114
|
+
<react_native_1.View style={styles.header}>
|
|
115
|
+
<react_native_1.Text style={styles.title}>Pair this Mac</react_native_1.Text>
|
|
116
|
+
<react_native_1.Pressable onPress={onClose} hitSlop={12} style={styles.closeBtn}>
|
|
117
|
+
<react_native_1.Text style={styles.closeIcon}>×</react_native_1.Text>
|
|
118
|
+
</react_native_1.Pressable>
|
|
119
|
+
</react_native_1.View>
|
|
120
|
+
|
|
121
|
+
<react_native_1.Text style={styles.deviceName}>{device?.name || device?.deviceId}</react_native_1.Text>
|
|
122
|
+
<react_native_1.Text style={styles.body}>
|
|
123
|
+
On the Mac where `yaver serve` is running, look for the 6-character code in the terminal output. Enter it here to adopt this machine.
|
|
124
|
+
</react_native_1.Text>
|
|
125
|
+
|
|
126
|
+
<react_native_1.TextInput style={styles.codeInput} value={code} onChangeText={(v) => setCode(v.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))} placeholder="ABCDEF" placeholderTextColor="#555" autoCapitalize="characters" autoCorrect={false} maxLength={6} keyboardType={react_native_1.Platform.OS === 'ios' ? 'ascii-capable' : 'visible-password'}/>
|
|
127
|
+
|
|
128
|
+
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
129
|
+
{success && <react_native_1.Text style={styles.success}>Paired ✓</react_native_1.Text>}
|
|
130
|
+
|
|
131
|
+
<react_native_1.Pressable onPress={handleSubmit} disabled={busy || success || code.length !== 6} style={({ pressed }) => [
|
|
132
|
+
styles.submit,
|
|
133
|
+
(busy || success || code.length !== 6) && styles.submitDisabled,
|
|
134
|
+
pressed && { opacity: 0.7 },
|
|
135
|
+
]}>
|
|
136
|
+
{busy ? (<react_native_1.ActivityIndicator color="#fff"/>) : (<react_native_1.Text style={styles.submitText}>{success ? 'Paired' : 'Pair'}</react_native_1.Text>)}
|
|
137
|
+
</react_native_1.Pressable>
|
|
138
|
+
</react_native_1.Pressable>
|
|
139
|
+
</react_native_1.Pressable>
|
|
140
|
+
</react_native_1.Modal>);
|
|
141
|
+
};
|
|
142
|
+
exports.PairDeviceModal = PairDeviceModal;
|
|
143
|
+
const styles = react_native_1.StyleSheet.create({
|
|
144
|
+
overlay: {
|
|
145
|
+
flex: 1,
|
|
146
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
147
|
+
justifyContent: 'flex-end',
|
|
148
|
+
},
|
|
149
|
+
card: {
|
|
150
|
+
backgroundColor: '#141422',
|
|
151
|
+
borderTopLeftRadius: 22,
|
|
152
|
+
borderTopRightRadius: 22,
|
|
153
|
+
padding: 24,
|
|
154
|
+
paddingBottom: 36,
|
|
155
|
+
gap: 14,
|
|
156
|
+
},
|
|
157
|
+
header: {
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
justifyContent: 'space-between',
|
|
161
|
+
},
|
|
162
|
+
title: { fontSize: 20, fontWeight: '700', color: '#fff' },
|
|
163
|
+
closeBtn: {
|
|
164
|
+
width: 36,
|
|
165
|
+
height: 36,
|
|
166
|
+
borderRadius: 18,
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
justifyContent: 'center',
|
|
169
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
170
|
+
},
|
|
171
|
+
closeIcon: { color: '#fff', fontSize: 22, lineHeight: 24 },
|
|
172
|
+
deviceName: { fontSize: 15, fontWeight: '600', color: '#c7c8ff' },
|
|
173
|
+
body: { fontSize: 13, color: '#9ca3af', lineHeight: 18 },
|
|
174
|
+
codeInput: {
|
|
175
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
176
|
+
borderWidth: 1,
|
|
177
|
+
borderColor: 'rgba(255,255,255,0.14)',
|
|
178
|
+
borderRadius: 12,
|
|
179
|
+
paddingHorizontal: 16,
|
|
180
|
+
paddingVertical: 16,
|
|
181
|
+
fontSize: 22,
|
|
182
|
+
fontWeight: '700',
|
|
183
|
+
letterSpacing: 4,
|
|
184
|
+
textAlign: 'center',
|
|
185
|
+
color: '#fff',
|
|
186
|
+
fontFamily: react_native_1.Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
187
|
+
},
|
|
188
|
+
error: { color: '#ef4444', fontSize: 13 },
|
|
189
|
+
success: { color: '#22c55e', fontSize: 14, fontWeight: '600' },
|
|
190
|
+
submit: {
|
|
191
|
+
backgroundColor: '#818cf8',
|
|
192
|
+
borderRadius: 12,
|
|
193
|
+
paddingVertical: 14,
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
},
|
|
196
|
+
submitDisabled: { opacity: 0.35 },
|
|
197
|
+
submitText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
|
198
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export { YaverLoginScreen } from './LoginScreen';
|
|
|
38
38
|
export type { YaverLoginScreenProps } from './LoginScreen';
|
|
39
39
|
export { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
40
40
|
export type { YaverMachinePickerProps } from './MachinePickerScreen';
|
|
41
|
+
export { PairDeviceModal } from './PairDeviceModal';
|
|
42
|
+
export type { PairDeviceModalProps } from './PairDeviceModal';
|
|
41
43
|
export { AuthOverlay } from './AuthOverlay';
|
|
42
44
|
export { ShakeDetector } from './ShakeDetector';
|
|
43
45
|
export { FloatingButton } from './FloatingButton';
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.FixReport = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
|
|
31
|
+
exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.FixReport = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
|
|
32
32
|
var YaverFeedback_1 = require("./YaverFeedback");
|
|
33
33
|
Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
|
|
34
34
|
var BlackBox_1 = require("./BlackBox");
|
|
@@ -47,6 +47,8 @@ var LoginScreen_1 = require("./LoginScreen");
|
|
|
47
47
|
Object.defineProperty(exports, "YaverLoginScreen", { enumerable: true, get: function () { return LoginScreen_1.YaverLoginScreen; } });
|
|
48
48
|
var MachinePickerScreen_1 = require("./MachinePickerScreen");
|
|
49
49
|
Object.defineProperty(exports, "YaverMachinePickerScreen", { enumerable: true, get: function () { return MachinePickerScreen_1.YaverMachinePickerScreen; } });
|
|
50
|
+
var PairDeviceModal_1 = require("./PairDeviceModal");
|
|
51
|
+
Object.defineProperty(exports, "PairDeviceModal", { enumerable: true, get: function () { return PairDeviceModal_1.PairDeviceModal; } });
|
|
50
52
|
var AuthOverlay_1 = require("./AuthOverlay");
|
|
51
53
|
Object.defineProperty(exports, "AuthOverlay", { enumerable: true, get: function () { return AuthOverlay_1.AuthOverlay; } });
|
|
52
54
|
var ShakeDetector_1 = require("./ShakeDetector");
|
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.8",
|
|
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",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
listReachableDevices,
|
|
16
16
|
saveSelectedDeviceId,
|
|
17
17
|
} from './auth';
|
|
18
|
+
import { PairDeviceModal } from './PairDeviceModal';
|
|
18
19
|
|
|
19
20
|
export interface YaverMachinePickerProps {
|
|
20
21
|
token: string;
|
|
@@ -43,6 +44,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
43
44
|
const [refreshing, setRefreshing] = useState(false);
|
|
44
45
|
const [error, setError] = useState<string | null>(null);
|
|
45
46
|
const [list, setList] = useState<DeviceList>({ owned: [], shared: [] });
|
|
47
|
+
const [pairingDevice, setPairingDevice] = useState<RemoteDevice | null>(null);
|
|
46
48
|
|
|
47
49
|
const load = useCallback(async (silent = false) => {
|
|
48
50
|
if (!silent) setLoading(true);
|
|
@@ -66,6 +68,15 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
66
68
|
}, [load]);
|
|
67
69
|
|
|
68
70
|
const handlePick = async (device: RemoteDevice) => {
|
|
71
|
+
// Needs-auth device — show the in-SDK pair modal instead of
|
|
72
|
+
// treating the tap as a "pick". The user enters the 6-char code
|
|
73
|
+
// from their Mac terminal; the SDK POSTs it to /auth/pair/submit
|
|
74
|
+
// on the agent directly. Once the device flips out of bootstrap
|
|
75
|
+
// mode, the next load() picks up the fresh state.
|
|
76
|
+
if (device.isOnline && device.needsAuth) {
|
|
77
|
+
setPairingDevice(device);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
69
80
|
await saveSelectedDeviceId(device.deviceId);
|
|
70
81
|
onPick(device);
|
|
71
82
|
};
|
|
@@ -165,6 +176,17 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
165
176
|
</>
|
|
166
177
|
)}
|
|
167
178
|
</ScrollView>
|
|
179
|
+
|
|
180
|
+
<PairDeviceModal
|
|
181
|
+
device={pairingDevice}
|
|
182
|
+
onClose={() => setPairingDevice(null)}
|
|
183
|
+
onPaired={() => {
|
|
184
|
+
// Give the agent a moment to flip bootstrap → owner mode,
|
|
185
|
+
// then reload the list so the now-authenticated device shows
|
|
186
|
+
// up with a green dot and can be selected normally.
|
|
187
|
+
setTimeout(() => void load(true), 1500);
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
168
190
|
</SafeAreaView>
|
|
169
191
|
);
|
|
170
192
|
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Modal,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
TextInput,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import type { RemoteDevice } from './auth';
|
|
13
|
+
import { getConvexSiteUrl, getToken } from './auth';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* In-SDK remote-pair modal. Shown when the user taps a device in the
|
|
17
|
+
* machine picker that's in `needsAuth` state.
|
|
18
|
+
*
|
|
19
|
+
* Flow:
|
|
20
|
+
* 1. User types the 6-char bootstrap passkey printed in the
|
|
21
|
+
* `yaver serve` terminal on the Mac (also shown in Yaver mobile
|
|
22
|
+
* app when pairing interactively).
|
|
23
|
+
* 2. SDK POSTs to `http://<device.quicHost>:<device.httpPort>/auth/pair/submit?code=XXXXXX`
|
|
24
|
+
* with the user's Convex session token (same one the SDK already
|
|
25
|
+
* has after Apple / Google / email sign-in).
|
|
26
|
+
* 3. Agent validates the token against Convex, persists it, flips
|
|
27
|
+
* out of bootstrap mode — within a couple of seconds it'll
|
|
28
|
+
* report `needsAuth=false` in /devices/list.
|
|
29
|
+
*
|
|
30
|
+
* This avoids making the user bounce to the Yaver mobile app just to
|
|
31
|
+
* adopt a machine. Works for owners and shared-scope guests since the
|
|
32
|
+
* pair endpoint accepts any valid Convex session that matches the
|
|
33
|
+
* expected account type.
|
|
34
|
+
*/
|
|
35
|
+
export interface PairDeviceModalProps {
|
|
36
|
+
device: RemoteDevice | null;
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
onPaired?: (device: RemoteDevice) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const PairDeviceModal: React.FC<PairDeviceModalProps> = ({
|
|
42
|
+
device,
|
|
43
|
+
onClose,
|
|
44
|
+
onPaired,
|
|
45
|
+
}) => {
|
|
46
|
+
const [code, setCode] = useState('');
|
|
47
|
+
const [busy, setBusy] = useState(false);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
const [success, setSuccess] = useState(false);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (device) {
|
|
53
|
+
setCode('');
|
|
54
|
+
setError(null);
|
|
55
|
+
setSuccess(false);
|
|
56
|
+
}
|
|
57
|
+
}, [device?.deviceId]);
|
|
58
|
+
|
|
59
|
+
const handleSubmit = async () => {
|
|
60
|
+
if (!device) return;
|
|
61
|
+
const trimmed = code.trim().toUpperCase();
|
|
62
|
+
if (trimmed.length !== 6) {
|
|
63
|
+
setError('Code must be 6 characters.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const token = await getToken();
|
|
67
|
+
if (!token) {
|
|
68
|
+
setError('Not signed in.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const host = (device.quicHost || '').trim();
|
|
72
|
+
const port = device.httpPort || device.quicPort || 18080;
|
|
73
|
+
if (!host) {
|
|
74
|
+
setError('No reachable address for this machine.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setBusy(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
try {
|
|
80
|
+
const url = `http://${host}:${port}/auth/pair/submit?code=${encodeURIComponent(
|
|
81
|
+
trimmed,
|
|
82
|
+
)}`;
|
|
83
|
+
const res = await fetch(url, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
token,
|
|
88
|
+
convexSiteUrl: getConvexSiteUrl(),
|
|
89
|
+
// Backend reads userId from the session, but older agents
|
|
90
|
+
// expect it in the body. Pass empty string if unknown.
|
|
91
|
+
userId: '',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
let msg = `Agent rejected pair (HTTP ${res.status}).`;
|
|
96
|
+
try {
|
|
97
|
+
const body = await res.json();
|
|
98
|
+
if (body?.error) msg = String(body.error);
|
|
99
|
+
} catch {
|
|
100
|
+
// body not JSON
|
|
101
|
+
}
|
|
102
|
+
throw new Error(msg);
|
|
103
|
+
}
|
|
104
|
+
setSuccess(true);
|
|
105
|
+
onPaired?.(device);
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
onClose();
|
|
108
|
+
}, 1200);
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
111
|
+
} finally {
|
|
112
|
+
setBusy(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Modal
|
|
118
|
+
visible={!!device}
|
|
119
|
+
animationType="slide"
|
|
120
|
+
transparent
|
|
121
|
+
onRequestClose={onClose}
|
|
122
|
+
>
|
|
123
|
+
<Pressable style={styles.overlay} onPress={onClose}>
|
|
124
|
+
<Pressable style={styles.card} onPress={(e) => e.stopPropagation()}>
|
|
125
|
+
<View style={styles.header}>
|
|
126
|
+
<Text style={styles.title}>Pair this Mac</Text>
|
|
127
|
+
<Pressable onPress={onClose} hitSlop={12} style={styles.closeBtn}>
|
|
128
|
+
<Text style={styles.closeIcon}>×</Text>
|
|
129
|
+
</Pressable>
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
<Text style={styles.deviceName}>{device?.name || device?.deviceId}</Text>
|
|
133
|
+
<Text style={styles.body}>
|
|
134
|
+
On the Mac where `yaver serve` is running, look for the 6-character code in the terminal output. Enter it here to adopt this machine.
|
|
135
|
+
</Text>
|
|
136
|
+
|
|
137
|
+
<TextInput
|
|
138
|
+
style={styles.codeInput}
|
|
139
|
+
value={code}
|
|
140
|
+
onChangeText={(v) => setCode(v.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))}
|
|
141
|
+
placeholder="ABCDEF"
|
|
142
|
+
placeholderTextColor="#555"
|
|
143
|
+
autoCapitalize="characters"
|
|
144
|
+
autoCorrect={false}
|
|
145
|
+
maxLength={6}
|
|
146
|
+
keyboardType={Platform.OS === 'ios' ? 'ascii-capable' : 'visible-password'}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
{error && <Text style={styles.error}>{error}</Text>}
|
|
150
|
+
{success && <Text style={styles.success}>Paired ✓</Text>}
|
|
151
|
+
|
|
152
|
+
<Pressable
|
|
153
|
+
onPress={handleSubmit}
|
|
154
|
+
disabled={busy || success || code.length !== 6}
|
|
155
|
+
style={({ pressed }) => [
|
|
156
|
+
styles.submit,
|
|
157
|
+
(busy || success || code.length !== 6) && styles.submitDisabled,
|
|
158
|
+
pressed && { opacity: 0.7 },
|
|
159
|
+
]}
|
|
160
|
+
>
|
|
161
|
+
{busy ? (
|
|
162
|
+
<ActivityIndicator color="#fff" />
|
|
163
|
+
) : (
|
|
164
|
+
<Text style={styles.submitText}>{success ? 'Paired' : 'Pair'}</Text>
|
|
165
|
+
)}
|
|
166
|
+
</Pressable>
|
|
167
|
+
</Pressable>
|
|
168
|
+
</Pressable>
|
|
169
|
+
</Modal>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const styles = StyleSheet.create({
|
|
174
|
+
overlay: {
|
|
175
|
+
flex: 1,
|
|
176
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
177
|
+
justifyContent: 'flex-end',
|
|
178
|
+
},
|
|
179
|
+
card: {
|
|
180
|
+
backgroundColor: '#141422',
|
|
181
|
+
borderTopLeftRadius: 22,
|
|
182
|
+
borderTopRightRadius: 22,
|
|
183
|
+
padding: 24,
|
|
184
|
+
paddingBottom: 36,
|
|
185
|
+
gap: 14,
|
|
186
|
+
},
|
|
187
|
+
header: {
|
|
188
|
+
flexDirection: 'row',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
justifyContent: 'space-between',
|
|
191
|
+
},
|
|
192
|
+
title: { fontSize: 20, fontWeight: '700', color: '#fff' },
|
|
193
|
+
closeBtn: {
|
|
194
|
+
width: 36,
|
|
195
|
+
height: 36,
|
|
196
|
+
borderRadius: 18,
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
justifyContent: 'center',
|
|
199
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
200
|
+
},
|
|
201
|
+
closeIcon: { color: '#fff', fontSize: 22, lineHeight: 24 },
|
|
202
|
+
deviceName: { fontSize: 15, fontWeight: '600', color: '#c7c8ff' },
|
|
203
|
+
body: { fontSize: 13, color: '#9ca3af', lineHeight: 18 },
|
|
204
|
+
codeInput: {
|
|
205
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
206
|
+
borderWidth: 1,
|
|
207
|
+
borderColor: 'rgba(255,255,255,0.14)',
|
|
208
|
+
borderRadius: 12,
|
|
209
|
+
paddingHorizontal: 16,
|
|
210
|
+
paddingVertical: 16,
|
|
211
|
+
fontSize: 22,
|
|
212
|
+
fontWeight: '700',
|
|
213
|
+
letterSpacing: 4,
|
|
214
|
+
textAlign: 'center',
|
|
215
|
+
color: '#fff',
|
|
216
|
+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
217
|
+
},
|
|
218
|
+
error: { color: '#ef4444', fontSize: 13 },
|
|
219
|
+
success: { color: '#22c55e', fontSize: 14, fontWeight: '600' },
|
|
220
|
+
submit: {
|
|
221
|
+
backgroundColor: '#818cf8',
|
|
222
|
+
borderRadius: 12,
|
|
223
|
+
paddingVertical: 14,
|
|
224
|
+
alignItems: 'center',
|
|
225
|
+
},
|
|
226
|
+
submitDisabled: { opacity: 0.35 },
|
|
227
|
+
submitText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
|
228
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ export { YaverLoginScreen } from './LoginScreen';
|
|
|
39
39
|
export type { YaverLoginScreenProps } from './LoginScreen';
|
|
40
40
|
export { YaverMachinePickerScreen } from './MachinePickerScreen';
|
|
41
41
|
export type { YaverMachinePickerProps } from './MachinePickerScreen';
|
|
42
|
+
export { PairDeviceModal } from './PairDeviceModal';
|
|
43
|
+
export type { PairDeviceModalProps } from './PairDeviceModal';
|
|
42
44
|
export { AuthOverlay } from './AuthOverlay';
|
|
43
45
|
export { ShakeDetector } from './ShakeDetector';
|
|
44
46
|
export { FloatingButton } from './FloatingButton';
|