clawdex-mobile 1.3.2 → 2.0.0
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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/npm-release.yml +18 -0
- package/AGENTS.md +3 -3
- package/README.md +101 -541
- package/apps/mobile/.env.example +1 -2
- package/apps/mobile/App.tsx +261 -68
- package/apps/mobile/app.json +31 -5
- package/apps/mobile/assets/brand/splash-icon-white.png +0 -0
- package/apps/mobile/eas.json +30 -0
- package/apps/mobile/package.json +22 -21
- package/apps/mobile/plugins/withAndroidCleartextTraffic.js +14 -0
- package/apps/mobile/src/api/__tests__/ws.test.ts +44 -6
- package/apps/mobile/src/api/chatMapping.ts +48 -8
- package/apps/mobile/src/api/client.ts +6 -0
- package/apps/mobile/src/api/types.ts +11 -0
- package/apps/mobile/src/api/ws.ts +52 -10
- package/apps/mobile/src/bridgeUrl.ts +105 -0
- package/apps/mobile/src/components/ActivityBar.tsx +32 -13
- package/apps/mobile/src/components/ChatHeader.tsx +3 -2
- package/apps/mobile/src/components/ChatInput.tsx +246 -91
- package/apps/mobile/src/components/ChatMessage.tsx +108 -4
- package/apps/mobile/src/config.ts +11 -29
- package/apps/mobile/src/hooks/useVoiceRecorder.ts +264 -0
- package/apps/mobile/src/navigation/DrawerContent.tsx +18 -8
- package/apps/mobile/src/screens/GitScreen.tsx +1 -1
- package/apps/mobile/src/screens/MainScreen.tsx +906 -268
- package/apps/mobile/src/screens/OnboardingScreen.tsx +1132 -0
- package/apps/mobile/src/screens/PrivacyScreen.tsx +1 -1
- package/apps/mobile/src/screens/SettingsScreen.tsx +65 -1
- package/apps/mobile/src/screens/TerminalScreen.tsx +1 -1
- package/apps/mobile/src/screens/TermsScreen.tsx +1 -1
- package/docs/app-review-notes.md +7 -2
- package/docs/eas-builds.md +91 -0
- package/docs/realtime-streaming-limitations.md +84 -0
- package/docs/setup-and-operations.md +239 -0
- package/docs/troubleshooting.md +121 -0
- package/docs/voice-transcription.md +87 -0
- package/package.json +8 -16
- package/scripts/setup-secure-dev.sh +122 -8
- package/scripts/setup-wizard.sh +342 -122
- package/scripts/start-bridge-secure.sh +7 -1
- package/scripts/sync-versions.js +63 -0
- package/services/rust-bridge/.env.example +1 -1
- package/services/rust-bridge/Cargo.lock +1104 -23
- package/services/rust-bridge/Cargo.toml +3 -1
- package/services/rust-bridge/package.json +1 -1
- package/services/rust-bridge/src/main.rs +587 -12
- package/apps/mobile/metro.config.js +0 -3
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { CameraView, type BarcodeScanningResult, useCameraPermissions } from 'expo-camera';
|
|
4
|
+
import {
|
|
5
|
+
ActivityIndicator,
|
|
6
|
+
KeyboardAvoidingView,
|
|
7
|
+
Modal,
|
|
8
|
+
Platform,
|
|
9
|
+
Pressable,
|
|
10
|
+
ScrollView,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Text,
|
|
13
|
+
TextInput,
|
|
14
|
+
View,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
isInsecureRemoteUrl,
|
|
20
|
+
normalizeBridgeUrlInput,
|
|
21
|
+
toBridgeHealthUrl,
|
|
22
|
+
} from '../bridgeUrl';
|
|
23
|
+
import { HostBridgeWsClient } from '../api/ws';
|
|
24
|
+
import { BrandMark } from '../components/BrandMark';
|
|
25
|
+
import { colors, radius, spacing, typography } from '../theme';
|
|
26
|
+
|
|
27
|
+
type OnboardingMode = 'initial' | 'edit';
|
|
28
|
+
|
|
29
|
+
interface OnboardingScreenProps {
|
|
30
|
+
mode?: OnboardingMode;
|
|
31
|
+
initialBridgeUrl?: string | null;
|
|
32
|
+
initialBridgeToken?: string | null;
|
|
33
|
+
allowInsecureRemoteBridge?: boolean;
|
|
34
|
+
allowQueryTokenAuth?: boolean;
|
|
35
|
+
onSave: (bridgeUrl: string, bridgeToken: string | null) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ConnectionCheck =
|
|
40
|
+
| { kind: 'idle' }
|
|
41
|
+
| { kind: 'success'; message: string }
|
|
42
|
+
| { kind: 'error'; message: string };
|
|
43
|
+
type OnboardingStep = 'intro' | 'connect';
|
|
44
|
+
type PairingPayload = { bridgeToken: string; bridgeUrl?: string };
|
|
45
|
+
type BridgeModePreset = 'local' | 'tailscale';
|
|
46
|
+
|
|
47
|
+
const LOCAL_EXAMPLE_URL = 'http://192.168.1.20:8787';
|
|
48
|
+
const TAILSCALE_EXAMPLE_URL = 'http://100.101.102.103:8787';
|
|
49
|
+
|
|
50
|
+
export function OnboardingScreen({
|
|
51
|
+
mode = 'initial',
|
|
52
|
+
initialBridgeUrl,
|
|
53
|
+
initialBridgeToken,
|
|
54
|
+
allowInsecureRemoteBridge = false,
|
|
55
|
+
allowQueryTokenAuth = false,
|
|
56
|
+
onSave,
|
|
57
|
+
onCancel,
|
|
58
|
+
}: OnboardingScreenProps) {
|
|
59
|
+
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep>(
|
|
60
|
+
mode === 'initial' ? 'intro' : 'connect'
|
|
61
|
+
);
|
|
62
|
+
const [urlInput, setUrlInput] = useState(initialBridgeUrl ?? '');
|
|
63
|
+
const [tokenInput, setTokenInput] = useState(initialBridgeToken ?? '');
|
|
64
|
+
const [tokenHidden, setTokenHidden] = useState(true);
|
|
65
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
66
|
+
const [checkingConnection, setCheckingConnection] = useState(false);
|
|
67
|
+
const [connectionCheck, setConnectionCheck] = useState<ConnectionCheck>({ kind: 'idle' });
|
|
68
|
+
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
|
|
69
|
+
const [scannerVisible, setScannerVisible] = useState(false);
|
|
70
|
+
const [scannerError, setScannerError] = useState<string | null>(null);
|
|
71
|
+
const [scannerLocked, setScannerLocked] = useState(false);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setOnboardingStep(mode === 'initial' ? 'intro' : 'connect');
|
|
75
|
+
}, [mode]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setUrlInput(initialBridgeUrl ?? '');
|
|
79
|
+
}, [initialBridgeUrl]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setTokenInput(initialBridgeToken ?? '');
|
|
83
|
+
}, [initialBridgeToken]);
|
|
84
|
+
|
|
85
|
+
const showIntroStep = mode === 'initial' && onboardingStep === 'intro';
|
|
86
|
+
|
|
87
|
+
const normalizedBridgeUrl = useMemo(
|
|
88
|
+
() => normalizeBridgeUrlInput(urlInput),
|
|
89
|
+
[urlInput]
|
|
90
|
+
);
|
|
91
|
+
const insecureRemoteWarning = useMemo(() => {
|
|
92
|
+
if (!normalizedBridgeUrl || allowInsecureRemoteBridge) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return isInsecureRemoteUrl(normalizedBridgeUrl)
|
|
97
|
+
? 'This is plain HTTP over a non-private host. Use HTTPS/WSS when crossing untrusted networks.'
|
|
98
|
+
: null;
|
|
99
|
+
}, [allowInsecureRemoteBridge, normalizedBridgeUrl]);
|
|
100
|
+
|
|
101
|
+
const modeTitle = mode === 'edit' ? 'Update Bridge URL' : 'Connect Your Bridge';
|
|
102
|
+
const modeDescription =
|
|
103
|
+
mode === 'edit'
|
|
104
|
+
? 'Switch to another host bridge without rebuilding the app.'
|
|
105
|
+
: 'Set the host bridge URL once, then use Codex from LAN, VPN, or Tailscale.';
|
|
106
|
+
|
|
107
|
+
const validateInput = useCallback((): { bridgeUrl: string; bridgeToken: string } | null => {
|
|
108
|
+
const normalized = normalizeBridgeUrlInput(urlInput);
|
|
109
|
+
if (!normalized) {
|
|
110
|
+
setFormError('Enter a valid URL. Example: http://100.101.102.103:8787');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalizedToken = tokenInput.trim();
|
|
115
|
+
if (!normalizedToken) {
|
|
116
|
+
setFormError('Bridge token is required.');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setFormError(null);
|
|
121
|
+
return { bridgeUrl: normalized, bridgeToken: normalizedToken };
|
|
122
|
+
}, [tokenInput, urlInput]);
|
|
123
|
+
|
|
124
|
+
const normalizeTokenInput = useCallback((value: string): string | null => {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const runConnectionCheck = useCallback(
|
|
130
|
+
async (normalized: string, token: string | null): Promise<boolean> => {
|
|
131
|
+
setCheckingConnection(true);
|
|
132
|
+
setConnectionCheck({ kind: 'idle' });
|
|
133
|
+
|
|
134
|
+
let probeClient: HostBridgeWsClient | null = null;
|
|
135
|
+
let healthCheckError: string | null = null;
|
|
136
|
+
try {
|
|
137
|
+
const headers: Record<string, string> | undefined = token
|
|
138
|
+
? { Authorization: `Bearer ${token}` }
|
|
139
|
+
: undefined;
|
|
140
|
+
const healthUrl = toBridgeHealthUrl(normalized);
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(healthUrl, { method: 'GET', headers });
|
|
143
|
+
if (response.status !== 200) {
|
|
144
|
+
healthCheckError = `health returned ${response.status}`;
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
healthCheckError = (error as Error).message || 'network request failed';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
probeClient = new HostBridgeWsClient(normalized, {
|
|
151
|
+
authToken: token,
|
|
152
|
+
allowQueryTokenAuth,
|
|
153
|
+
requestTimeoutMs: 10_000,
|
|
154
|
+
});
|
|
155
|
+
const rpcHealth = await probeClient.request<{ status?: string }>('bridge/health/read');
|
|
156
|
+
if (rpcHealth?.status !== 'ok') {
|
|
157
|
+
throw new Error('authenticated RPC probe returned unexpected response');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setConnectionCheck({
|
|
161
|
+
kind: 'success',
|
|
162
|
+
message: healthCheckError
|
|
163
|
+
? 'Connected. Authenticated RPC verified; /health endpoint did not return 200.'
|
|
164
|
+
: 'Connected. URL and token both verified.',
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const baseMessage = (error as Error).message || 'request failed';
|
|
169
|
+
const hint =
|
|
170
|
+
Platform.OS === 'android' && baseMessage.includes('Network request failed')
|
|
171
|
+
? ' (If using Android emulator, use http://10.0.2.2:8787 for localhost bridge.)'
|
|
172
|
+
: '';
|
|
173
|
+
setConnectionCheck({
|
|
174
|
+
kind: 'error',
|
|
175
|
+
message: `Bridge verification failed: ${baseMessage}${hint}`,
|
|
176
|
+
});
|
|
177
|
+
return false;
|
|
178
|
+
} finally {
|
|
179
|
+
probeClient?.disconnect();
|
|
180
|
+
setCheckingConnection(false);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
[allowQueryTokenAuth]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const handleSave = useCallback(async () => {
|
|
187
|
+
const validated = validateInput();
|
|
188
|
+
if (!validated) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const normalizedToken = normalizeTokenInput(validated.bridgeToken);
|
|
193
|
+
const ok = await runConnectionCheck(validated.bridgeUrl, normalizedToken);
|
|
194
|
+
if (!ok) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
onSave(validated.bridgeUrl, normalizedToken);
|
|
199
|
+
}, [normalizeTokenInput, onSave, runConnectionCheck, validateInput]);
|
|
200
|
+
|
|
201
|
+
const handleConnectionCheck = useCallback(async () => {
|
|
202
|
+
const validated = validateInput();
|
|
203
|
+
if (!validated) {
|
|
204
|
+
setConnectionCheck({ kind: 'idle' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const normalizedToken = normalizeTokenInput(validated.bridgeToken);
|
|
209
|
+
await runConnectionCheck(validated.bridgeUrl, normalizedToken);
|
|
210
|
+
}, [normalizeTokenInput, runConnectionCheck, validateInput]);
|
|
211
|
+
|
|
212
|
+
const applyPreset = useCallback((value: string) => {
|
|
213
|
+
setUrlInput(value);
|
|
214
|
+
setFormError(null);
|
|
215
|
+
setConnectionCheck({ kind: 'idle' });
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
const applyModePreset = useCallback((preset: BridgeModePreset) => {
|
|
219
|
+
applyPreset(preset === 'local' ? LOCAL_EXAMPLE_URL : TAILSCALE_EXAMPLE_URL);
|
|
220
|
+
}, [applyPreset]);
|
|
221
|
+
|
|
222
|
+
const goToConnectStep = useCallback(() => {
|
|
223
|
+
setOnboardingStep('connect');
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
const closeScanner = useCallback(() => {
|
|
227
|
+
setScannerVisible(false);
|
|
228
|
+
setScannerLocked(false);
|
|
229
|
+
setScannerError(null);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
const openScanner = useCallback(async () => {
|
|
233
|
+
setFormError(null);
|
|
234
|
+
setConnectionCheck({ kind: 'idle' });
|
|
235
|
+
setScannerError(null);
|
|
236
|
+
|
|
237
|
+
if (!cameraPermission?.granted) {
|
|
238
|
+
const result = await requestCameraPermission();
|
|
239
|
+
if (!result.granted) {
|
|
240
|
+
setFormError('Camera permission is required to scan bridge QR.');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setScannerLocked(false);
|
|
246
|
+
setScannerVisible(true);
|
|
247
|
+
}, [cameraPermission?.granted, requestCameraPermission]);
|
|
248
|
+
|
|
249
|
+
const applyPairingPayload = useCallback((pairing: PairingPayload) => {
|
|
250
|
+
if (pairing.bridgeUrl) {
|
|
251
|
+
setUrlInput(pairing.bridgeUrl);
|
|
252
|
+
}
|
|
253
|
+
setTokenInput(pairing.bridgeToken);
|
|
254
|
+
setFormError(null);
|
|
255
|
+
setConnectionCheck({ kind: 'idle' });
|
|
256
|
+
setScannerError(null);
|
|
257
|
+
setScannerLocked(false);
|
|
258
|
+
setScannerVisible(false);
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
const handleBarcodeScanned = useCallback(
|
|
262
|
+
(result: BarcodeScanningResult) => {
|
|
263
|
+
if (scannerLocked) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setScannerLocked(true);
|
|
268
|
+
const pairing = parsePairingPayload(result.data);
|
|
269
|
+
if (!pairing) {
|
|
270
|
+
setScannerError('QR code is not a valid Clawdex bridge pairing code.');
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
setScannerLocked(false);
|
|
273
|
+
}, 1200);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
applyPairingPayload(pairing);
|
|
278
|
+
},
|
|
279
|
+
[applyPairingPayload, scannerLocked]
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<View style={styles.container}>
|
|
284
|
+
<SafeAreaView style={styles.safeArea}>
|
|
285
|
+
<KeyboardAvoidingView
|
|
286
|
+
behavior={Platform.select({ ios: 'padding', default: undefined })}
|
|
287
|
+
style={styles.keyboardAvoiding}
|
|
288
|
+
>
|
|
289
|
+
{showIntroStep ? (
|
|
290
|
+
<View style={styles.introRoot}>
|
|
291
|
+
<View style={styles.introBrandRow}>
|
|
292
|
+
<BrandMark size={24} />
|
|
293
|
+
<Text style={styles.introBrandName}>Clawdex</Text>
|
|
294
|
+
</View>
|
|
295
|
+
<View style={styles.introMain}>
|
|
296
|
+
<View style={styles.heroCard}>
|
|
297
|
+
<View style={styles.heroTopRow}>
|
|
298
|
+
<View style={styles.heroIconWrap}>
|
|
299
|
+
<Ionicons name="phone-portrait-outline" size={20} color={colors.textPrimary} />
|
|
300
|
+
</View>
|
|
301
|
+
</View>
|
|
302
|
+
<Text style={styles.heroTitle}>Codex on mobile</Text>
|
|
303
|
+
<Text style={styles.heroDescription}>
|
|
304
|
+
Run your host-side Codex workflows from your phone across LAN, VPN, or Tailscale.
|
|
305
|
+
</Text>
|
|
306
|
+
</View>
|
|
307
|
+
|
|
308
|
+
<View style={[styles.formCard, styles.introFeaturesCard]}>
|
|
309
|
+
<ScrollView
|
|
310
|
+
style={styles.introFeaturesList}
|
|
311
|
+
contentContainerStyle={styles.introFeaturesListContent}
|
|
312
|
+
showsVerticalScrollIndicator
|
|
313
|
+
>
|
|
314
|
+
<Text style={styles.introSectionTitle}>What You Can Do</Text>
|
|
315
|
+
<IntroFeatureRow
|
|
316
|
+
icon="chatbubble-ellipses-outline"
|
|
317
|
+
title="Continue threads"
|
|
318
|
+
description="Follow active chats and start new runs from your phone."
|
|
319
|
+
/>
|
|
320
|
+
<IntroFeatureRow
|
|
321
|
+
icon="pulse-outline"
|
|
322
|
+
title="Track run progress"
|
|
323
|
+
description="See live status and streaming updates as Codex works."
|
|
324
|
+
/>
|
|
325
|
+
<IntroFeatureRow
|
|
326
|
+
icon="git-branch-outline"
|
|
327
|
+
title="Handle git tasks"
|
|
328
|
+
description="Review status, diffs, and commits for chat workspaces."
|
|
329
|
+
/>
|
|
330
|
+
<IntroFeatureRow
|
|
331
|
+
icon="mic-outline"
|
|
332
|
+
title="Talk to Codex"
|
|
333
|
+
description="Use voice input to speak your prompts directly from mobile."
|
|
334
|
+
/>
|
|
335
|
+
<IntroFeatureRow
|
|
336
|
+
icon="attach-outline"
|
|
337
|
+
title="Share files and images"
|
|
338
|
+
description="Attach workspace files and phone media to your prompts."
|
|
339
|
+
/>
|
|
340
|
+
<IntroFeatureRow
|
|
341
|
+
icon="shield-checkmark-outline"
|
|
342
|
+
title="Approve actions"
|
|
343
|
+
description="Review and approve command and file changes in-app."
|
|
344
|
+
/>
|
|
345
|
+
</ScrollView>
|
|
346
|
+
</View>
|
|
347
|
+
</View>
|
|
348
|
+
<View style={styles.introFooter}>
|
|
349
|
+
<Pressable
|
|
350
|
+
onPress={goToConnectStep}
|
|
351
|
+
style={({ pressed }) => [
|
|
352
|
+
styles.introNextButton,
|
|
353
|
+
pressed && styles.introNextButtonPressed,
|
|
354
|
+
]}
|
|
355
|
+
>
|
|
356
|
+
<Text style={styles.introNextButtonText}>Next</Text>
|
|
357
|
+
<Ionicons name="arrow-forward" size={19} color={colors.black} />
|
|
358
|
+
</Pressable>
|
|
359
|
+
</View>
|
|
360
|
+
</View>
|
|
361
|
+
) : (
|
|
362
|
+
<ScrollView
|
|
363
|
+
style={styles.scroll}
|
|
364
|
+
contentContainerStyle={styles.scrollContent}
|
|
365
|
+
keyboardShouldPersistTaps="handled"
|
|
366
|
+
>
|
|
367
|
+
<View style={styles.heroCard}>
|
|
368
|
+
<View style={styles.heroTopRow}>
|
|
369
|
+
<View style={styles.heroIconWrap}>
|
|
370
|
+
<Ionicons name="hardware-chip-outline" size={20} color={colors.textPrimary} />
|
|
371
|
+
</View>
|
|
372
|
+
{mode === 'edit' && onCancel ? (
|
|
373
|
+
<Pressable
|
|
374
|
+
onPress={onCancel}
|
|
375
|
+
hitSlop={8}
|
|
376
|
+
style={({ pressed }) => [styles.cancelBtn, pressed && styles.cancelBtnPressed]}
|
|
377
|
+
>
|
|
378
|
+
<Ionicons name="close" size={16} color={colors.textPrimary} />
|
|
379
|
+
</Pressable>
|
|
380
|
+
) : null}
|
|
381
|
+
</View>
|
|
382
|
+
<Text style={styles.heroTitle}>{modeTitle}</Text>
|
|
383
|
+
<Text style={styles.heroDescription}>{modeDescription}</Text>
|
|
384
|
+
</View>
|
|
385
|
+
|
|
386
|
+
<View style={styles.formCard}>
|
|
387
|
+
<Text style={styles.label}>Bridge Mode</Text>
|
|
388
|
+
<View style={styles.modeRow}>
|
|
389
|
+
<Pressable
|
|
390
|
+
onPress={() => applyModePreset('local')}
|
|
391
|
+
style={({ pressed }) => [
|
|
392
|
+
styles.modeButton,
|
|
393
|
+
pressed && styles.modeButtonPressed,
|
|
394
|
+
]}
|
|
395
|
+
>
|
|
396
|
+
<Ionicons name="wifi-outline" size={16} color={colors.textPrimary} />
|
|
397
|
+
<Text style={styles.modeButtonText}>Local (LAN)</Text>
|
|
398
|
+
</Pressable>
|
|
399
|
+
<Pressable
|
|
400
|
+
onPress={() => applyModePreset('tailscale')}
|
|
401
|
+
style={({ pressed }) => [
|
|
402
|
+
styles.modeButton,
|
|
403
|
+
pressed && styles.modeButtonPressed,
|
|
404
|
+
]}
|
|
405
|
+
>
|
|
406
|
+
<Ionicons name="shield-outline" size={16} color={colors.textPrimary} />
|
|
407
|
+
<Text style={styles.modeButtonText}>Tailscale</Text>
|
|
408
|
+
</Pressable>
|
|
409
|
+
</View>
|
|
410
|
+
<Text style={styles.helperText}>
|
|
411
|
+
Pick the same mode used while starting the bridge, then adjust the IP if needed.
|
|
412
|
+
</Text>
|
|
413
|
+
|
|
414
|
+
<Text style={styles.label}>Bridge URL</Text>
|
|
415
|
+
<TextInput
|
|
416
|
+
value={urlInput}
|
|
417
|
+
onChangeText={(value) => {
|
|
418
|
+
setUrlInput(value);
|
|
419
|
+
setFormError(null);
|
|
420
|
+
setConnectionCheck({ kind: 'idle' });
|
|
421
|
+
}}
|
|
422
|
+
autoCapitalize="none"
|
|
423
|
+
autoCorrect={false}
|
|
424
|
+
keyboardType="url"
|
|
425
|
+
placeholder="http://100.101.102.103:8787"
|
|
426
|
+
placeholderTextColor={colors.textMuted}
|
|
427
|
+
style={styles.input}
|
|
428
|
+
returnKeyType="done"
|
|
429
|
+
onSubmitEditing={() => {
|
|
430
|
+
void handleSave();
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
<View style={styles.tokenHeaderRow}>
|
|
434
|
+
<Text style={styles.label}>Bridge Token</Text>
|
|
435
|
+
<Text style={styles.optionalLabel}>Required</Text>
|
|
436
|
+
</View>
|
|
437
|
+
<View style={styles.tokenInputWrap}>
|
|
438
|
+
<TextInput
|
|
439
|
+
value={tokenInput}
|
|
440
|
+
onChangeText={(value) => {
|
|
441
|
+
setTokenInput(value);
|
|
442
|
+
setConnectionCheck({ kind: 'idle' });
|
|
443
|
+
}}
|
|
444
|
+
autoCapitalize="none"
|
|
445
|
+
autoCorrect={false}
|
|
446
|
+
keyboardType="default"
|
|
447
|
+
placeholder="Paste bridge token"
|
|
448
|
+
placeholderTextColor={colors.textMuted}
|
|
449
|
+
style={[styles.input, styles.tokenInput]}
|
|
450
|
+
secureTextEntry={tokenHidden}
|
|
451
|
+
returnKeyType="done"
|
|
452
|
+
onSubmitEditing={() => {
|
|
453
|
+
void handleSave();
|
|
454
|
+
}}
|
|
455
|
+
/>
|
|
456
|
+
<Pressable
|
|
457
|
+
onPress={() => setTokenHidden((prev) => !prev)}
|
|
458
|
+
style={({ pressed }) => [
|
|
459
|
+
styles.tokenRevealBtn,
|
|
460
|
+
pressed && styles.tokenRevealBtnPressed,
|
|
461
|
+
]}
|
|
462
|
+
>
|
|
463
|
+
<Ionicons
|
|
464
|
+
name={tokenHidden ? 'eye-outline' : 'eye-off-outline'}
|
|
465
|
+
size={16}
|
|
466
|
+
color={colors.textSecondary}
|
|
467
|
+
/>
|
|
468
|
+
<Text style={styles.tokenRevealBtnText}>
|
|
469
|
+
{tokenHidden ? 'Show' : 'Hide'}
|
|
470
|
+
</Text>
|
|
471
|
+
</Pressable>
|
|
472
|
+
</View>
|
|
473
|
+
<Pressable
|
|
474
|
+
onPress={() => {
|
|
475
|
+
void openScanner();
|
|
476
|
+
}}
|
|
477
|
+
style={({ pressed }) => [
|
|
478
|
+
styles.scanButton,
|
|
479
|
+
pressed && styles.scanButtonPressed,
|
|
480
|
+
]}
|
|
481
|
+
>
|
|
482
|
+
<Ionicons name="qr-code-outline" size={16} color={colors.textPrimary} />
|
|
483
|
+
<Text style={styles.scanButtonText}>Scan Bridge QR</Text>
|
|
484
|
+
</Pressable>
|
|
485
|
+
<Text style={styles.helperText}>
|
|
486
|
+
URL supports `http`, `https`, `ws`, and `wss`. `/rpc` is added automatically.
|
|
487
|
+
</Text>
|
|
488
|
+
|
|
489
|
+
{normalizedBridgeUrl ? (
|
|
490
|
+
<View style={styles.previewWrap}>
|
|
491
|
+
<Text style={styles.previewLabel}>Normalized URL</Text>
|
|
492
|
+
<Text selectable style={styles.previewValue}>
|
|
493
|
+
{normalizedBridgeUrl}
|
|
494
|
+
</Text>
|
|
495
|
+
</View>
|
|
496
|
+
) : null}
|
|
497
|
+
|
|
498
|
+
{insecureRemoteWarning ? (
|
|
499
|
+
<Text style={styles.warningText}>{insecureRemoteWarning}</Text>
|
|
500
|
+
) : null}
|
|
501
|
+
|
|
502
|
+
{formError ? <Text style={styles.errorText}>{formError}</Text> : null}
|
|
503
|
+
{connectionCheck.kind === 'success' ? (
|
|
504
|
+
<Text style={styles.successText}>{connectionCheck.message}</Text>
|
|
505
|
+
) : null}
|
|
506
|
+
{connectionCheck.kind === 'error' ? (
|
|
507
|
+
<Text style={styles.errorText}>{connectionCheck.message}</Text>
|
|
508
|
+
) : null}
|
|
509
|
+
|
|
510
|
+
<View style={styles.actionRow}>
|
|
511
|
+
<Pressable
|
|
512
|
+
onPress={() => {
|
|
513
|
+
void handleConnectionCheck();
|
|
514
|
+
}}
|
|
515
|
+
disabled={checkingConnection}
|
|
516
|
+
style={({ pressed }) => [
|
|
517
|
+
styles.secondaryButton,
|
|
518
|
+
pressed && !checkingConnection && styles.secondaryButtonPressed,
|
|
519
|
+
checkingConnection && styles.secondaryButtonDisabled,
|
|
520
|
+
]}
|
|
521
|
+
>
|
|
522
|
+
{checkingConnection ? (
|
|
523
|
+
<ActivityIndicator size="small" color={colors.textPrimary} />
|
|
524
|
+
) : (
|
|
525
|
+
<Ionicons name="pulse-outline" size={16} color={colors.textPrimary} />
|
|
526
|
+
)}
|
|
527
|
+
<Text style={styles.secondaryButtonText}>Test Connection</Text>
|
|
528
|
+
</Pressable>
|
|
529
|
+
<Pressable
|
|
530
|
+
onPress={() => {
|
|
531
|
+
void handleSave();
|
|
532
|
+
}}
|
|
533
|
+
disabled={checkingConnection}
|
|
534
|
+
style={({ pressed }) => [
|
|
535
|
+
styles.primaryButton,
|
|
536
|
+
pressed && !checkingConnection && styles.primaryButtonPressed,
|
|
537
|
+
checkingConnection && styles.primaryButtonDisabled,
|
|
538
|
+
]}
|
|
539
|
+
>
|
|
540
|
+
{checkingConnection ? (
|
|
541
|
+
<ActivityIndicator size="small" color={colors.black} />
|
|
542
|
+
) : (
|
|
543
|
+
<Ionicons name="arrow-forward" size={16} color={colors.black} />
|
|
544
|
+
)}
|
|
545
|
+
<Text style={styles.primaryButtonText}>
|
|
546
|
+
{mode === 'edit' ? 'Save URL' : 'Continue'}
|
|
547
|
+
</Text>
|
|
548
|
+
</Pressable>
|
|
549
|
+
</View>
|
|
550
|
+
</View>
|
|
551
|
+
|
|
552
|
+
<View style={styles.hintCard}>
|
|
553
|
+
<Text style={styles.hintTitle}>Quick Setup</Text>
|
|
554
|
+
<Text style={styles.hintText}>1. Start the bridge in Local (LAN) or Tailscale mode.</Text>
|
|
555
|
+
<Text style={styles.hintText}>2. Pick Local or Tailscale above, then confirm bridge URL.</Text>
|
|
556
|
+
<Text style={styles.hintText}>3. Scan bridge QR, then test connection and continue.</Text>
|
|
557
|
+
</View>
|
|
558
|
+
</ScrollView>
|
|
559
|
+
)}
|
|
560
|
+
<Modal
|
|
561
|
+
animationType="slide"
|
|
562
|
+
visible={scannerVisible}
|
|
563
|
+
transparent
|
|
564
|
+
onRequestClose={closeScanner}
|
|
565
|
+
>
|
|
566
|
+
<View style={styles.scannerModalRoot}>
|
|
567
|
+
<View style={styles.scannerSheet}>
|
|
568
|
+
<View style={styles.scannerHeader}>
|
|
569
|
+
<Text style={styles.scannerTitle}>Scan Bridge QR</Text>
|
|
570
|
+
<Pressable
|
|
571
|
+
onPress={closeScanner}
|
|
572
|
+
hitSlop={8}
|
|
573
|
+
style={({ pressed }) => [
|
|
574
|
+
styles.scannerCloseBtn,
|
|
575
|
+
pressed && styles.scannerCloseBtnPressed,
|
|
576
|
+
]}
|
|
577
|
+
>
|
|
578
|
+
<Ionicons name="close" size={18} color={colors.textPrimary} />
|
|
579
|
+
</Pressable>
|
|
580
|
+
</View>
|
|
581
|
+
<View style={styles.scannerCameraFrame}>
|
|
582
|
+
{cameraPermission?.granted ? (
|
|
583
|
+
<CameraView
|
|
584
|
+
style={styles.scannerCamera}
|
|
585
|
+
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
|
|
586
|
+
onBarcodeScanned={scannerLocked ? undefined : handleBarcodeScanned}
|
|
587
|
+
/>
|
|
588
|
+
) : (
|
|
589
|
+
<View style={styles.scannerPermissionWrap}>
|
|
590
|
+
<Text style={styles.scannerPermissionText}>
|
|
591
|
+
Camera permission is required to scan bridge QR.
|
|
592
|
+
</Text>
|
|
593
|
+
</View>
|
|
594
|
+
)}
|
|
595
|
+
</View>
|
|
596
|
+
<Text style={styles.scannerHintText}>
|
|
597
|
+
Scan the bridge QR to autofill URL and token (or token-only fallback).
|
|
598
|
+
</Text>
|
|
599
|
+
{scannerError ? <Text style={styles.errorText}>{scannerError}</Text> : null}
|
|
600
|
+
</View>
|
|
601
|
+
</View>
|
|
602
|
+
</Modal>
|
|
603
|
+
</KeyboardAvoidingView>
|
|
604
|
+
</SafeAreaView>
|
|
605
|
+
</View>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function IntroFeatureRow({
|
|
610
|
+
icon,
|
|
611
|
+
title,
|
|
612
|
+
description,
|
|
613
|
+
}: {
|
|
614
|
+
icon: keyof typeof Ionicons.glyphMap;
|
|
615
|
+
title: string;
|
|
616
|
+
description: string;
|
|
617
|
+
}) {
|
|
618
|
+
return (
|
|
619
|
+
<View style={styles.introFeatureRow}>
|
|
620
|
+
<View style={styles.introFeatureIconWrap}>
|
|
621
|
+
<Ionicons name={icon} size={16} color={colors.textPrimary} />
|
|
622
|
+
</View>
|
|
623
|
+
<View style={styles.introFeatureTextWrap}>
|
|
624
|
+
<Text style={styles.introFeatureTitle}>{title}</Text>
|
|
625
|
+
<Text style={styles.introFeatureDescription}>{description}</Text>
|
|
626
|
+
</View>
|
|
627
|
+
</View>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function parsePairingPayload(rawValue: string): PairingPayload | null {
|
|
632
|
+
const raw = rawValue.trim();
|
|
633
|
+
if (!raw) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const parsed = JSON.parse(raw) as {
|
|
639
|
+
type?: unknown;
|
|
640
|
+
bridgeUrl?: unknown;
|
|
641
|
+
url?: unknown;
|
|
642
|
+
bridgeToken?: unknown;
|
|
643
|
+
token?: unknown;
|
|
644
|
+
};
|
|
645
|
+
const type = typeof parsed.type === 'string' ? parsed.type.trim().toLowerCase() : '';
|
|
646
|
+
const bridgeUrlRaw =
|
|
647
|
+
typeof parsed.bridgeUrl === 'string'
|
|
648
|
+
? parsed.bridgeUrl
|
|
649
|
+
: typeof parsed.url === 'string'
|
|
650
|
+
? parsed.url
|
|
651
|
+
: '';
|
|
652
|
+
const bridgeTokenRaw =
|
|
653
|
+
typeof parsed.bridgeToken === 'string'
|
|
654
|
+
? parsed.bridgeToken
|
|
655
|
+
: typeof parsed.token === 'string'
|
|
656
|
+
? parsed.token
|
|
657
|
+
: '';
|
|
658
|
+
const bridgeUrl = normalizeBridgeUrlInput(bridgeUrlRaw) ?? undefined;
|
|
659
|
+
const bridgeToken = bridgeTokenRaw.trim();
|
|
660
|
+
if (
|
|
661
|
+
bridgeToken &&
|
|
662
|
+
(
|
|
663
|
+
type === 'clawdex-bridge-pair' ||
|
|
664
|
+
type === 'clawdex/bridge-pair' ||
|
|
665
|
+
type === 'clawdex-bridge-token' ||
|
|
666
|
+
type === 'clawdex/bridge-token' ||
|
|
667
|
+
!type
|
|
668
|
+
)
|
|
669
|
+
) {
|
|
670
|
+
return bridgeUrl ? { bridgeToken, bridgeUrl } : { bridgeToken };
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
// Try URI form fallback below.
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const parsed = new URL(raw);
|
|
678
|
+
if (parsed.protocol !== 'clawdex:') {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const bridgeUrl =
|
|
682
|
+
normalizeBridgeUrlInput(
|
|
683
|
+
parsed.searchParams.get('bridgeUrl') ?? parsed.searchParams.get('url') ?? ''
|
|
684
|
+
) ?? undefined;
|
|
685
|
+
const bridgeToken = (
|
|
686
|
+
parsed.searchParams.get('bridgeToken') ?? parsed.searchParams.get('token') ?? ''
|
|
687
|
+
).trim();
|
|
688
|
+
if (!bridgeToken) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return bridgeUrl ? { bridgeToken, bridgeUrl } : { bridgeToken };
|
|
692
|
+
} catch {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const styles = StyleSheet.create({
|
|
698
|
+
container: {
|
|
699
|
+
flex: 1,
|
|
700
|
+
backgroundColor: colors.bgMain,
|
|
701
|
+
},
|
|
702
|
+
safeArea: {
|
|
703
|
+
flex: 1,
|
|
704
|
+
},
|
|
705
|
+
keyboardAvoiding: {
|
|
706
|
+
flex: 1,
|
|
707
|
+
},
|
|
708
|
+
introRoot: {
|
|
709
|
+
flex: 1,
|
|
710
|
+
paddingHorizontal: spacing.lg,
|
|
711
|
+
paddingTop: spacing.md,
|
|
712
|
+
paddingBottom: spacing.xxl,
|
|
713
|
+
gap: spacing.md,
|
|
714
|
+
},
|
|
715
|
+
introBrandRow: {
|
|
716
|
+
flexDirection: 'row',
|
|
717
|
+
alignItems: 'center',
|
|
718
|
+
gap: spacing.sm,
|
|
719
|
+
paddingHorizontal: spacing.xs,
|
|
720
|
+
},
|
|
721
|
+
introBrandName: {
|
|
722
|
+
...typography.headline,
|
|
723
|
+
color: colors.textPrimary,
|
|
724
|
+
fontSize: 18,
|
|
725
|
+
letterSpacing: -0.2,
|
|
726
|
+
},
|
|
727
|
+
introMain: {
|
|
728
|
+
flex: 1,
|
|
729
|
+
gap: spacing.md,
|
|
730
|
+
},
|
|
731
|
+
introFeaturesCard: {
|
|
732
|
+
flex: 1,
|
|
733
|
+
paddingVertical: spacing.md,
|
|
734
|
+
},
|
|
735
|
+
introFeaturesList: {
|
|
736
|
+
flex: 1,
|
|
737
|
+
},
|
|
738
|
+
introFeaturesListContent: {
|
|
739
|
+
gap: spacing.sm,
|
|
740
|
+
paddingBottom: spacing.sm,
|
|
741
|
+
},
|
|
742
|
+
introFooter: {
|
|
743
|
+
paddingTop: spacing.sm,
|
|
744
|
+
},
|
|
745
|
+
scroll: {
|
|
746
|
+
flex: 1,
|
|
747
|
+
},
|
|
748
|
+
scrollContent: {
|
|
749
|
+
paddingHorizontal: spacing.lg,
|
|
750
|
+
paddingTop: spacing.md,
|
|
751
|
+
paddingBottom: spacing.xxl,
|
|
752
|
+
gap: spacing.md,
|
|
753
|
+
},
|
|
754
|
+
heroCard: {
|
|
755
|
+
borderRadius: radius.lg,
|
|
756
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
757
|
+
borderColor: colors.borderHighlight,
|
|
758
|
+
backgroundColor: colors.black,
|
|
759
|
+
padding: spacing.lg,
|
|
760
|
+
gap: spacing.sm,
|
|
761
|
+
overflow: 'hidden',
|
|
762
|
+
},
|
|
763
|
+
heroTopRow: {
|
|
764
|
+
flexDirection: 'row',
|
|
765
|
+
justifyContent: 'space-between',
|
|
766
|
+
alignItems: 'center',
|
|
767
|
+
},
|
|
768
|
+
heroIconWrap: {
|
|
769
|
+
width: 34,
|
|
770
|
+
height: 34,
|
|
771
|
+
borderRadius: 17,
|
|
772
|
+
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
773
|
+
alignItems: 'center',
|
|
774
|
+
justifyContent: 'center',
|
|
775
|
+
},
|
|
776
|
+
cancelBtn: {
|
|
777
|
+
width: 30,
|
|
778
|
+
height: 30,
|
|
779
|
+
borderRadius: 15,
|
|
780
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
781
|
+
borderColor: colors.borderLight,
|
|
782
|
+
backgroundColor: colors.bgMain,
|
|
783
|
+
alignItems: 'center',
|
|
784
|
+
justifyContent: 'center',
|
|
785
|
+
},
|
|
786
|
+
cancelBtnPressed: {
|
|
787
|
+
opacity: 0.75,
|
|
788
|
+
},
|
|
789
|
+
heroTitle: {
|
|
790
|
+
...typography.largeTitle,
|
|
791
|
+
fontSize: 28,
|
|
792
|
+
letterSpacing: -0.5,
|
|
793
|
+
},
|
|
794
|
+
heroDescription: {
|
|
795
|
+
...typography.body,
|
|
796
|
+
color: colors.textSecondary,
|
|
797
|
+
},
|
|
798
|
+
introSectionTitle: {
|
|
799
|
+
...typography.caption,
|
|
800
|
+
textTransform: 'uppercase',
|
|
801
|
+
letterSpacing: 0.9,
|
|
802
|
+
color: colors.textMuted,
|
|
803
|
+
},
|
|
804
|
+
introFeatureRow: {
|
|
805
|
+
flexDirection: 'row',
|
|
806
|
+
gap: spacing.sm,
|
|
807
|
+
borderRadius: radius.md,
|
|
808
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
809
|
+
borderColor: colors.borderLight,
|
|
810
|
+
backgroundColor: colors.black,
|
|
811
|
+
paddingHorizontal: spacing.md,
|
|
812
|
+
paddingVertical: spacing.md,
|
|
813
|
+
minHeight: 62,
|
|
814
|
+
},
|
|
815
|
+
introFeatureIconWrap: {
|
|
816
|
+
width: 30,
|
|
817
|
+
height: 30,
|
|
818
|
+
borderRadius: 15,
|
|
819
|
+
alignItems: 'center',
|
|
820
|
+
justifyContent: 'center',
|
|
821
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
822
|
+
marginTop: 2,
|
|
823
|
+
},
|
|
824
|
+
introFeatureTextWrap: {
|
|
825
|
+
flex: 1,
|
|
826
|
+
gap: 2,
|
|
827
|
+
},
|
|
828
|
+
introFeatureTitle: {
|
|
829
|
+
...typography.headline,
|
|
830
|
+
color: colors.textPrimary,
|
|
831
|
+
fontSize: 14,
|
|
832
|
+
},
|
|
833
|
+
introFeatureDescription: {
|
|
834
|
+
...typography.caption,
|
|
835
|
+
color: colors.textSecondary,
|
|
836
|
+
lineHeight: 18,
|
|
837
|
+
},
|
|
838
|
+
introNextButton: {
|
|
839
|
+
borderRadius: radius.md,
|
|
840
|
+
backgroundColor: colors.accent,
|
|
841
|
+
minHeight: 62,
|
|
842
|
+
flexDirection: 'row',
|
|
843
|
+
alignItems: 'center',
|
|
844
|
+
justifyContent: 'center',
|
|
845
|
+
gap: spacing.sm,
|
|
846
|
+
width: '100%',
|
|
847
|
+
},
|
|
848
|
+
introNextButtonPressed: {
|
|
849
|
+
backgroundColor: colors.accentPressed,
|
|
850
|
+
},
|
|
851
|
+
introNextButtonText: {
|
|
852
|
+
...typography.headline,
|
|
853
|
+
color: colors.black,
|
|
854
|
+
fontSize: 18,
|
|
855
|
+
fontWeight: '700',
|
|
856
|
+
},
|
|
857
|
+
formCard: {
|
|
858
|
+
borderRadius: radius.lg,
|
|
859
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
860
|
+
borderColor: colors.borderHighlight,
|
|
861
|
+
backgroundColor: colors.black,
|
|
862
|
+
padding: spacing.lg,
|
|
863
|
+
gap: spacing.sm,
|
|
864
|
+
overflow: 'hidden',
|
|
865
|
+
},
|
|
866
|
+
label: {
|
|
867
|
+
...typography.caption,
|
|
868
|
+
textTransform: 'uppercase',
|
|
869
|
+
letterSpacing: 0.9,
|
|
870
|
+
color: colors.textMuted,
|
|
871
|
+
},
|
|
872
|
+
tokenHeaderRow: {
|
|
873
|
+
marginTop: spacing.xs,
|
|
874
|
+
flexDirection: 'row',
|
|
875
|
+
justifyContent: 'space-between',
|
|
876
|
+
alignItems: 'center',
|
|
877
|
+
gap: spacing.sm,
|
|
878
|
+
},
|
|
879
|
+
optionalLabel: {
|
|
880
|
+
...typography.caption,
|
|
881
|
+
color: colors.textMuted,
|
|
882
|
+
fontSize: 11,
|
|
883
|
+
},
|
|
884
|
+
tokenInputWrap: {
|
|
885
|
+
flexDirection: 'row',
|
|
886
|
+
alignItems: 'center',
|
|
887
|
+
gap: spacing.xs,
|
|
888
|
+
},
|
|
889
|
+
input: {
|
|
890
|
+
...typography.body,
|
|
891
|
+
color: colors.textPrimary,
|
|
892
|
+
borderWidth: 1,
|
|
893
|
+
borderColor: colors.border,
|
|
894
|
+
borderRadius: radius.md,
|
|
895
|
+
backgroundColor: colors.black,
|
|
896
|
+
paddingHorizontal: spacing.md,
|
|
897
|
+
paddingVertical: spacing.md,
|
|
898
|
+
},
|
|
899
|
+
tokenInput: {
|
|
900
|
+
flex: 1,
|
|
901
|
+
},
|
|
902
|
+
tokenRevealBtn: {
|
|
903
|
+
minWidth: 74,
|
|
904
|
+
borderRadius: radius.md,
|
|
905
|
+
borderWidth: 1,
|
|
906
|
+
borderColor: colors.border,
|
|
907
|
+
backgroundColor: colors.bgMain,
|
|
908
|
+
paddingHorizontal: spacing.sm,
|
|
909
|
+
paddingVertical: spacing.sm,
|
|
910
|
+
flexDirection: 'row',
|
|
911
|
+
alignItems: 'center',
|
|
912
|
+
justifyContent: 'center',
|
|
913
|
+
gap: 4,
|
|
914
|
+
},
|
|
915
|
+
tokenRevealBtnPressed: {
|
|
916
|
+
opacity: 0.8,
|
|
917
|
+
},
|
|
918
|
+
tokenRevealBtnText: {
|
|
919
|
+
...typography.caption,
|
|
920
|
+
color: colors.textSecondary,
|
|
921
|
+
fontWeight: '600',
|
|
922
|
+
},
|
|
923
|
+
modeRow: {
|
|
924
|
+
flexDirection: 'row',
|
|
925
|
+
gap: spacing.sm,
|
|
926
|
+
marginBottom: spacing.xs,
|
|
927
|
+
},
|
|
928
|
+
modeButton: {
|
|
929
|
+
flex: 1,
|
|
930
|
+
minHeight: 44,
|
|
931
|
+
borderRadius: radius.md,
|
|
932
|
+
borderWidth: 1,
|
|
933
|
+
borderColor: colors.border,
|
|
934
|
+
backgroundColor: colors.bgMain,
|
|
935
|
+
paddingHorizontal: spacing.md,
|
|
936
|
+
flexDirection: 'row',
|
|
937
|
+
alignItems: 'center',
|
|
938
|
+
justifyContent: 'center',
|
|
939
|
+
gap: spacing.xs,
|
|
940
|
+
},
|
|
941
|
+
modeButtonPressed: {
|
|
942
|
+
opacity: 0.82,
|
|
943
|
+
},
|
|
944
|
+
modeButtonText: {
|
|
945
|
+
...typography.caption,
|
|
946
|
+
color: colors.textPrimary,
|
|
947
|
+
fontWeight: '700',
|
|
948
|
+
},
|
|
949
|
+
scanButton: {
|
|
950
|
+
marginTop: spacing.xs,
|
|
951
|
+
borderRadius: radius.md,
|
|
952
|
+
borderWidth: 1,
|
|
953
|
+
borderColor: colors.border,
|
|
954
|
+
backgroundColor: colors.bgMain,
|
|
955
|
+
minHeight: 44,
|
|
956
|
+
paddingHorizontal: spacing.md,
|
|
957
|
+
flexDirection: 'row',
|
|
958
|
+
alignItems: 'center',
|
|
959
|
+
justifyContent: 'center',
|
|
960
|
+
gap: spacing.xs,
|
|
961
|
+
},
|
|
962
|
+
scanButtonPressed: {
|
|
963
|
+
opacity: 0.82,
|
|
964
|
+
},
|
|
965
|
+
scanButtonText: {
|
|
966
|
+
...typography.caption,
|
|
967
|
+
color: colors.textPrimary,
|
|
968
|
+
fontWeight: '700',
|
|
969
|
+
},
|
|
970
|
+
helperText: {
|
|
971
|
+
...typography.caption,
|
|
972
|
+
color: colors.textMuted,
|
|
973
|
+
},
|
|
974
|
+
previewWrap: {
|
|
975
|
+
borderRadius: radius.md,
|
|
976
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
977
|
+
borderColor: colors.borderLight,
|
|
978
|
+
backgroundColor: colors.black,
|
|
979
|
+
paddingHorizontal: spacing.md,
|
|
980
|
+
paddingVertical: spacing.sm,
|
|
981
|
+
gap: spacing.xs,
|
|
982
|
+
},
|
|
983
|
+
previewLabel: {
|
|
984
|
+
...typography.caption,
|
|
985
|
+
color: colors.textMuted,
|
|
986
|
+
},
|
|
987
|
+
previewValue: {
|
|
988
|
+
...typography.mono,
|
|
989
|
+
color: colors.textPrimary,
|
|
990
|
+
fontSize: 13,
|
|
991
|
+
},
|
|
992
|
+
warningText: {
|
|
993
|
+
...typography.caption,
|
|
994
|
+
color: '#F7D27E',
|
|
995
|
+
},
|
|
996
|
+
errorText: {
|
|
997
|
+
...typography.caption,
|
|
998
|
+
color: colors.error,
|
|
999
|
+
},
|
|
1000
|
+
successText: {
|
|
1001
|
+
...typography.caption,
|
|
1002
|
+
color: colors.statusComplete,
|
|
1003
|
+
},
|
|
1004
|
+
actionRow: {
|
|
1005
|
+
flexDirection: 'row',
|
|
1006
|
+
gap: spacing.sm,
|
|
1007
|
+
marginTop: spacing.sm,
|
|
1008
|
+
},
|
|
1009
|
+
secondaryButton: {
|
|
1010
|
+
flex: 1,
|
|
1011
|
+
flexDirection: 'row',
|
|
1012
|
+
gap: spacing.xs,
|
|
1013
|
+
alignItems: 'center',
|
|
1014
|
+
justifyContent: 'center',
|
|
1015
|
+
borderWidth: 1,
|
|
1016
|
+
borderColor: colors.border,
|
|
1017
|
+
backgroundColor: colors.bgMain,
|
|
1018
|
+
borderRadius: radius.md,
|
|
1019
|
+
paddingVertical: spacing.md,
|
|
1020
|
+
},
|
|
1021
|
+
secondaryButtonPressed: {
|
|
1022
|
+
opacity: 0.8,
|
|
1023
|
+
},
|
|
1024
|
+
secondaryButtonDisabled: {
|
|
1025
|
+
opacity: 0.65,
|
|
1026
|
+
},
|
|
1027
|
+
secondaryButtonText: {
|
|
1028
|
+
...typography.caption,
|
|
1029
|
+
color: colors.textPrimary,
|
|
1030
|
+
fontWeight: '600',
|
|
1031
|
+
},
|
|
1032
|
+
primaryButton: {
|
|
1033
|
+
flex: 1,
|
|
1034
|
+
flexDirection: 'row',
|
|
1035
|
+
gap: spacing.xs,
|
|
1036
|
+
alignItems: 'center',
|
|
1037
|
+
justifyContent: 'center',
|
|
1038
|
+
backgroundColor: colors.accent,
|
|
1039
|
+
borderRadius: radius.md,
|
|
1040
|
+
paddingVertical: spacing.md,
|
|
1041
|
+
},
|
|
1042
|
+
primaryButtonPressed: {
|
|
1043
|
+
backgroundColor: colors.accentPressed,
|
|
1044
|
+
},
|
|
1045
|
+
primaryButtonDisabled: {
|
|
1046
|
+
opacity: 0.72,
|
|
1047
|
+
},
|
|
1048
|
+
primaryButtonText: {
|
|
1049
|
+
...typography.headline,
|
|
1050
|
+
color: colors.black,
|
|
1051
|
+
fontWeight: '700',
|
|
1052
|
+
},
|
|
1053
|
+
scannerModalRoot: {
|
|
1054
|
+
flex: 1,
|
|
1055
|
+
backgroundColor: 'rgba(0,0,0,0.94)',
|
|
1056
|
+
justifyContent: 'center',
|
|
1057
|
+
paddingHorizontal: spacing.lg,
|
|
1058
|
+
},
|
|
1059
|
+
scannerSheet: {
|
|
1060
|
+
borderRadius: radius.lg,
|
|
1061
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1062
|
+
borderColor: colors.borderHighlight,
|
|
1063
|
+
backgroundColor: colors.black,
|
|
1064
|
+
padding: spacing.lg,
|
|
1065
|
+
gap: spacing.md,
|
|
1066
|
+
},
|
|
1067
|
+
scannerHeader: {
|
|
1068
|
+
flexDirection: 'row',
|
|
1069
|
+
alignItems: 'center',
|
|
1070
|
+
justifyContent: 'space-between',
|
|
1071
|
+
},
|
|
1072
|
+
scannerTitle: {
|
|
1073
|
+
...typography.headline,
|
|
1074
|
+
color: colors.textPrimary,
|
|
1075
|
+
},
|
|
1076
|
+
scannerCloseBtn: {
|
|
1077
|
+
width: 30,
|
|
1078
|
+
height: 30,
|
|
1079
|
+
borderRadius: 15,
|
|
1080
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1081
|
+
borderColor: colors.borderLight,
|
|
1082
|
+
backgroundColor: colors.bgMain,
|
|
1083
|
+
alignItems: 'center',
|
|
1084
|
+
justifyContent: 'center',
|
|
1085
|
+
},
|
|
1086
|
+
scannerCloseBtnPressed: {
|
|
1087
|
+
opacity: 0.75,
|
|
1088
|
+
},
|
|
1089
|
+
scannerCameraFrame: {
|
|
1090
|
+
width: '100%',
|
|
1091
|
+
aspectRatio: 1,
|
|
1092
|
+
borderRadius: radius.md,
|
|
1093
|
+
overflow: 'hidden',
|
|
1094
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1095
|
+
borderColor: colors.borderLight,
|
|
1096
|
+
backgroundColor: colors.bgMain,
|
|
1097
|
+
},
|
|
1098
|
+
scannerCamera: {
|
|
1099
|
+
flex: 1,
|
|
1100
|
+
},
|
|
1101
|
+
scannerPermissionWrap: {
|
|
1102
|
+
flex: 1,
|
|
1103
|
+
alignItems: 'center',
|
|
1104
|
+
justifyContent: 'center',
|
|
1105
|
+
paddingHorizontal: spacing.lg,
|
|
1106
|
+
},
|
|
1107
|
+
scannerPermissionText: {
|
|
1108
|
+
...typography.caption,
|
|
1109
|
+
color: colors.textSecondary,
|
|
1110
|
+
textAlign: 'center',
|
|
1111
|
+
},
|
|
1112
|
+
scannerHintText: {
|
|
1113
|
+
...typography.caption,
|
|
1114
|
+
color: colors.textMuted,
|
|
1115
|
+
},
|
|
1116
|
+
hintCard: {
|
|
1117
|
+
borderRadius: radius.md,
|
|
1118
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1119
|
+
borderColor: colors.borderLight,
|
|
1120
|
+
backgroundColor: colors.black,
|
|
1121
|
+
padding: spacing.lg,
|
|
1122
|
+
gap: spacing.sm,
|
|
1123
|
+
},
|
|
1124
|
+
hintTitle: {
|
|
1125
|
+
...typography.headline,
|
|
1126
|
+
color: colors.textPrimary,
|
|
1127
|
+
},
|
|
1128
|
+
hintText: {
|
|
1129
|
+
...typography.caption,
|
|
1130
|
+
color: colors.textSecondary,
|
|
1131
|
+
},
|
|
1132
|
+
});
|