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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Pressable,
|
|
5
|
+
SafeAreaView,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
TextInput,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import {
|
|
13
|
+
acceptGuestByCode,
|
|
14
|
+
acceptGuestInvitation,
|
|
15
|
+
fetchGuestHosts,
|
|
16
|
+
findInviteByCode,
|
|
17
|
+
type GuestHostsResponse,
|
|
18
|
+
type InvitationPreview,
|
|
19
|
+
} from './auth';
|
|
20
|
+
|
|
21
|
+
export interface YaverGuestOnboardingScreenProps {
|
|
22
|
+
token: string;
|
|
23
|
+
initialInviteCode?: string;
|
|
24
|
+
onContinue: () => void;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const YaverGuestOnboardingScreen: React.FC<YaverGuestOnboardingScreenProps> = ({
|
|
29
|
+
token,
|
|
30
|
+
initialInviteCode,
|
|
31
|
+
onContinue,
|
|
32
|
+
onCancel,
|
|
33
|
+
}) => {
|
|
34
|
+
const [code, setCode] = useState((initialInviteCode ?? '').toUpperCase());
|
|
35
|
+
const [preview, setPreview] = useState<InvitationPreview | null>(null);
|
|
36
|
+
const [hosts, setHosts] = useState<GuestHostsResponse>({ pending: [], active: [] });
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [busy, setBusy] = useState(false);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
const cleanedCode = useMemo(() => code.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6), [code]);
|
|
41
|
+
|
|
42
|
+
const loadHosts = useCallback(async () => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
try {
|
|
46
|
+
const result = await fetchGuestHosts(token);
|
|
47
|
+
setHosts(result);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, [token]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
void loadHosts();
|
|
57
|
+
}, [loadHosts]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
if (cleanedCode.length !== 6) {
|
|
62
|
+
setPreview(null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const result = await findInviteByCode(token, cleanedCode);
|
|
68
|
+
if (!cancelled) {
|
|
69
|
+
setPreview(result);
|
|
70
|
+
if (!result) {
|
|
71
|
+
setError('Invite code not found or expired.');
|
|
72
|
+
} else {
|
|
73
|
+
setError(null);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setPreview(null);
|
|
79
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
};
|
|
86
|
+
}, [cleanedCode, token]);
|
|
87
|
+
|
|
88
|
+
const handleAcceptCode = useCallback(async () => {
|
|
89
|
+
if (cleanedCode.length !== 6) {
|
|
90
|
+
setError('Enter the 6-character invite code from the host.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setBusy(true);
|
|
94
|
+
setError(null);
|
|
95
|
+
try {
|
|
96
|
+
await acceptGuestByCode(token, cleanedCode, preview?.proposedDeviceIds);
|
|
97
|
+
await loadHosts();
|
|
98
|
+
onContinue();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
101
|
+
} finally {
|
|
102
|
+
setBusy(false);
|
|
103
|
+
}
|
|
104
|
+
}, [cleanedCode, loadHosts, onContinue, preview?.proposedDeviceIds, token]);
|
|
105
|
+
|
|
106
|
+
const handleAcceptPending = useCallback(async (hostUserId: string) => {
|
|
107
|
+
setBusy(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
try {
|
|
110
|
+
await acceptGuestInvitation(token, hostUserId);
|
|
111
|
+
await loadHosts();
|
|
112
|
+
onContinue();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
115
|
+
} finally {
|
|
116
|
+
setBusy(false);
|
|
117
|
+
}
|
|
118
|
+
}, [loadHosts, onContinue, token]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<SafeAreaView style={styles.container}>
|
|
122
|
+
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
|
123
|
+
<View style={styles.header}>
|
|
124
|
+
<View>
|
|
125
|
+
<Text style={styles.title}>Guest Access</Text>
|
|
126
|
+
<Text style={styles.subtitle}>
|
|
127
|
+
Join a host's repo-scoped Feedback SDK access without leaving the app.
|
|
128
|
+
</Text>
|
|
129
|
+
</View>
|
|
130
|
+
{onCancel && (
|
|
131
|
+
<Pressable onPress={onCancel} style={styles.skipBtn}>
|
|
132
|
+
<Text style={styles.skipText}>Skip</Text>
|
|
133
|
+
</Pressable>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
<View style={styles.card}>
|
|
138
|
+
<Text style={styles.cardTitle}>Have an invite code?</Text>
|
|
139
|
+
<Text style={styles.cardText}>
|
|
140
|
+
Enter the 6-character code from the host. After redemption you can pick the shared machine directly here.
|
|
141
|
+
</Text>
|
|
142
|
+
<TextInput
|
|
143
|
+
style={styles.input}
|
|
144
|
+
value={cleanedCode}
|
|
145
|
+
onChangeText={setCode}
|
|
146
|
+
placeholder="ABC123"
|
|
147
|
+
placeholderTextColor="#667085"
|
|
148
|
+
autoCapitalize="characters"
|
|
149
|
+
autoCorrect={false}
|
|
150
|
+
maxLength={6}
|
|
151
|
+
/>
|
|
152
|
+
{preview && (
|
|
153
|
+
<View style={styles.previewBox}>
|
|
154
|
+
<Text style={styles.previewTitle}>{preview.hostName}</Text>
|
|
155
|
+
<Text style={styles.previewMeta}>{preview.hostEmail}</Text>
|
|
156
|
+
<Text style={styles.previewMeta}>
|
|
157
|
+
{preview.hostDevices.length > 0
|
|
158
|
+
? `${preview.hostDevices.length} host machine${preview.hostDevices.length === 1 ? '' : 's'} shared`
|
|
159
|
+
: 'Host machine list will appear after acceptance'}
|
|
160
|
+
</Text>
|
|
161
|
+
</View>
|
|
162
|
+
)}
|
|
163
|
+
<Pressable
|
|
164
|
+
onPress={() => void handleAcceptCode()}
|
|
165
|
+
disabled={busy || cleanedCode.length !== 6}
|
|
166
|
+
style={({ pressed }) => [
|
|
167
|
+
styles.primaryBtn,
|
|
168
|
+
(pressed || busy || cleanedCode.length !== 6) && styles.primaryBtnPressed,
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
{busy ? <ActivityIndicator color="#fff" /> : <Text style={styles.primaryBtnText}>Redeem Code</Text>}
|
|
172
|
+
</Pressable>
|
|
173
|
+
</View>
|
|
174
|
+
|
|
175
|
+
<View style={styles.card}>
|
|
176
|
+
<Text style={styles.cardTitle}>Pending host invites</Text>
|
|
177
|
+
<Text style={styles.cardText}>
|
|
178
|
+
If a host invited your email directly, it should appear here after you sign up.
|
|
179
|
+
</Text>
|
|
180
|
+
{loading ? (
|
|
181
|
+
<ActivityIndicator color="#98a2b3" style={{ marginTop: 12 }} />
|
|
182
|
+
) : hosts.pending.length > 0 ? (
|
|
183
|
+
hosts.pending.map((invite) => (
|
|
184
|
+
<View key={`${invite.hostUserId}:${invite.createdAt}`} style={styles.inviteRow}>
|
|
185
|
+
<View style={{ flex: 1 }}>
|
|
186
|
+
<Text style={styles.inviteName}>{invite.hostName}</Text>
|
|
187
|
+
<Text style={styles.inviteMeta}>{invite.hostEmail}</Text>
|
|
188
|
+
</View>
|
|
189
|
+
<Pressable
|
|
190
|
+
onPress={() => void handleAcceptPending(invite.hostUserId)}
|
|
191
|
+
disabled={busy}
|
|
192
|
+
style={({ pressed }) => [
|
|
193
|
+
styles.secondaryBtn,
|
|
194
|
+
(pressed || busy) && styles.secondaryBtnPressed,
|
|
195
|
+
]}
|
|
196
|
+
>
|
|
197
|
+
<Text style={styles.secondaryBtnText}>Accept</Text>
|
|
198
|
+
</Pressable>
|
|
199
|
+
</View>
|
|
200
|
+
))
|
|
201
|
+
) : (
|
|
202
|
+
<Text style={styles.emptyText}>No pending host invites on this account yet.</Text>
|
|
203
|
+
)}
|
|
204
|
+
</View>
|
|
205
|
+
|
|
206
|
+
<View style={styles.cardMuted}>
|
|
207
|
+
<Text style={styles.cardTitle}>No machine yet?</Text>
|
|
208
|
+
<Text style={styles.cardText}>
|
|
209
|
+
That is fine. You can still sign up here and redeem the host's guest access. Shared machines will appear once the host has one running. Later you can install Yaver on your own machine too.
|
|
210
|
+
</Text>
|
|
211
|
+
</View>
|
|
212
|
+
|
|
213
|
+
{error ? <Text style={styles.error}>{error}</Text> : null}
|
|
214
|
+
|
|
215
|
+
<Pressable
|
|
216
|
+
onPress={onContinue}
|
|
217
|
+
style={({ pressed }) => [styles.linkBtn, pressed && { opacity: 0.7 }]}
|
|
218
|
+
>
|
|
219
|
+
<Text style={styles.linkBtnText}>Continue to machine picker</Text>
|
|
220
|
+
</Pressable>
|
|
221
|
+
</ScrollView>
|
|
222
|
+
</SafeAreaView>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const styles = StyleSheet.create({
|
|
227
|
+
container: { flex: 1, backgroundColor: '#0f172a' },
|
|
228
|
+
content: { padding: 20, gap: 16 },
|
|
229
|
+
header: {
|
|
230
|
+
flexDirection: 'row',
|
|
231
|
+
justifyContent: 'space-between',
|
|
232
|
+
alignItems: 'flex-start',
|
|
233
|
+
marginBottom: 8,
|
|
234
|
+
},
|
|
235
|
+
title: { color: '#f8fafc', fontSize: 24, fontWeight: '700' },
|
|
236
|
+
subtitle: { color: '#94a3b8', fontSize: 14, marginTop: 6, maxWidth: 280 },
|
|
237
|
+
skipBtn: { paddingVertical: 6, paddingHorizontal: 10 },
|
|
238
|
+
skipText: { color: '#cbd5e1', fontSize: 13, fontWeight: '600' },
|
|
239
|
+
card: {
|
|
240
|
+
backgroundColor: 'rgba(15,23,42,0.82)',
|
|
241
|
+
borderWidth: 1,
|
|
242
|
+
borderColor: 'rgba(148,163,184,0.18)',
|
|
243
|
+
borderRadius: 16,
|
|
244
|
+
padding: 16,
|
|
245
|
+
},
|
|
246
|
+
cardMuted: {
|
|
247
|
+
backgroundColor: 'rgba(30,41,59,0.9)',
|
|
248
|
+
borderRadius: 16,
|
|
249
|
+
padding: 16,
|
|
250
|
+
},
|
|
251
|
+
cardTitle: { color: '#e2e8f0', fontSize: 16, fontWeight: '700' },
|
|
252
|
+
cardText: { color: '#94a3b8', fontSize: 13, lineHeight: 20, marginTop: 8 },
|
|
253
|
+
input: {
|
|
254
|
+
marginTop: 14,
|
|
255
|
+
borderWidth: 1,
|
|
256
|
+
borderColor: 'rgba(148,163,184,0.28)',
|
|
257
|
+
borderRadius: 12,
|
|
258
|
+
paddingHorizontal: 14,
|
|
259
|
+
paddingVertical: 12,
|
|
260
|
+
backgroundColor: 'rgba(2,6,23,0.88)',
|
|
261
|
+
color: '#f8fafc',
|
|
262
|
+
fontSize: 18,
|
|
263
|
+
letterSpacing: 3,
|
|
264
|
+
textAlign: 'center',
|
|
265
|
+
},
|
|
266
|
+
previewBox: {
|
|
267
|
+
marginTop: 12,
|
|
268
|
+
padding: 12,
|
|
269
|
+
borderRadius: 12,
|
|
270
|
+
backgroundColor: 'rgba(30,41,59,0.75)',
|
|
271
|
+
},
|
|
272
|
+
previewTitle: { color: '#f8fafc', fontSize: 15, fontWeight: '600' },
|
|
273
|
+
previewMeta: { color: '#94a3b8', fontSize: 12, marginTop: 4 },
|
|
274
|
+
primaryBtn: {
|
|
275
|
+
marginTop: 14,
|
|
276
|
+
backgroundColor: '#2563eb',
|
|
277
|
+
borderRadius: 12,
|
|
278
|
+
minHeight: 46,
|
|
279
|
+
alignItems: 'center',
|
|
280
|
+
justifyContent: 'center',
|
|
281
|
+
},
|
|
282
|
+
primaryBtnPressed: { opacity: 0.6 },
|
|
283
|
+
primaryBtnText: { color: '#fff', fontSize: 15, fontWeight: '700' },
|
|
284
|
+
inviteRow: {
|
|
285
|
+
flexDirection: 'row',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
gap: 12,
|
|
288
|
+
marginTop: 12,
|
|
289
|
+
paddingTop: 12,
|
|
290
|
+
borderTopWidth: 1,
|
|
291
|
+
borderTopColor: 'rgba(148,163,184,0.12)',
|
|
292
|
+
},
|
|
293
|
+
inviteName: { color: '#f8fafc', fontSize: 14, fontWeight: '600' },
|
|
294
|
+
inviteMeta: { color: '#94a3b8', fontSize: 12, marginTop: 4 },
|
|
295
|
+
secondaryBtn: {
|
|
296
|
+
paddingHorizontal: 12,
|
|
297
|
+
paddingVertical: 10,
|
|
298
|
+
borderRadius: 10,
|
|
299
|
+
backgroundColor: 'rgba(37,99,235,0.16)',
|
|
300
|
+
},
|
|
301
|
+
secondaryBtnPressed: { opacity: 0.6 },
|
|
302
|
+
secondaryBtnText: { color: '#bfdbfe', fontSize: 13, fontWeight: '700' },
|
|
303
|
+
emptyText: { color: '#64748b', fontSize: 13, marginTop: 12 },
|
|
304
|
+
error: { color: '#f87171', fontSize: 13, marginTop: 4 },
|
|
305
|
+
linkBtn: { alignItems: 'center', paddingVertical: 8 },
|
|
306
|
+
linkBtnText: { color: '#cbd5e1', fontSize: 14, fontWeight: '600' },
|
|
307
|
+
});
|
package/src/LoginScreen.tsx
CHANGED
|
@@ -113,9 +113,11 @@ const iconStyles = StyleSheet.create({
|
|
|
113
113
|
|
|
114
114
|
export interface YaverLoginScreenProps {
|
|
115
115
|
/** Invoked once a session token is issued and the user is loaded. */
|
|
116
|
-
onLoggedIn: (token: string) => void;
|
|
116
|
+
onLoggedIn: (token: string, opts?: { inviteCode?: string }) => void;
|
|
117
117
|
/** Optional cancel button shown in header. */
|
|
118
118
|
onCancel?: () => void;
|
|
119
|
+
/** Optional prefilled guest invite code from config / deep link. */
|
|
120
|
+
initialInviteCode?: string;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/**
|
|
@@ -126,6 +128,7 @@ export interface YaverLoginScreenProps {
|
|
|
126
128
|
export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
127
129
|
onLoggedIn,
|
|
128
130
|
onCancel,
|
|
131
|
+
initialInviteCode,
|
|
129
132
|
}) => {
|
|
130
133
|
const [busyProvider, setBusyProvider] = useState<OAuthProvider | 'apple' | null>(null);
|
|
131
134
|
const [showEmailForm, setShowEmailForm] = useState(false);
|
|
@@ -134,6 +137,7 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
134
137
|
const [email, setEmail] = useState('');
|
|
135
138
|
const [password, setPassword] = useState('');
|
|
136
139
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
140
|
+
const [inviteCode, setInviteCode] = useState((initialInviteCode ?? '').toUpperCase());
|
|
137
141
|
const [emailBusy, setEmailBusy] = useState(false);
|
|
138
142
|
const [emailError, setEmailError] = useState('');
|
|
139
143
|
|
|
@@ -141,7 +145,8 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
141
145
|
const user = await validateToken(token);
|
|
142
146
|
await saveToken(token);
|
|
143
147
|
if (user) await saveUser(user);
|
|
144
|
-
|
|
148
|
+
const cleanedInviteCode = inviteCode.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6);
|
|
149
|
+
onLoggedIn(token, cleanedInviteCode ? { inviteCode: cleanedInviteCode } : undefined);
|
|
145
150
|
};
|
|
146
151
|
|
|
147
152
|
const handleApple = async () => {
|
|
@@ -321,6 +326,20 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
321
326
|
secureTextEntry
|
|
322
327
|
/>
|
|
323
328
|
)}
|
|
329
|
+
{isSignUp && (
|
|
330
|
+
<TextInput
|
|
331
|
+
style={styles.input}
|
|
332
|
+
placeholder="Invite Code (optional)"
|
|
333
|
+
placeholderTextColor="#666"
|
|
334
|
+
value={inviteCode}
|
|
335
|
+
onChangeText={(value) =>
|
|
336
|
+
setInviteCode(value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))
|
|
337
|
+
}
|
|
338
|
+
autoCapitalize="characters"
|
|
339
|
+
autoCorrect={false}
|
|
340
|
+
maxLength={6}
|
|
341
|
+
/>
|
|
342
|
+
)}
|
|
324
343
|
|
|
325
344
|
{emailError ? (
|
|
326
345
|
<Text style={styles.errorText}>{emailError}</Text>
|
|
@@ -53,7 +53,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
|
|
|
53
53
|
const result = await listReachableDevices(token);
|
|
54
54
|
setList(result);
|
|
55
55
|
if (result.owned.length === 0 && result.shared.length === 0) {
|
|
56
|
-
setError(
|
|
56
|
+
setError(
|
|
57
|
+
'No machines found yet. If you do not have your own computer, redeem a host invite code first. Otherwise run `yaver auth` + `yaver serve` on your machine.',
|
|
58
|
+
);
|
|
57
59
|
}
|
|
58
60
|
} catch (err) {
|
|
59
61
|
setError(err instanceof Error ? err.message : String(err));
|
package/src/P2PClient.ts
CHANGED
|
@@ -7,6 +7,15 @@ export interface FeedbackEvent {
|
|
|
7
7
|
data: any;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface ReloadAck {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
mode: 'dev' | 'bundle';
|
|
13
|
+
acknowledged: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
nativeChangesDetected?: boolean;
|
|
16
|
+
changeClass?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
12
21
|
* agent can map the reload request to a MobileProject in its scan
|
|
@@ -317,7 +326,7 @@ export class P2PClient {
|
|
|
317
326
|
async reloadApp(
|
|
318
327
|
mode: 'dev' | 'bundle' = 'bundle',
|
|
319
328
|
opts?: { projectName?: string; bundleId?: string; projectPath?: string },
|
|
320
|
-
): Promise<
|
|
329
|
+
): Promise<ReloadAck> {
|
|
321
330
|
// Default path: always rebuild a fresh Hermes bundle.
|
|
322
331
|
//
|
|
323
332
|
// Rationale: the SDK's common caller is a phone user who's not
|
|
@@ -338,7 +347,19 @@ export class P2PClient {
|
|
|
338
347
|
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
339
348
|
});
|
|
340
349
|
if (primary.ok) {
|
|
341
|
-
|
|
350
|
+
const payload = await primary.json().catch(() => ({} as Record<string, unknown>));
|
|
351
|
+
const nativeChangesDetected = payload.nativeChangesDetected === true;
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
mode: 'dev',
|
|
355
|
+
acknowledged: true,
|
|
356
|
+
nativeChangesDetected,
|
|
357
|
+
changeClass:
|
|
358
|
+
typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
|
|
359
|
+
message: nativeChangesDetected
|
|
360
|
+
? 'Reload accepted, but native changes need a rebuild.'
|
|
361
|
+
: 'Hot reload request accepted.',
|
|
362
|
+
};
|
|
342
363
|
}
|
|
343
364
|
// Dev mode failed — fall through to bundle rebuild below rather
|
|
344
365
|
// than surfacing the raw error, so the user never has to know
|
|
@@ -370,7 +391,19 @@ export class P2PClient {
|
|
|
370
391
|
const text = await res.text().catch(() => '');
|
|
371
392
|
throw new Error(friendlyReloadError(res.status, text));
|
|
372
393
|
}
|
|
373
|
-
|
|
394
|
+
const payload = await res.json().catch(() => ({} as Record<string, unknown>));
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
mode: 'bundle',
|
|
398
|
+
acknowledged: true,
|
|
399
|
+
changeClass:
|
|
400
|
+
typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
|
|
401
|
+
nativeChangesDetected: payload.nativeChangesDetected === true,
|
|
402
|
+
message:
|
|
403
|
+
typeof payload.message === 'string' && payload.message.trim()
|
|
404
|
+
? payload.message
|
|
405
|
+
: 'Reload request acknowledged. Agent is rebuilding the bundle.',
|
|
406
|
+
};
|
|
374
407
|
}
|
|
375
408
|
|
|
376
409
|
/**
|
|
@@ -416,6 +449,39 @@ export class P2PClient {
|
|
|
416
449
|
return response.json();
|
|
417
450
|
}
|
|
418
451
|
|
|
452
|
+
async getVibingEligibility(
|
|
453
|
+
opts?: { projectName?: string; bundleId?: string; projectPath?: string },
|
|
454
|
+
): Promise<{
|
|
455
|
+
canVibe: boolean;
|
|
456
|
+
reason?: string;
|
|
457
|
+
guidance?: string;
|
|
458
|
+
projectName?: string;
|
|
459
|
+
projectPath?: string;
|
|
460
|
+
provider?: string;
|
|
461
|
+
repoFullName?: string;
|
|
462
|
+
}> {
|
|
463
|
+
const identity = resolveAppIdentity(opts);
|
|
464
|
+
const params = new URLSearchParams();
|
|
465
|
+
if (identity.projectName ?? opts?.projectName) {
|
|
466
|
+
params.set('projectName', identity.projectName ?? opts?.projectName ?? '');
|
|
467
|
+
}
|
|
468
|
+
if (identity.bundleId ?? opts?.bundleId) {
|
|
469
|
+
params.set('bundleId', identity.bundleId ?? opts?.bundleId ?? '');
|
|
470
|
+
}
|
|
471
|
+
if (identity.projectPath ?? opts?.projectPath) {
|
|
472
|
+
params.set('projectPath', identity.projectPath ?? opts?.projectPath ?? '');
|
|
473
|
+
}
|
|
474
|
+
const response = await fetch(`${this.baseUrl}/vibing/eligibility?${params.toString()}`, {
|
|
475
|
+
method: 'GET',
|
|
476
|
+
headers: { Authorization: `Bearer ${this.authToken}` },
|
|
477
|
+
});
|
|
478
|
+
if (!response.ok) {
|
|
479
|
+
const text = await response.text().catch(() => '');
|
|
480
|
+
throw new Error(`[P2PClient] Vibing eligibility failed (${response.status}): ${text}`);
|
|
481
|
+
}
|
|
482
|
+
return response.json();
|
|
483
|
+
}
|
|
484
|
+
|
|
419
485
|
/**
|
|
420
486
|
* After uploading a feedback bundle with `uploadFeedback`, call this
|
|
421
487
|
* with the returned report id to create a fix task on the agent. The
|
package/src/QuickActionIcon.tsx
CHANGED
|
@@ -12,7 +12,13 @@ import {
|
|
|
12
12
|
View,
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { YaverFeedback } from './YaverFeedback';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getQuickIconColorPreset,
|
|
17
|
+
getQuickIconDisabled,
|
|
18
|
+
QUICK_ICON_COLOR_PRESETS,
|
|
19
|
+
setQuickIconColorPreset,
|
|
20
|
+
setQuickIconDisabled,
|
|
21
|
+
} from './preferences';
|
|
16
22
|
|
|
17
23
|
// Mirror the suppression rule used by YaverFeedback + ShakeDetector:
|
|
18
24
|
// when loaded through Yaver's super-host Hermes bundle, the host owns
|
|
@@ -123,6 +129,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
123
129
|
const didDrag = useRef(false);
|
|
124
130
|
|
|
125
131
|
const [userDisabled, setUserDisabled] = useState<boolean | null>(null);
|
|
132
|
+
const [colorPreset, setColorPreset] = useState<keyof typeof QUICK_ICON_COLOR_PRESETS | null>(null);
|
|
126
133
|
const [shakenThisSession, setShakenThisSession] = useState(false);
|
|
127
134
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
128
135
|
const [hostSuppressed] = useState<boolean>(() => isRunningInsideYaverHost());
|
|
@@ -135,6 +142,9 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
135
142
|
getQuickIconDisabled().then((v) => {
|
|
136
143
|
if (alive) setUserDisabled(v);
|
|
137
144
|
});
|
|
145
|
+
getQuickIconColorPreset().then((v) => {
|
|
146
|
+
if (alive) setColorPreset(v);
|
|
147
|
+
});
|
|
138
148
|
return () => {
|
|
139
149
|
alive = false;
|
|
140
150
|
};
|
|
@@ -169,9 +179,18 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
169
179
|
setMenuOpen(false);
|
|
170
180
|
},
|
|
171
181
|
);
|
|
182
|
+
const colorSub = DeviceEventEmitter.addListener(
|
|
183
|
+
'yaverFeedback:quickIconColorChange',
|
|
184
|
+
(next: { preset?: keyof typeof QUICK_ICON_COLOR_PRESETS | null }) => {
|
|
185
|
+
const preset = next?.preset ?? null;
|
|
186
|
+
setColorPreset(preset);
|
|
187
|
+
void setQuickIconColorPreset(preset);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
172
190
|
return () => {
|
|
173
191
|
showSub.remove();
|
|
174
192
|
hideSub.remove();
|
|
193
|
+
colorSub.remove();
|
|
175
194
|
};
|
|
176
195
|
}, []);
|
|
177
196
|
|
|
@@ -229,6 +248,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
229
248
|
if (mode === 'after-shake' && !shakenThisSession) return null;
|
|
230
249
|
if (!YaverFeedback.isEnabled()) return null;
|
|
231
250
|
|
|
251
|
+
const presetColors = colorPreset ? QUICK_ICON_COLOR_PRESETS[colorPreset] : null;
|
|
232
252
|
const visualSize = size;
|
|
233
253
|
const radius = visualSize / 2;
|
|
234
254
|
|
|
@@ -271,9 +291,9 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
271
291
|
width: visualSize,
|
|
272
292
|
height: visualSize,
|
|
273
293
|
borderRadius: radius,
|
|
274
|
-
backgroundColor,
|
|
275
|
-
borderColor,
|
|
276
|
-
shadowColor,
|
|
294
|
+
backgroundColor: presetColors?.backgroundColor ?? backgroundColor,
|
|
295
|
+
borderColor: presetColors?.borderColor ?? borderColor,
|
|
296
|
+
shadowColor: presetColors?.shadowColor ?? shadowColor,
|
|
277
297
|
opacity: pressed ? 0.85 : 1,
|
|
278
298
|
},
|
|
279
299
|
]}
|
|
@@ -282,7 +302,7 @@ export const QuickActionIcon: React.FC<QuickActionIconProps> = ({
|
|
|
282
302
|
style={[
|
|
283
303
|
styles.iconLabel,
|
|
284
304
|
{
|
|
285
|
-
color: foregroundColor,
|
|
305
|
+
color: presetColors?.foregroundColor ?? foregroundColor,
|
|
286
306
|
fontSize: Math.round(visualSize * 0.5),
|
|
287
307
|
},
|
|
288
308
|
]}
|