yaver-feedback-react-native 0.7.6 → 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.
@@ -105,18 +105,27 @@ const FeedbackModal = () => {
105
105
  await fn(client);
106
106
  }
107
107
  catch (err) {
108
- const msg = err instanceof Error ? err.message : String(err);
109
- // 401 / 403 "invalid token"the cached session is stale
110
- // (common after a Convex deployment migration). Sign out so
111
- // the next interaction surfaces the login sheet, then bubble
112
- // a readable error up to the user.
113
- const authFailed = /\b(401|403)\b.*invalid token|invalid token|unauthor/i.test(msg);
108
+ const msg = (err instanceof Error ? err.message : String(err)) || '';
109
+ // Avoid unbounded `.*` in regexon RN 0.81 / Hermes rope
110
+ // strings plus a background SSE reconnect, that pattern has
111
+ // reliably SIGSEGV'd Hermes's string-view flattening path.
112
+ // Split into short, literal-only alternations.
113
+ const lower = msg.toLowerCase();
114
+ const authFailed = lower.indexOf('invalid token') >= 0 ||
115
+ lower.indexOf('unauthor') >= 0 ||
116
+ lower.indexOf(' 401') >= 0 ||
117
+ lower.indexOf(' 403') >= 0;
114
118
  if (authFailed) {
115
119
  await YaverFeedback_1.YaverFeedback.signOut();
116
120
  YaverFeedback_1.YaverFeedback.showLogin();
117
121
  throw new Error('Session expired — please sign in again.');
118
122
  }
119
- const transient = /Network request failed|timeout|ECONNREFUSED|Failed to fetch|fetch failed|aborted/i.test(msg);
123
+ const transient = lower.indexOf('network request failed') >= 0 ||
124
+ lower.indexOf('econnrefused') >= 0 ||
125
+ lower.indexOf('failed to fetch') >= 0 ||
126
+ lower.indexOf('fetch failed') >= 0 ||
127
+ lower.indexOf('aborted') >= 0 ||
128
+ lower.indexOf('timeout') >= 0;
120
129
  if (!transient)
121
130
  throw err;
122
131
  const ok = await YaverFeedback_1.YaverFeedback.reconnect();
@@ -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
  };
@@ -94,15 +105,33 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
94
105
  : device.needsAuth
95
106
  ? '#f59e0b'
96
107
  : '#22c55e';
108
+ // Derive a single short status phrase the user can act on.
109
+ let statusLine = device.platform;
110
+ if (!device.isOnline) {
111
+ statusLine = 'Offline — start `yaver serve` on the Mac';
112
+ }
113
+ else if (device.needsAuth) {
114
+ statusLine =
115
+ 'Needs pairing — open the Yaver app to adopt this machine';
116
+ }
117
+ else if (device.runnerDown) {
118
+ statusLine = 'Runner down — restart the coding agent on the Mac';
119
+ }
120
+ else {
121
+ // Happy-path subtitle: platform + optional host/share hint.
122
+ statusLine = device.platform;
123
+ if (device.isGuest && device.hostEmail) {
124
+ statusLine = `${statusLine} • ${device.hostEmail}`;
125
+ }
126
+ else if (device.accessScope === 'shared-scoped') {
127
+ statusLine = `${statusLine} • paylaşılan`;
128
+ }
129
+ }
97
130
  return (<react_native_1.TouchableOpacity key={device.deviceId} style={[styles.deviceRow, selected && styles.deviceSelected]} onPress={() => handlePick(device)}>
98
131
  <react_native_1.View style={[styles.health, { backgroundColor: healthColor }]}/>
99
132
  <react_native_1.View style={{ flex: 1 }}>
100
133
  <react_native_1.Text style={styles.deviceName}>{device.name || device.deviceId}</react_native_1.Text>
101
- <react_native_1.Text style={styles.deviceMeta}>
102
- {device.platform}
103
- {device.isGuest && device.hostEmail ? ` • ${device.hostEmail}` : ''}
104
- {device.accessScope === 'shared-scoped' ? ' • paylaşılan' : ''}
105
- </react_native_1.Text>
134
+ <react_native_1.Text style={styles.deviceMeta}>{statusLine}</react_native_1.Text>
106
135
  </react_native_1.View>
107
136
  {selected && <react_native_1.Text style={styles.selectedBadge}>seçili</react_native_1.Text>}
108
137
  </react_native_1.TouchableOpacity>);
@@ -131,6 +160,13 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
131
160
  {error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
132
161
  </>)}
133
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
+ }}/>
134
170
  </react_native_1.SafeAreaView>);
135
171
  };
136
172
  exports.YaverMachinePickerScreen = YaverMachinePickerScreen;
package/dist/P2PClient.js CHANGED
@@ -13,21 +13,28 @@ const react_native_1 = require("react-native");
13
13
  * running on the host machine".
14
14
  */
15
15
  function friendlyReloadError(status, body) {
16
- const lower = body.toLowerCase();
17
- if (/connection refused|econnrefused/.test(lower) &&
18
- /127\.0\.0\.1|localhost/.test(lower)) {
16
+ // Plain .indexOf instead of regex — short literal tests sidestep
17
+ // a Hermes rope-flatten SIGSEGV we saw on RN 0.81 / iOS 18.3.1
18
+ // when the same body was already being processed by a concurrent
19
+ // SSE reconnect loop.
20
+ const lower = (body || '').toLowerCase();
21
+ const hasRefused = lower.indexOf('connection refused') >= 0 ||
22
+ lower.indexOf('econnrefused') >= 0;
23
+ const hasLoopback = lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0;
24
+ if (hasRefused && hasLoopback) {
19
25
  return ('No dev server running on your machine. ' +
20
26
  'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.');
21
27
  }
22
- if (/no dev server/.test(lower) || /not running/.test(lower)) {
28
+ if (lower.indexOf('no dev server') >= 0 ||
29
+ lower.indexOf('not running') >= 0) {
23
30
  return 'No dev server running on your machine. Start Metro first.';
24
31
  }
25
- if (status === 403)
32
+ if (status === 401 || status === 403) {
26
33
  return 'Agent rejected the session — please sign in again.';
27
- if (status === 401)
28
- return 'Agent rejected the session — please sign in again.';
29
- if (status >= 500)
34
+ }
35
+ if (status >= 500) {
30
36
  return 'Agent hit an internal error while reloading. Check `yaver logs`.';
37
+ }
31
38
  return `Reload failed (${status}).`;
32
39
  }
33
40
  /**
@@ -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
+ });
@@ -171,19 +171,18 @@ class YaverFeedback {
171
171
  });
172
172
  }
173
173
  });
174
- // Auto-open the command-stream SSE channel so `status` +
175
- // `reload_bundle` messages from the agent actually reach us.
176
- // Host apps can still call BlackBox.start(...) themselves to
177
- // customise deviceId / appName, but they don't have to.
178
- if (!BlackBox_1.BlackBox.isStreaming) {
179
- try {
180
- BlackBox_1.BlackBox.start();
181
- }
182
- catch {
183
- // BlackBox.start throws if agentUrl/token aren't ready yet;
184
- // a later setAuthToken() / discoverAgent() tick will retry.
185
- }
186
- }
174
+ // NOTE: BlackBox.start() is intentionally NOT auto-called here.
175
+ // An earlier version (0.7.6) did auto-start it, and when the
176
+ // agent was in bootstrap / needs-auth mode — which can happen
177
+ // any time after `yaver serve` restarts before the user pairs —
178
+ // the SSE channel retried with exponential backoff on 401s,
179
+ // producing a tight loop of string concatenation + JSON parse
180
+ // that tripped a Hermes rope-string SIGSEGV on iOS 18.3.1
181
+ // during any other JS-thread regex work (e.g. react-native-
182
+ // view-shot's internal string handling during Screenshot &
183
+ // Fix). Host apps call BlackBox.start() explicitly once they
184
+ // know the agent URL + token are valid (SFMG does this inside
185
+ // its YaverFeedbackWidget after auth).
187
186
  }
188
187
  // NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
189
188
  // Sentry, Crashlytics, Bugsnag, and other tools all compete for that
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.6",
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",
@@ -116,18 +116,29 @@ export const FeedbackModal: React.FC = () => {
116
116
  try {
117
117
  await fn(client);
118
118
  } catch (err) {
119
- const msg = err instanceof Error ? err.message : String(err);
120
- // 401 / 403 "invalid token"the cached session is stale
121
- // (common after a Convex deployment migration). Sign out so
122
- // the next interaction surfaces the login sheet, then bubble
123
- // a readable error up to the user.
124
- const authFailed = /\b(401|403)\b.*invalid token|invalid token|unauthor/i.test(msg);
119
+ const msg = (err instanceof Error ? err.message : String(err)) || '';
120
+ // Avoid unbounded `.*` in regexon RN 0.81 / Hermes rope
121
+ // strings plus a background SSE reconnect, that pattern has
122
+ // reliably SIGSEGV'd Hermes's string-view flattening path.
123
+ // Split into short, literal-only alternations.
124
+ const lower = msg.toLowerCase();
125
+ const authFailed =
126
+ lower.indexOf('invalid token') >= 0 ||
127
+ lower.indexOf('unauthor') >= 0 ||
128
+ lower.indexOf(' 401') >= 0 ||
129
+ lower.indexOf(' 403') >= 0;
125
130
  if (authFailed) {
126
131
  await YaverFeedback.signOut();
127
132
  YaverFeedback.showLogin();
128
133
  throw new Error('Session expired — please sign in again.');
129
134
  }
130
- const transient = /Network request failed|timeout|ECONNREFUSED|Failed to fetch|fetch failed|aborted/i.test(msg);
135
+ const transient =
136
+ lower.indexOf('network request failed') >= 0 ||
137
+ lower.indexOf('econnrefused') >= 0 ||
138
+ lower.indexOf('failed to fetch') >= 0 ||
139
+ lower.indexOf('fetch failed') >= 0 ||
140
+ lower.indexOf('aborted') >= 0 ||
141
+ lower.indexOf('timeout') >= 0;
131
142
  if (!transient) throw err;
132
143
  const ok = await YaverFeedback.reconnect();
133
144
  if (!ok) throw err;
@@ -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
  };
@@ -87,6 +98,24 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
87
98
  : device.needsAuth
88
99
  ? '#f59e0b'
89
100
  : '#22c55e';
101
+ // Derive a single short status phrase the user can act on.
102
+ let statusLine = device.platform;
103
+ if (!device.isOnline) {
104
+ statusLine = 'Offline — start `yaver serve` on the Mac';
105
+ } else if (device.needsAuth) {
106
+ statusLine =
107
+ 'Needs pairing — open the Yaver app to adopt this machine';
108
+ } else if (device.runnerDown) {
109
+ statusLine = 'Runner down — restart the coding agent on the Mac';
110
+ } else {
111
+ // Happy-path subtitle: platform + optional host/share hint.
112
+ statusLine = device.platform;
113
+ if (device.isGuest && device.hostEmail) {
114
+ statusLine = `${statusLine} • ${device.hostEmail}`;
115
+ } else if (device.accessScope === 'shared-scoped') {
116
+ statusLine = `${statusLine} • paylaşılan`;
117
+ }
118
+ }
90
119
  return (
91
120
  <TouchableOpacity
92
121
  key={device.deviceId}
@@ -96,11 +125,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
96
125
  <View style={[styles.health, { backgroundColor: healthColor }]} />
97
126
  <View style={{ flex: 1 }}>
98
127
  <Text style={styles.deviceName}>{device.name || device.deviceId}</Text>
99
- <Text style={styles.deviceMeta}>
100
- {device.platform}
101
- {device.isGuest && device.hostEmail ? ` • ${device.hostEmail}` : ''}
102
- {device.accessScope === 'shared-scoped' ? ' • paylaşılan' : ''}
103
- </Text>
128
+ <Text style={styles.deviceMeta}>{statusLine}</Text>
104
129
  </View>
105
130
  {selected && <Text style={styles.selectedBadge}>seçili</Text>}
106
131
  </TouchableOpacity>
@@ -151,6 +176,17 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
151
176
  </>
152
177
  )}
153
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
+ />
154
190
  </SafeAreaView>
155
191
  );
156
192
  };
package/src/P2PClient.ts CHANGED
@@ -18,22 +18,34 @@ export interface FeedbackEvent {
18
18
  * running on the host machine".
19
19
  */
20
20
  function friendlyReloadError(status: number, body: string): string {
21
- const lower = body.toLowerCase();
22
- if (
23
- /connection refused|econnrefused/.test(lower) &&
24
- /127\.0\.0\.1|localhost/.test(lower)
25
- ) {
21
+ // Plain .indexOf instead of regex — short literal tests sidestep
22
+ // a Hermes rope-flatten SIGSEGV we saw on RN 0.81 / iOS 18.3.1
23
+ // when the same body was already being processed by a concurrent
24
+ // SSE reconnect loop.
25
+ const lower = (body || '').toLowerCase();
26
+ const hasRefused =
27
+ lower.indexOf('connection refused') >= 0 ||
28
+ lower.indexOf('econnrefused') >= 0;
29
+ const hasLoopback =
30
+ lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0;
31
+ if (hasRefused && hasLoopback) {
26
32
  return (
27
33
  'No dev server running on your machine. ' +
28
34
  'Start Metro with `yaver dev start` or use "Screenshot & Fix" instead.'
29
35
  );
30
36
  }
31
- if (/no dev server/.test(lower) || /not running/.test(lower)) {
37
+ if (
38
+ lower.indexOf('no dev server') >= 0 ||
39
+ lower.indexOf('not running') >= 0
40
+ ) {
32
41
  return 'No dev server running on your machine. Start Metro first.';
33
42
  }
34
- if (status === 403) return 'Agent rejected the session — please sign in again.';
35
- if (status === 401) return 'Agent rejected the session — please sign in again.';
36
- if (status >= 500) return 'Agent hit an internal error while reloading. Check `yaver logs`.';
43
+ if (status === 401 || status === 403) {
44
+ return 'Agent rejected the session — please sign in again.';
45
+ }
46
+ if (status >= 500) {
47
+ return 'Agent hit an internal error while reloading. Check `yaver logs`.';
48
+ }
37
49
  return `Reload failed (${status}).`;
38
50
  }
39
51
 
@@ -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
+ });
@@ -185,18 +185,18 @@ export class YaverFeedback {
185
185
  });
186
186
  }
187
187
  });
188
- // Auto-open the command-stream SSE channel so `status` +
189
- // `reload_bundle` messages from the agent actually reach us.
190
- // Host apps can still call BlackBox.start(...) themselves to
191
- // customise deviceId / appName, but they don't have to.
192
- if (!BlackBox.isStreaming) {
193
- try {
194
- BlackBox.start();
195
- } catch {
196
- // BlackBox.start throws if agentUrl/token aren't ready yet;
197
- // a later setAuthToken() / discoverAgent() tick will retry.
198
- }
199
- }
188
+ // NOTE: BlackBox.start() is intentionally NOT auto-called here.
189
+ // An earlier version (0.7.6) did auto-start it, and when the
190
+ // agent was in bootstrap / needs-auth mode — which can happen
191
+ // any time after `yaver serve` restarts before the user pairs —
192
+ // the SSE channel retried with exponential backoff on 401s,
193
+ // producing a tight loop of string concatenation + JSON parse
194
+ // that tripped a Hermes rope-string SIGSEGV on iOS 18.3.1
195
+ // during any other JS-thread regex work (e.g. react-native-
196
+ // view-shot's internal string handling during Screenshot &
197
+ // Fix). Host apps call BlackBox.start() explicitly once they
198
+ // know the agent URL + token are valid (SFMG does this inside
199
+ // its YaverFeedbackWidget after auth).
200
200
  }
201
201
 
202
202
  // NOTE: We intentionally do NOT hook ErrorUtils.setGlobalHandler().
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';