yaver-feedback-react-native 0.7.9 → 0.7.11
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/FeedbackModal.js +41 -3
- package/dist/LoginScreen.js +57 -1
- package/dist/P2PClient.d.ts +5 -1
- package/dist/P2PClient.js +62 -2
- package/dist/YaverFeedback.js +6 -2
- package/package.json +1 -1
- package/src/FeedbackModal.tsx +46 -4
- package/src/LoginScreen.tsx +94 -1
- package/src/P2PClient.ts +68 -2
- package/src/YaverFeedback.ts +7 -2
package/dist/FeedbackModal.js
CHANGED
|
@@ -45,6 +45,7 @@ const FeedbackModal = () => {
|
|
|
45
45
|
const [action, setAction] = (0, react_1.useState)('idle');
|
|
46
46
|
const [error, setError] = (0, react_1.useState)(null);
|
|
47
47
|
const [toast, setToast] = (0, react_1.useState)(null);
|
|
48
|
+
const [progress, setProgress] = (0, react_1.useState)(null);
|
|
48
49
|
const [isRecordingVideo, setIsRecordingVideo] = (0, react_1.useState)(false);
|
|
49
50
|
const [lastVideo, setLastVideo] = (0, react_1.useState)(null);
|
|
50
51
|
const mountedRef = (0, react_1.useRef)(true);
|
|
@@ -69,6 +70,14 @@ const FeedbackModal = () => {
|
|
|
69
70
|
const msg = payload?.message || payload?.phase || '';
|
|
70
71
|
if (msg)
|
|
71
72
|
setToast(msg);
|
|
73
|
+
if (typeof payload?.progress === 'number') {
|
|
74
|
+
setProgress(payload.progress);
|
|
75
|
+
}
|
|
76
|
+
// On final phases, fade the bar to 100% so the user sees
|
|
77
|
+
// completion before the modal auto-dismisses.
|
|
78
|
+
if (payload?.phase === 'done' || payload?.phase === 'error') {
|
|
79
|
+
setProgress(1);
|
|
80
|
+
}
|
|
72
81
|
});
|
|
73
82
|
return () => {
|
|
74
83
|
mountedRef.current = false;
|
|
@@ -141,15 +150,26 @@ const FeedbackModal = () => {
|
|
|
141
150
|
const handleHotReload = (0, react_1.useCallback)(async () => {
|
|
142
151
|
setAction('hot-reloading');
|
|
143
152
|
setError(null);
|
|
153
|
+
setProgress(0);
|
|
154
|
+
setToast('Sending…');
|
|
144
155
|
try {
|
|
156
|
+
// Default mode: bundle. Always rebuilds via the agent regardless
|
|
157
|
+
// of Metro state. P2PClient.reloadApp auto-resolves projectName +
|
|
158
|
+
// bundleId from expo-constants / NativeModules so the agent can
|
|
159
|
+
// map this app to its MobileProject scan entry without needing
|
|
160
|
+
// `yaver dev start` to have been run.
|
|
145
161
|
await runWithReconnect(async (client) => {
|
|
146
|
-
await client.reloadApp('
|
|
162
|
+
await client.reloadApp('bundle');
|
|
147
163
|
});
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
// We don't auto-close here — the agent's BlackBox status pings
|
|
165
|
+
// will keep the modal updated, and the on-device YaverBundleLoader
|
|
166
|
+
// will reload the JS once the fresh bundle arrives. Modal stays
|
|
167
|
+
// up for a beat so the user sees the final progress state.
|
|
168
|
+
closeSoon(2500);
|
|
150
169
|
}
|
|
151
170
|
catch (err) {
|
|
152
171
|
setError(err instanceof Error ? err.message : String(err));
|
|
172
|
+
setProgress(null);
|
|
153
173
|
}
|
|
154
174
|
finally {
|
|
155
175
|
if (mountedRef.current)
|
|
@@ -387,6 +407,12 @@ const FeedbackModal = () => {
|
|
|
387
407
|
? `Send Video · ${Math.round(lastVideo.duration)}s`
|
|
388
408
|
: 'Send Video'} tint="#a78bfa" onPress={handleSendVideo} disabled={busy || !lastVideo} busy={action === 'sending-video'}/>
|
|
389
409
|
|
|
410
|
+
{progress !== null && (<react_native_1.View style={styles.progressTrack}>
|
|
411
|
+
<react_native_1.View style={[
|
|
412
|
+
styles.progressFill,
|
|
413
|
+
{ width: `${Math.round(progress * 100)}%` },
|
|
414
|
+
]}/>
|
|
415
|
+
</react_native_1.View>)}
|
|
390
416
|
{toast && <react_native_1.Text style={styles.toast}>{toast}</react_native_1.Text>}
|
|
391
417
|
{error && <react_native_1.Text style={styles.error}>{error}</react_native_1.Text>}
|
|
392
418
|
</react_native_1.Pressable>
|
|
@@ -459,6 +485,18 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
459
485
|
fontSize: 15,
|
|
460
486
|
fontWeight: '700',
|
|
461
487
|
},
|
|
488
|
+
progressTrack: {
|
|
489
|
+
height: 6,
|
|
490
|
+
borderRadius: 3,
|
|
491
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
492
|
+
overflow: 'hidden',
|
|
493
|
+
marginTop: 4,
|
|
494
|
+
},
|
|
495
|
+
progressFill: {
|
|
496
|
+
height: '100%',
|
|
497
|
+
backgroundColor: '#818cf8',
|
|
498
|
+
borderRadius: 3,
|
|
499
|
+
},
|
|
462
500
|
toast: {
|
|
463
501
|
color: '#22c55e',
|
|
464
502
|
fontSize: 13,
|
package/dist/LoginScreen.js
CHANGED
|
@@ -37,6 +37,51 @@ exports.YaverLoginScreen = 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 PROVIDER_THEME = {
|
|
41
|
+
apple: { ion: 'logo-apple', letter: '', fg: '#FFFFFF', bg: 'transparent' },
|
|
42
|
+
google: { ion: 'logo-google', letter: 'G', fg: '#EA4335', bg: 'transparent' },
|
|
43
|
+
github: { ion: 'logo-github', letter: 'G', fg: '#FFFFFF', bg: 'transparent' },
|
|
44
|
+
gitlab: { ion: 'logo-gitlab', letter: 'G', fg: '#FC6D26', bg: 'transparent' },
|
|
45
|
+
microsoft: { ion: 'logo-microsoft', letter: 'M', fg: '#00A4EF', bg: 'transparent' },
|
|
46
|
+
};
|
|
47
|
+
const ProviderLogo = ({ provider, }) => {
|
|
48
|
+
const theme = PROVIDER_THEME[provider] ?? PROVIDER_THEME.apple;
|
|
49
|
+
// Soft-require @expo/vector-icons so the SDK works in bare-RN apps
|
|
50
|
+
// that don't ship Expo. If present, render the brand glyph; if
|
|
51
|
+
// not, render a coloured monogram fallback.
|
|
52
|
+
let Ionicons = null;
|
|
53
|
+
try {
|
|
54
|
+
const mod = require('@expo/vector-icons');
|
|
55
|
+
Ionicons = mod?.Ionicons ?? null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// package not installed — use letter fallback
|
|
59
|
+
}
|
|
60
|
+
if (Ionicons) {
|
|
61
|
+
return (<Ionicons name={theme.ion} size={18} color={theme.fg} style={iconStyles.icon}/>);
|
|
62
|
+
}
|
|
63
|
+
if (!theme.letter) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return (<react_native_1.View style={[iconStyles.fallback, { backgroundColor: theme.fg + '22' }]}>
|
|
67
|
+
<react_native_1.Text style={[iconStyles.fallbackText, { color: theme.fg }]}>{theme.letter}</react_native_1.Text>
|
|
68
|
+
</react_native_1.View>);
|
|
69
|
+
};
|
|
70
|
+
const iconStyles = react_native_1.StyleSheet.create({
|
|
71
|
+
icon: { marginRight: 12 },
|
|
72
|
+
fallback: {
|
|
73
|
+
width: 22,
|
|
74
|
+
height: 22,
|
|
75
|
+
borderRadius: 11,
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
marginRight: 12,
|
|
79
|
+
},
|
|
80
|
+
fallbackText: {
|
|
81
|
+
fontSize: 12,
|
|
82
|
+
fontWeight: '700',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
40
85
|
/**
|
|
41
86
|
* Full-screen in-SDK login. Mirrors the Yaver mobile app login UX: native
|
|
42
87
|
* Apple Sign-In on iOS, in-app browser OAuth for Google/GitHub/GitLab/
|
|
@@ -120,7 +165,10 @@ const YaverLoginScreen = ({ onLoggedIn, onCancel, }) => {
|
|
|
120
165
|
pressed && styles.buttonPressed,
|
|
121
166
|
busyProvider === id && { opacity: 0.6 },
|
|
122
167
|
]} onPress={onPress} disabled={busyProvider !== null}>
|
|
123
|
-
{busyProvider === id ? (<react_native_1.ActivityIndicator color="#e0e0e0"/>) : (<react_native_1.
|
|
168
|
+
{busyProvider === id ? (<react_native_1.ActivityIndicator color="#e0e0e0"/>) : (<react_native_1.View style={styles.buttonContent}>
|
|
169
|
+
<ProviderLogo provider={id}/>
|
|
170
|
+
<react_native_1.Text style={[styles.buttonText, styles.buttonTextWithIcon]}>{label}</react_native_1.Text>
|
|
171
|
+
</react_native_1.View>)}
|
|
124
172
|
</react_native_1.Pressable>);
|
|
125
173
|
return (<react_native_1.SafeAreaView style={styles.safeArea}>
|
|
126
174
|
<react_native_1.KeyboardAvoidingView style={{ flex: 1 }} behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
@@ -211,6 +259,14 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
211
259
|
},
|
|
212
260
|
buttonPressed: { opacity: 0.7 },
|
|
213
261
|
buttonText: { color: '#e0e0e0', fontSize: 15, fontWeight: '600' },
|
|
262
|
+
buttonContent: {
|
|
263
|
+
flexDirection: 'row',
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
justifyContent: 'center',
|
|
266
|
+
},
|
|
267
|
+
buttonTextWithIcon: {
|
|
268
|
+
// No extra spacing — ProviderLogo provides its own marginRight.
|
|
269
|
+
},
|
|
214
270
|
divider: {
|
|
215
271
|
flexDirection: 'row',
|
|
216
272
|
alignItems: 'center',
|
package/dist/P2PClient.d.ts
CHANGED
|
@@ -65,7 +65,11 @@ export declare class P2PClient {
|
|
|
65
65
|
* via the BlackBox command channel.
|
|
66
66
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
67
67
|
*/
|
|
68
|
-
reloadApp(mode?: 'dev' | 'bundle'
|
|
68
|
+
reloadApp(mode?: 'dev' | 'bundle', opts?: {
|
|
69
|
+
projectName?: string;
|
|
70
|
+
bundleId?: string;
|
|
71
|
+
projectPath?: string;
|
|
72
|
+
}): Promise<{
|
|
69
73
|
ok: boolean;
|
|
70
74
|
}>;
|
|
71
75
|
/**
|
package/dist/P2PClient.js
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.P2PClient = void 0;
|
|
4
4
|
const react_native_1 = require("react-native");
|
|
5
|
+
/**
|
|
6
|
+
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
7
|
+
* agent can map the reload request to a MobileProject in its scan
|
|
8
|
+
* cache. Order: caller-supplied opts → Expo Constants → react-native
|
|
9
|
+
* NativeModules. None of the lookups throw — missing data just means
|
|
10
|
+
* the agent will fall back to its own dev-server resolution.
|
|
11
|
+
*/
|
|
12
|
+
function resolveAppIdentity(opts) {
|
|
13
|
+
let projectName = opts?.projectName;
|
|
14
|
+
let bundleId = opts?.bundleId;
|
|
15
|
+
const projectPath = opts?.projectPath;
|
|
16
|
+
if (!projectName || !bundleId) {
|
|
17
|
+
try {
|
|
18
|
+
const Constants = require('expo-constants').default ?? require('expo-constants');
|
|
19
|
+
const cfg = Constants?.expoConfig ?? Constants?.manifest ?? {};
|
|
20
|
+
projectName = projectName || cfg?.name;
|
|
21
|
+
bundleId =
|
|
22
|
+
bundleId ||
|
|
23
|
+
cfg?.ios?.bundleIdentifier ||
|
|
24
|
+
cfg?.android?.package;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// expo-constants not installed (bare RN). Fall through.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!bundleId) {
|
|
31
|
+
try {
|
|
32
|
+
const { Platform, NativeModules } = require('react-native');
|
|
33
|
+
if (Platform.OS === 'ios') {
|
|
34
|
+
bundleId = NativeModules?.SettingsManager?.settings?.CFBundleIdentifier;
|
|
35
|
+
}
|
|
36
|
+
else if (Platform.OS === 'android') {
|
|
37
|
+
bundleId = NativeModules?.PlatformConstants?.Package;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// SettingsManager/PlatformConstants missing on some RN versions.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const out = {};
|
|
45
|
+
if (projectName)
|
|
46
|
+
out.projectName = projectName;
|
|
47
|
+
if (bundleId)
|
|
48
|
+
out.bundleId = bundleId;
|
|
49
|
+
if (projectPath)
|
|
50
|
+
out.projectPath = projectPath;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
5
53
|
/**
|
|
6
54
|
* Translate a raw Go-agent error into something a user can act on.
|
|
7
55
|
*
|
|
@@ -215,7 +263,7 @@ class P2PClient {
|
|
|
215
263
|
* via the BlackBox command channel.
|
|
216
264
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
217
265
|
*/
|
|
218
|
-
async reloadApp(mode = 'bundle') {
|
|
266
|
+
async reloadApp(mode = 'bundle', opts) {
|
|
219
267
|
// Default path: always rebuild a fresh Hermes bundle.
|
|
220
268
|
//
|
|
221
269
|
// Rationale: the SDK's common caller is a phone user who's not
|
|
@@ -242,13 +290,25 @@ class P2PClient {
|
|
|
242
290
|
// than surfacing the raw error, so the user never has to know
|
|
243
291
|
// Metro wasn't running.
|
|
244
292
|
}
|
|
293
|
+
// Auto-resolve identity if the caller didn't pass it. Reads from
|
|
294
|
+
// expo-constants when present (host can pin via app.json
|
|
295
|
+
// `expo.name` / `ios.bundleIdentifier` / `android.package`); falls
|
|
296
|
+
// back to react-native's NativeModules.SettingsManager.settings
|
|
297
|
+
// (iOS `CFBundleIdentifier`, `CFBundleName`) and Application
|
|
298
|
+
// (Android packageName). On the agent side these resolve to the
|
|
299
|
+
// matching MobileProject in the cached scan, so we don't need
|
|
300
|
+
// `yaver dev start` to have run on the host.
|
|
301
|
+
const identity = resolveAppIdentity(opts);
|
|
245
302
|
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
246
303
|
method: 'POST',
|
|
247
304
|
headers: {
|
|
248
305
|
Authorization: `Bearer ${this.authToken}`,
|
|
249
306
|
'Content-Type': 'application/json',
|
|
250
307
|
},
|
|
251
|
-
body: JSON.stringify({
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
mode: 'bundle',
|
|
310
|
+
...identity,
|
|
311
|
+
}),
|
|
252
312
|
});
|
|
253
313
|
if (!res.ok) {
|
|
254
314
|
const text = await res.text().catch(() => '');
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -155,18 +155,22 @@ class YaverFeedback {
|
|
|
155
155
|
}
|
|
156
156
|
else if (cmd.command === 'status') {
|
|
157
157
|
// Pipe agent progress pings to the UI. The FeedbackModal
|
|
158
|
-
// subscribes to this event and renders the message
|
|
159
|
-
// reload / build is in flight.
|
|
158
|
+
// subscribes to this event and renders the message + a
|
|
159
|
+
// progress bar while a reload / build is in flight.
|
|
160
160
|
const message = typeof cmd.data?.message === 'string'
|
|
161
161
|
? cmd.data.message
|
|
162
162
|
: '';
|
|
163
163
|
const phase = typeof cmd.data?.phase === 'string'
|
|
164
164
|
? cmd.data.phase
|
|
165
165
|
: '';
|
|
166
|
+
const progress = typeof cmd.data?.progress === 'number'
|
|
167
|
+
? Math.max(0, Math.min(1, cmd.data.progress))
|
|
168
|
+
: undefined;
|
|
166
169
|
const { DeviceEventEmitter } = require('react-native');
|
|
167
170
|
DeviceEventEmitter.emit('yaverFeedback:status', {
|
|
168
171
|
message,
|
|
169
172
|
phase,
|
|
173
|
+
progress,
|
|
170
174
|
at: Date.now(),
|
|
171
175
|
});
|
|
172
176
|
}
|
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.11",
|
|
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",
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -53,6 +53,7 @@ export const FeedbackModal: React.FC = () => {
|
|
|
53
53
|
const [action, setAction] = useState<ActionState>('idle');
|
|
54
54
|
const [error, setError] = useState<string | null>(null);
|
|
55
55
|
const [toast, setToast] = useState<string | null>(null);
|
|
56
|
+
const [progress, setProgress] = useState<number | null>(null);
|
|
56
57
|
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
|
|
57
58
|
const [lastVideo, setLastVideo] = useState<LastVideo | null>(null);
|
|
58
59
|
const mountedRef = useRef(true);
|
|
@@ -74,10 +75,18 @@ export const FeedbackModal: React.FC = () => {
|
|
|
74
75
|
// "stuck".
|
|
75
76
|
const statusSub = DeviceEventEmitter.addListener(
|
|
76
77
|
'yaverFeedback:status',
|
|
77
|
-
(payload: { message?: string; phase?: string }) => {
|
|
78
|
+
(payload: { message?: string; phase?: string; progress?: number }) => {
|
|
78
79
|
if (!mountedRef.current) return;
|
|
79
80
|
const msg = payload?.message || payload?.phase || '';
|
|
80
81
|
if (msg) setToast(msg);
|
|
82
|
+
if (typeof payload?.progress === 'number') {
|
|
83
|
+
setProgress(payload.progress);
|
|
84
|
+
}
|
|
85
|
+
// On final phases, fade the bar to 100% so the user sees
|
|
86
|
+
// completion before the modal auto-dismisses.
|
|
87
|
+
if (payload?.phase === 'done' || payload?.phase === 'error') {
|
|
88
|
+
setProgress(1);
|
|
89
|
+
}
|
|
81
90
|
},
|
|
82
91
|
);
|
|
83
92
|
return () => {
|
|
@@ -154,14 +163,25 @@ export const FeedbackModal: React.FC = () => {
|
|
|
154
163
|
const handleHotReload = useCallback(async () => {
|
|
155
164
|
setAction('hot-reloading');
|
|
156
165
|
setError(null);
|
|
166
|
+
setProgress(0);
|
|
167
|
+
setToast('Sending…');
|
|
157
168
|
try {
|
|
169
|
+
// Default mode: bundle. Always rebuilds via the agent regardless
|
|
170
|
+
// of Metro state. P2PClient.reloadApp auto-resolves projectName +
|
|
171
|
+
// bundleId from expo-constants / NativeModules so the agent can
|
|
172
|
+
// map this app to its MobileProject scan entry without needing
|
|
173
|
+
// `yaver dev start` to have been run.
|
|
158
174
|
await runWithReconnect(async (client) => {
|
|
159
|
-
await client.reloadApp('
|
|
175
|
+
await client.reloadApp('bundle');
|
|
160
176
|
});
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
// We don't auto-close here — the agent's BlackBox status pings
|
|
178
|
+
// will keep the modal updated, and the on-device YaverBundleLoader
|
|
179
|
+
// will reload the JS once the fresh bundle arrives. Modal stays
|
|
180
|
+
// up for a beat so the user sees the final progress state.
|
|
181
|
+
closeSoon(2500);
|
|
163
182
|
} catch (err: unknown) {
|
|
164
183
|
setError(err instanceof Error ? err.message : String(err));
|
|
184
|
+
setProgress(null);
|
|
165
185
|
} finally {
|
|
166
186
|
if (mountedRef.current) setAction('idle');
|
|
167
187
|
}
|
|
@@ -449,6 +469,16 @@ export const FeedbackModal: React.FC = () => {
|
|
|
449
469
|
busy={action === 'sending-video'}
|
|
450
470
|
/>
|
|
451
471
|
|
|
472
|
+
{progress !== null && (
|
|
473
|
+
<View style={styles.progressTrack}>
|
|
474
|
+
<View
|
|
475
|
+
style={[
|
|
476
|
+
styles.progressFill,
|
|
477
|
+
{ width: `${Math.round(progress * 100)}%` },
|
|
478
|
+
]}
|
|
479
|
+
/>
|
|
480
|
+
</View>
|
|
481
|
+
)}
|
|
452
482
|
{toast && <Text style={styles.toast}>{toast}</Text>}
|
|
453
483
|
{error && <Text style={styles.error}>{error}</Text>}
|
|
454
484
|
</Pressable>
|
|
@@ -550,6 +580,18 @@ const styles = StyleSheet.create({
|
|
|
550
580
|
fontSize: 15,
|
|
551
581
|
fontWeight: '700',
|
|
552
582
|
},
|
|
583
|
+
progressTrack: {
|
|
584
|
+
height: 6,
|
|
585
|
+
borderRadius: 3,
|
|
586
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
587
|
+
overflow: 'hidden',
|
|
588
|
+
marginTop: 4,
|
|
589
|
+
},
|
|
590
|
+
progressFill: {
|
|
591
|
+
height: '100%',
|
|
592
|
+
backgroundColor: '#818cf8',
|
|
593
|
+
borderRadius: 3,
|
|
594
|
+
},
|
|
553
595
|
toast: {
|
|
554
596
|
color: '#22c55e',
|
|
555
597
|
fontSize: 13,
|
package/src/LoginScreen.tsx
CHANGED
|
@@ -23,6 +23,88 @@ import {
|
|
|
23
23
|
type OAuthProvider,
|
|
24
24
|
} from './auth';
|
|
25
25
|
|
|
26
|
+
// ── ProviderLogo ─────────────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Mirrors the Yaver mobile app's login screen — Apple, Google, GitHub,
|
|
29
|
+
// GitLab, Microsoft each get their brand mark next to the label.
|
|
30
|
+
// Mobile uses `Ionicons` from `@expo/vector-icons`. We soft-require
|
|
31
|
+
// the same package so apps that have it (every Expo app does) get
|
|
32
|
+
// real logos automatically; bare-RN apps without the package fall
|
|
33
|
+
// back to a single-letter monogram in a tinted circle.
|
|
34
|
+
//
|
|
35
|
+
// Each entry holds: Ionicons name, fallback letter, brand colour,
|
|
36
|
+
// optional Apple-on-light invert (Apple's wordmark needs the
|
|
37
|
+
// alternate "logo-apple-appstore" / dark variant on dark backgrounds).
|
|
38
|
+
type ProviderTheme = {
|
|
39
|
+
ion: string;
|
|
40
|
+
letter: string;
|
|
41
|
+
fg: string;
|
|
42
|
+
bg: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PROVIDER_THEME: Record<OAuthProvider | 'apple', ProviderTheme> = {
|
|
46
|
+
apple: { ion: 'logo-apple', letter: '', fg: '#FFFFFF', bg: 'transparent' },
|
|
47
|
+
google: { ion: 'logo-google', letter: 'G', fg: '#EA4335', bg: 'transparent' },
|
|
48
|
+
github: { ion: 'logo-github', letter: 'G', fg: '#FFFFFF', bg: 'transparent' },
|
|
49
|
+
gitlab: { ion: 'logo-gitlab', letter: 'G', fg: '#FC6D26', bg: 'transparent' },
|
|
50
|
+
microsoft: { ion: 'logo-microsoft', letter: 'M', fg: '#00A4EF', bg: 'transparent' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ProviderLogo: React.FC<{ provider: OAuthProvider | 'apple' }> = ({
|
|
54
|
+
provider,
|
|
55
|
+
}) => {
|
|
56
|
+
const theme = PROVIDER_THEME[provider] ?? PROVIDER_THEME.apple;
|
|
57
|
+
// Soft-require @expo/vector-icons so the SDK works in bare-RN apps
|
|
58
|
+
// that don't ship Expo. If present, render the brand glyph; if
|
|
59
|
+
// not, render a coloured monogram fallback.
|
|
60
|
+
let Ionicons: React.ComponentType<{
|
|
61
|
+
name: string;
|
|
62
|
+
size?: number;
|
|
63
|
+
color?: string;
|
|
64
|
+
style?: object;
|
|
65
|
+
}> | null = null;
|
|
66
|
+
try {
|
|
67
|
+
const mod = require('@expo/vector-icons');
|
|
68
|
+
Ionicons = mod?.Ionicons ?? null;
|
|
69
|
+
} catch {
|
|
70
|
+
// package not installed — use letter fallback
|
|
71
|
+
}
|
|
72
|
+
if (Ionicons) {
|
|
73
|
+
return (
|
|
74
|
+
<Ionicons
|
|
75
|
+
name={theme.ion}
|
|
76
|
+
size={18}
|
|
77
|
+
color={theme.fg}
|
|
78
|
+
style={iconStyles.icon}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!theme.letter) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<View style={[iconStyles.fallback, { backgroundColor: theme.fg + '22' }]}>
|
|
87
|
+
<Text style={[iconStyles.fallbackText, { color: theme.fg }]}>{theme.letter}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const iconStyles = StyleSheet.create({
|
|
93
|
+
icon: { marginRight: 12 },
|
|
94
|
+
fallback: {
|
|
95
|
+
width: 22,
|
|
96
|
+
height: 22,
|
|
97
|
+
borderRadius: 11,
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
marginRight: 12,
|
|
101
|
+
},
|
|
102
|
+
fallbackText: {
|
|
103
|
+
fontSize: 12,
|
|
104
|
+
fontWeight: '700',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
26
108
|
export interface YaverLoginScreenProps {
|
|
27
109
|
/** Invoked once a session token is issued and the user is loaded. */
|
|
28
110
|
onLoggedIn: (token: string) => void;
|
|
@@ -125,7 +207,10 @@ export const YaverLoginScreen: React.FC<YaverLoginScreenProps> = ({
|
|
|
125
207
|
{busyProvider === id ? (
|
|
126
208
|
<ActivityIndicator color="#e0e0e0" />
|
|
127
209
|
) : (
|
|
128
|
-
<
|
|
210
|
+
<View style={styles.buttonContent}>
|
|
211
|
+
<ProviderLogo provider={id} />
|
|
212
|
+
<Text style={[styles.buttonText, styles.buttonTextWithIcon]}>{label}</Text>
|
|
213
|
+
</View>
|
|
129
214
|
)}
|
|
130
215
|
</Pressable>
|
|
131
216
|
);
|
|
@@ -295,6 +380,14 @@ const styles = StyleSheet.create({
|
|
|
295
380
|
},
|
|
296
381
|
buttonPressed: { opacity: 0.7 },
|
|
297
382
|
buttonText: { color: '#e0e0e0', fontSize: 15, fontWeight: '600' },
|
|
383
|
+
buttonContent: {
|
|
384
|
+
flexDirection: 'row',
|
|
385
|
+
alignItems: 'center',
|
|
386
|
+
justifyContent: 'center',
|
|
387
|
+
},
|
|
388
|
+
buttonTextWithIcon: {
|
|
389
|
+
// No extra spacing — ProviderLogo provides its own marginRight.
|
|
390
|
+
},
|
|
298
391
|
divider: {
|
|
299
392
|
flexDirection: 'row',
|
|
300
393
|
alignItems: 'center',
|
package/src/P2PClient.ts
CHANGED
|
@@ -7,6 +7,56 @@ export interface FeedbackEvent {
|
|
|
7
7
|
data: any;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Try to resolve `{projectName, bundleId}` for the running app so the
|
|
12
|
+
* agent can map the reload request to a MobileProject in its scan
|
|
13
|
+
* cache. Order: caller-supplied opts → Expo Constants → react-native
|
|
14
|
+
* NativeModules. None of the lookups throw — missing data just means
|
|
15
|
+
* the agent will fall back to its own dev-server resolution.
|
|
16
|
+
*/
|
|
17
|
+
function resolveAppIdentity(opts?: {
|
|
18
|
+
projectName?: string;
|
|
19
|
+
bundleId?: string;
|
|
20
|
+
projectPath?: string;
|
|
21
|
+
}): { projectName?: string; bundleId?: string; projectPath?: string } {
|
|
22
|
+
let projectName = opts?.projectName;
|
|
23
|
+
let bundleId = opts?.bundleId;
|
|
24
|
+
const projectPath = opts?.projectPath;
|
|
25
|
+
|
|
26
|
+
if (!projectName || !bundleId) {
|
|
27
|
+
try {
|
|
28
|
+
const Constants = require('expo-constants').default ?? require('expo-constants');
|
|
29
|
+
const cfg = Constants?.expoConfig ?? Constants?.manifest ?? {};
|
|
30
|
+
projectName = projectName || cfg?.name;
|
|
31
|
+
bundleId =
|
|
32
|
+
bundleId ||
|
|
33
|
+
cfg?.ios?.bundleIdentifier ||
|
|
34
|
+
cfg?.android?.package;
|
|
35
|
+
} catch {
|
|
36
|
+
// expo-constants not installed (bare RN). Fall through.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!bundleId) {
|
|
41
|
+
try {
|
|
42
|
+
const { Platform, NativeModules } = require('react-native');
|
|
43
|
+
if (Platform.OS === 'ios') {
|
|
44
|
+
bundleId = NativeModules?.SettingsManager?.settings?.CFBundleIdentifier;
|
|
45
|
+
} else if (Platform.OS === 'android') {
|
|
46
|
+
bundleId = NativeModules?.PlatformConstants?.Package;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// SettingsManager/PlatformConstants missing on some RN versions.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const out: { projectName?: string; bundleId?: string; projectPath?: string } = {};
|
|
54
|
+
if (projectName) out.projectName = projectName;
|
|
55
|
+
if (bundleId) out.bundleId = bundleId;
|
|
56
|
+
if (projectPath) out.projectPath = projectPath;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
10
60
|
/**
|
|
11
61
|
* Translate a raw Go-agent error into something a user can act on.
|
|
12
62
|
*
|
|
@@ -256,7 +306,10 @@ export class P2PClient {
|
|
|
256
306
|
* via the BlackBox command channel.
|
|
257
307
|
* @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
|
|
258
308
|
*/
|
|
259
|
-
async reloadApp(
|
|
309
|
+
async reloadApp(
|
|
310
|
+
mode: 'dev' | 'bundle' = 'bundle',
|
|
311
|
+
opts?: { projectName?: string; bundleId?: string; projectPath?: string },
|
|
312
|
+
): Promise<{ ok: boolean }> {
|
|
260
313
|
// Default path: always rebuild a fresh Hermes bundle.
|
|
261
314
|
//
|
|
262
315
|
// Rationale: the SDK's common caller is a phone user who's not
|
|
@@ -284,13 +337,26 @@ export class P2PClient {
|
|
|
284
337
|
// Metro wasn't running.
|
|
285
338
|
}
|
|
286
339
|
|
|
340
|
+
// Auto-resolve identity if the caller didn't pass it. Reads from
|
|
341
|
+
// expo-constants when present (host can pin via app.json
|
|
342
|
+
// `expo.name` / `ios.bundleIdentifier` / `android.package`); falls
|
|
343
|
+
// back to react-native's NativeModules.SettingsManager.settings
|
|
344
|
+
// (iOS `CFBundleIdentifier`, `CFBundleName`) and Application
|
|
345
|
+
// (Android packageName). On the agent side these resolve to the
|
|
346
|
+
// matching MobileProject in the cached scan, so we don't need
|
|
347
|
+
// `yaver dev start` to have run on the host.
|
|
348
|
+
const identity = resolveAppIdentity(opts);
|
|
349
|
+
|
|
287
350
|
const res = await fetch(`${this.baseUrl}/dev/reload-app`, {
|
|
288
351
|
method: 'POST',
|
|
289
352
|
headers: {
|
|
290
353
|
Authorization: `Bearer ${this.authToken}`,
|
|
291
354
|
'Content-Type': 'application/json',
|
|
292
355
|
},
|
|
293
|
-
body: JSON.stringify({
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
mode: 'bundle',
|
|
358
|
+
...identity,
|
|
359
|
+
}),
|
|
294
360
|
});
|
|
295
361
|
if (!res.ok) {
|
|
296
362
|
const text = await res.text().catch(() => '');
|
package/src/YaverFeedback.ts
CHANGED
|
@@ -167,8 +167,8 @@ export class YaverFeedback {
|
|
|
167
167
|
}
|
|
168
168
|
} else if (cmd.command === 'status') {
|
|
169
169
|
// Pipe agent progress pings to the UI. The FeedbackModal
|
|
170
|
-
// subscribes to this event and renders the message
|
|
171
|
-
// reload / build is in flight.
|
|
170
|
+
// subscribes to this event and renders the message + a
|
|
171
|
+
// progress bar while a reload / build is in flight.
|
|
172
172
|
const message =
|
|
173
173
|
typeof cmd.data?.message === 'string'
|
|
174
174
|
? (cmd.data.message as string)
|
|
@@ -177,10 +177,15 @@ export class YaverFeedback {
|
|
|
177
177
|
typeof cmd.data?.phase === 'string'
|
|
178
178
|
? (cmd.data.phase as string)
|
|
179
179
|
: '';
|
|
180
|
+
const progress =
|
|
181
|
+
typeof cmd.data?.progress === 'number'
|
|
182
|
+
? Math.max(0, Math.min(1, cmd.data.progress as number))
|
|
183
|
+
: undefined;
|
|
180
184
|
const { DeviceEventEmitter } = require('react-native');
|
|
181
185
|
DeviceEventEmitter.emit('yaverFeedback:status', {
|
|
182
186
|
message,
|
|
183
187
|
phase,
|
|
188
|
+
progress,
|
|
184
189
|
at: Date.now(),
|
|
185
190
|
});
|
|
186
191
|
}
|