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.
Files changed (48) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/npm-release.yml +18 -0
  3. package/AGENTS.md +3 -3
  4. package/README.md +101 -541
  5. package/apps/mobile/.env.example +1 -2
  6. package/apps/mobile/App.tsx +261 -68
  7. package/apps/mobile/app.json +31 -5
  8. package/apps/mobile/assets/brand/splash-icon-white.png +0 -0
  9. package/apps/mobile/eas.json +30 -0
  10. package/apps/mobile/package.json +22 -21
  11. package/apps/mobile/plugins/withAndroidCleartextTraffic.js +14 -0
  12. package/apps/mobile/src/api/__tests__/ws.test.ts +44 -6
  13. package/apps/mobile/src/api/chatMapping.ts +48 -8
  14. package/apps/mobile/src/api/client.ts +6 -0
  15. package/apps/mobile/src/api/types.ts +11 -0
  16. package/apps/mobile/src/api/ws.ts +52 -10
  17. package/apps/mobile/src/bridgeUrl.ts +105 -0
  18. package/apps/mobile/src/components/ActivityBar.tsx +32 -13
  19. package/apps/mobile/src/components/ChatHeader.tsx +3 -2
  20. package/apps/mobile/src/components/ChatInput.tsx +246 -91
  21. package/apps/mobile/src/components/ChatMessage.tsx +108 -4
  22. package/apps/mobile/src/config.ts +11 -29
  23. package/apps/mobile/src/hooks/useVoiceRecorder.ts +264 -0
  24. package/apps/mobile/src/navigation/DrawerContent.tsx +18 -8
  25. package/apps/mobile/src/screens/GitScreen.tsx +1 -1
  26. package/apps/mobile/src/screens/MainScreen.tsx +906 -268
  27. package/apps/mobile/src/screens/OnboardingScreen.tsx +1132 -0
  28. package/apps/mobile/src/screens/PrivacyScreen.tsx +1 -1
  29. package/apps/mobile/src/screens/SettingsScreen.tsx +65 -1
  30. package/apps/mobile/src/screens/TerminalScreen.tsx +1 -1
  31. package/apps/mobile/src/screens/TermsScreen.tsx +1 -1
  32. package/docs/app-review-notes.md +7 -2
  33. package/docs/eas-builds.md +91 -0
  34. package/docs/realtime-streaming-limitations.md +84 -0
  35. package/docs/setup-and-operations.md +239 -0
  36. package/docs/troubleshooting.md +121 -0
  37. package/docs/voice-transcription.md +87 -0
  38. package/package.json +8 -16
  39. package/scripts/setup-secure-dev.sh +122 -8
  40. package/scripts/setup-wizard.sh +342 -122
  41. package/scripts/start-bridge-secure.sh +7 -1
  42. package/scripts/sync-versions.js +63 -0
  43. package/services/rust-bridge/.env.example +1 -1
  44. package/services/rust-bridge/Cargo.lock +1104 -23
  45. package/services/rust-bridge/Cargo.toml +3 -1
  46. package/services/rust-bridge/package.json +1 -1
  47. package/services/rust-bridge/src/main.rs +587 -12
  48. package/apps/mobile/metro.config.js +0 -3
@@ -1,6 +1,5 @@
1
- EXPO_PUBLIC_HOST_BRIDGE_URL=http://127.0.0.1:8787
2
1
  EXPO_PUBLIC_HOST_BRIDGE_TOKEN=change-me
3
- EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH=false
2
+ EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH=true
4
3
  EXPO_PUBLIC_ALLOW_INSECURE_REMOTE_BRIDGE=false
5
4
  EXPO_PUBLIC_EXTERNAL_STATUS_FULL_SYNC_DEBOUNCE_MS=450
6
5
  EXPO_PUBLIC_PRIVACY_POLICY_URL=https://example.com/privacy
@@ -3,6 +3,7 @@ import 'react-native-gesture-handler';
3
3
  import * as FileSystem from 'expo-file-system/legacy';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import {
6
+ ActivityIndicator,
6
7
  Animated,
7
8
  Keyboard,
8
9
  PanResponder,
@@ -11,21 +12,25 @@ import {
11
12
  useWindowDimensions,
12
13
  View,
13
14
  } from 'react-native';
15
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
14
16
 
15
17
  import { HostBridgeApiClient } from './src/api/client';
16
18
  import type { ApprovalMode, Chat, ReasoningEffort } from './src/api/types';
17
19
  import { HostBridgeWsClient } from './src/api/ws';
20
+ import { normalizeBridgeUrlInput } from './src/bridgeUrl';
18
21
  import { env } from './src/config';
19
22
  import { DrawerContent } from './src/navigation/DrawerContent';
20
23
  import { GitScreen } from './src/screens/GitScreen';
21
24
  import { MainScreen, type MainScreenHandle } from './src/screens/MainScreen';
25
+ import { OnboardingScreen } from './src/screens/OnboardingScreen';
22
26
  import { PrivacyScreen } from './src/screens/PrivacyScreen';
23
27
  import { SettingsScreen } from './src/screens/SettingsScreen';
24
- import { TerminalScreen } from './src/screens/TerminalScreen';
25
28
  import { TermsScreen } from './src/screens/TermsScreen';
26
29
  import { colors } from './src/theme';
27
30
 
28
- type Screen = 'Main' | 'ChatGit' | 'Terminal' | 'Settings' | 'Privacy' | 'Terms';
31
+ type AppScreen = 'Main' | 'ChatGit' | 'Settings' | 'Privacy' | 'Terms';
32
+ type Screen = AppScreen | 'Onboarding';
33
+ type OnboardingMode = 'initial' | 'edit';
29
34
 
30
35
  const DRAWER_WIDTH = 280;
31
36
  const EDGE_SWIPE_WIDTH = 24;
@@ -34,22 +39,32 @@ const SWIPE_CLOSE_DISTANCE = 56;
34
39
  const SWIPE_OPEN_VELOCITY = 0.4;
35
40
  const SWIPE_CLOSE_VELOCITY = -0.4;
36
41
  const APP_SETTINGS_FILE = 'clawdex-app-settings.json';
37
- const APP_SETTINGS_VERSION = 1;
42
+ const APP_SETTINGS_VERSION = 3;
38
43
 
39
44
  export default function App() {
45
+ const [settingsLoaded, setSettingsLoaded] = useState(false);
46
+ const [bridgeUrl, setBridgeUrl] = useState<string | null>(null);
47
+ const [bridgeToken, setBridgeToken] = useState<string | null>(env.hostBridgeToken);
48
+ const [onboardingMode, setOnboardingMode] = useState<OnboardingMode>('initial');
49
+ const [onboardingReturnScreen, setOnboardingReturnScreen] =
50
+ useState<AppScreen>('Settings');
40
51
  const ws = useMemo(
41
52
  () =>
42
- new HostBridgeWsClient(env.hostBridgeUrl, {
43
- authToken: env.hostBridgeToken,
44
- allowQueryTokenAuth: env.allowWsQueryTokenAuth
45
- }),
46
- []
53
+ bridgeUrl
54
+ ? new HostBridgeWsClient(bridgeUrl, {
55
+ authToken: bridgeToken ?? env.hostBridgeToken,
56
+ allowQueryTokenAuth: env.allowWsQueryTokenAuth
57
+ })
58
+ : null,
59
+ [bridgeToken, bridgeUrl]
47
60
  );
48
61
  const api = useMemo(
49
62
  () =>
50
- new HostBridgeApiClient({
51
- ws,
52
- }),
63
+ ws
64
+ ? new HostBridgeApiClient({
65
+ ws,
66
+ })
67
+ : null,
53
68
  [ws]
54
69
  );
55
70
  const mainRef = useRef<MainScreenHandle>(null);
@@ -70,12 +85,18 @@ export default function App() {
70
85
  const { width: screenWidth } = useWindowDimensions();
71
86
 
72
87
  useEffect(() => {
88
+ if (!ws) {
89
+ return;
90
+ }
91
+
73
92
  ws.connect();
74
93
  return () => ws.disconnect();
75
94
  }, [ws]);
76
95
 
77
96
  const saveAppSettings = useCallback(
78
97
  async (
98
+ nextBridgeUrl: string | null,
99
+ nextBridgeToken: string | null,
79
100
  nextModelId: string | null,
80
101
  nextEffort: ReasoningEffort | null,
81
102
  nextApprovalMode: ApprovalMode
@@ -87,6 +108,8 @@ export default function App() {
87
108
 
88
109
  const payload = JSON.stringify({
89
110
  version: APP_SETTINGS_VERSION,
111
+ bridgeUrl: nextBridgeUrl,
112
+ bridgeToken: nextBridgeToken,
90
113
  defaultModelId: nextModelId,
91
114
  defaultReasoningEffort: nextEffort,
92
115
  approvalMode: nextApprovalMode,
@@ -104,9 +127,21 @@ export default function App() {
104
127
  useEffect(() => {
105
128
  let cancelled = false;
106
129
 
130
+ const resetToDefaults = () => {
131
+ setDefaultModelId(null);
132
+ setDefaultReasoningEffort(null);
133
+ setApprovalMode('normal');
134
+ };
135
+
107
136
  const loadSettings = async () => {
108
137
  const settingsPath = getAppSettingsPath();
109
138
  if (!settingsPath) {
139
+ if (!cancelled) {
140
+ resetToDefaults();
141
+ setBridgeUrl(null);
142
+ setBridgeToken(env.hostBridgeToken);
143
+ setSettingsLoaded(true);
144
+ }
110
145
  return;
111
146
  }
112
147
 
@@ -116,14 +151,21 @@ export default function App() {
116
151
  return;
117
152
  }
118
153
  const parsed = parseAppSettings(raw);
154
+ const resolvedBridgeUrl = parsed.bridgeUrl ?? null;
155
+ setBridgeUrl(resolvedBridgeUrl);
156
+ setBridgeToken(parsed.bridgeToken ?? env.hostBridgeToken);
119
157
  setDefaultModelId(parsed.defaultModelId);
120
158
  setDefaultReasoningEffort(parsed.defaultReasoningEffort);
121
159
  setApprovalMode(parsed.approvalMode);
122
160
  } catch {
123
161
  if (!cancelled) {
124
- setDefaultModelId(null);
125
- setDefaultReasoningEffort(null);
126
- setApprovalMode('normal');
162
+ resetToDefaults();
163
+ setBridgeUrl(null);
164
+ setBridgeToken(env.hostBridgeToken);
165
+ }
166
+ } finally {
167
+ if (!cancelled) {
168
+ setSettingsLoaded(true);
127
169
  }
128
170
  }
129
171
  };
@@ -280,20 +322,100 @@ export default function App() {
280
322
  const normalizedEffort = normalizeReasoningEffort(effort);
281
323
  setDefaultModelId(normalizedModelId);
282
324
  setDefaultReasoningEffort(normalizedEffort);
283
- void saveAppSettings(normalizedModelId, normalizedEffort, approvalMode);
325
+ void saveAppSettings(
326
+ bridgeUrl,
327
+ bridgeToken,
328
+ normalizedModelId,
329
+ normalizedEffort,
330
+ approvalMode
331
+ );
284
332
  },
285
- [approvalMode, saveAppSettings]
333
+ [approvalMode, bridgeToken, bridgeUrl, saveAppSettings]
286
334
  );
287
335
 
288
336
  const handleApprovalModeChange = useCallback(
289
337
  (nextMode: ApprovalMode) => {
290
338
  const normalizedMode = normalizeApprovalMode(nextMode);
291
339
  setApprovalMode(normalizedMode);
292
- void saveAppSettings(defaultModelId, defaultReasoningEffort, normalizedMode);
340
+ void saveAppSettings(
341
+ bridgeUrl,
342
+ bridgeToken,
343
+ defaultModelId,
344
+ defaultReasoningEffort,
345
+ normalizedMode
346
+ );
347
+ },
348
+ [bridgeToken, bridgeUrl, defaultModelId, defaultReasoningEffort, saveAppSettings]
349
+ );
350
+
351
+ const handleBridgeUrlSaved = useCallback(
352
+ (nextBridgeUrl: string, nextBridgeToken: string | null) => {
353
+ const normalized = normalizeBridgeUrlInput(nextBridgeUrl);
354
+ if (!normalized) {
355
+ return;
356
+ }
357
+
358
+ setBridgeUrl(normalized);
359
+ setBridgeToken(normalizeBridgeToken(nextBridgeToken));
360
+ setSelectedChatId(null);
361
+ setActiveChat(null);
362
+ setGitChat(null);
363
+ setPendingMainChatId(null);
364
+ setPendingMainChatSnapshot(null);
365
+ void saveAppSettings(
366
+ normalized,
367
+ normalizeBridgeToken(nextBridgeToken),
368
+ defaultModelId,
369
+ defaultReasoningEffort,
370
+ approvalMode
371
+ );
372
+ setCurrentScreen(onboardingMode === 'edit' ? onboardingReturnScreen : 'Main');
373
+ setOnboardingMode('edit');
374
+ closeDrawer();
293
375
  },
294
- [defaultModelId, defaultReasoningEffort, saveAppSettings]
376
+ [
377
+ approvalMode,
378
+ closeDrawer,
379
+ defaultModelId,
380
+ defaultReasoningEffort,
381
+ onboardingMode,
382
+ onboardingReturnScreen,
383
+ saveAppSettings,
384
+ ]
295
385
  );
296
386
 
387
+ const handleOpenBridgeUrlSettings = useCallback(() => {
388
+ setOnboardingMode(bridgeUrl ? 'edit' : 'initial');
389
+ setOnboardingReturnScreen(currentScreen === 'Onboarding' ? 'Settings' : currentScreen);
390
+ setCurrentScreen('Onboarding');
391
+ closeDrawer();
392
+ }, [bridgeUrl, closeDrawer, currentScreen]);
393
+
394
+ const handleResetOnboarding = useCallback(() => {
395
+ setBridgeUrl(null);
396
+ setBridgeToken(null);
397
+ setSelectedChatId(null);
398
+ setActiveChat(null);
399
+ setGitChat(null);
400
+ setPendingMainChatId(null);
401
+ setPendingMainChatSnapshot(null);
402
+ setOnboardingMode('initial');
403
+ setOnboardingReturnScreen('Main');
404
+ setCurrentScreen('Onboarding');
405
+ void saveAppSettings(null, null, defaultModelId, defaultReasoningEffort, approvalMode);
406
+ closeDrawer();
407
+ }, [
408
+ approvalMode,
409
+ closeDrawer,
410
+ defaultModelId,
411
+ defaultReasoningEffort,
412
+ saveAppSettings,
413
+ ]);
414
+
415
+ const handleCancelOnboarding = useCallback(() => {
416
+ setCurrentScreen(onboardingReturnScreen);
417
+ }, [onboardingReturnScreen]);
418
+
297
419
  const handleOpenChatGit = useCallback((chat: Chat) => {
298
420
  setGitChat(chat);
299
421
  setSelectedChatId(chat.id);
@@ -335,14 +457,45 @@ export default function App() {
335
457
  setCurrentScreen('Terms');
336
458
  }, []);
337
459
 
460
+ if (!settingsLoaded) {
461
+ return (
462
+ <SafeAreaProvider>
463
+ <View style={styles.loadingRoot}>
464
+ <ActivityIndicator size="large" color={colors.textMuted} />
465
+ </View>
466
+ </SafeAreaProvider>
467
+ );
468
+ }
469
+
470
+ if (!bridgeUrl || !api || !ws || currentScreen === 'Onboarding') {
471
+ const initialUrl = bridgeUrl ?? env.legacyHostBridgeUrl ?? '';
472
+ const initialToken = bridgeToken ?? env.hostBridgeToken ?? '';
473
+ const mode: OnboardingMode = bridgeUrl ? onboardingMode : 'initial';
474
+ const canCancel = mode === 'edit' && Boolean(bridgeUrl);
475
+ return (
476
+ <SafeAreaProvider>
477
+ <OnboardingScreen
478
+ mode={mode}
479
+ initialBridgeUrl={initialUrl}
480
+ initialBridgeToken={initialToken}
481
+ allowInsecureRemoteBridge={env.allowInsecureRemoteBridge}
482
+ allowQueryTokenAuth={env.allowWsQueryTokenAuth}
483
+ onSave={handleBridgeUrlSaved}
484
+ onCancel={canCancel ? handleCancelOnboarding : undefined}
485
+ />
486
+ </SafeAreaProvider>
487
+ );
488
+ }
489
+
490
+ const activeApi = api;
491
+ const activeWs = ws;
492
+
338
493
  const renderScreen = () => {
339
494
  switch (currentScreen) {
340
- case 'Terminal':
341
- return <TerminalScreen api={api} ws={ws} onOpenDrawer={openDrawer} />;
342
495
  case 'ChatGit':
343
496
  return gitChat ? (
344
497
  <GitScreen
345
- api={api}
498
+ api={activeApi}
346
499
  chat={gitChat}
347
500
  onBack={handleCloseGit}
348
501
  onChatUpdated={handleGitChatUpdated}
@@ -350,8 +503,8 @@ export default function App() {
350
503
  ) : (
351
504
  <MainScreen
352
505
  ref={mainRef}
353
- api={api}
354
- ws={ws}
506
+ api={activeApi}
507
+ ws={activeWs}
355
508
  onOpenDrawer={openDrawer}
356
509
  onOpenGit={handleOpenChatGit}
357
510
  defaultStartCwd={defaultStartCwd}
@@ -371,14 +524,16 @@ export default function App() {
371
524
  case 'Settings':
372
525
  return (
373
526
  <SettingsScreen
374
- api={api}
375
- ws={ws}
376
- bridgeUrl={env.hostBridgeUrl}
527
+ api={activeApi}
528
+ ws={activeWs}
529
+ bridgeUrl={bridgeUrl}
377
530
  defaultModelId={defaultModelId}
378
531
  defaultReasoningEffort={defaultReasoningEffort}
379
532
  onDefaultModelSettingsChange={handleDefaultModelSettingsChange}
380
533
  approvalMode={approvalMode}
381
534
  onApprovalModeChange={handleApprovalModeChange}
535
+ onEditBridgeUrl={handleOpenBridgeUrlSettings}
536
+ onResetOnboarding={handleResetOnboarding}
382
537
  onOpenDrawer={openDrawer}
383
538
  onOpenPrivacy={openPrivacy}
384
539
  onOpenTerms={openTerms}
@@ -402,8 +557,8 @@ export default function App() {
402
557
  return (
403
558
  <MainScreen
404
559
  ref={mainRef}
405
- api={api}
406
- ws={ws}
560
+ api={activeApi}
561
+ ws={activeWs}
407
562
  onOpenDrawer={openDrawer}
408
563
  onOpenGit={handleOpenChatGit}
409
564
  defaultStartCwd={defaultStartCwd}
@@ -424,47 +579,49 @@ export default function App() {
424
579
  };
425
580
 
426
581
  return (
427
- <View style={styles.root}>
428
- {/* Main content */}
429
- <View style={[styles.screen, { width: screenWidth }]}>
430
- {renderScreen()}
431
- </View>
582
+ <SafeAreaProvider>
583
+ <View style={styles.root}>
584
+ {/* Main content */}
585
+ <View style={[styles.screen, { width: screenWidth }]}>
586
+ {renderScreen()}
587
+ </View>
588
+
589
+ {/* Overlay */}
590
+ <Animated.View
591
+ pointerEvents={drawerOpen ? 'auto' : 'none'}
592
+ {...closeSwipeResponder.panHandlers}
593
+ style={[styles.overlay, { opacity: overlayAnim }]}
594
+ >
595
+ <Pressable style={StyleSheet.absoluteFill} onPress={closeDrawer} />
596
+ </Animated.View>
597
+
598
+ {/* Drawer */}
599
+ <Animated.View
600
+ {...closeSwipeResponder.panHandlers}
601
+ style={[
602
+ styles.drawer,
603
+ { transform: [{ translateX: drawerAnim }] },
604
+ ]}
605
+ >
606
+ <DrawerContent
607
+ api={activeApi}
608
+ ws={activeWs}
609
+ selectedChatId={selectedChatId}
610
+ selectedDefaultCwd={defaultStartCwd}
611
+ onSelectDefaultCwd={setDefaultStartCwd}
612
+ onSelectChat={handleSelectChat}
613
+ onNewChat={handleNewChat}
614
+ onNavigate={navigate}
615
+ />
616
+ </Animated.View>
432
617
 
433
- {/* Overlay */}
434
- <Animated.View
435
- pointerEvents={drawerOpen ? 'auto' : 'none'}
436
- {...closeSwipeResponder.panHandlers}
437
- style={[styles.overlay, { opacity: overlayAnim }]}
438
- >
439
- <Pressable style={StyleSheet.absoluteFill} onPress={closeDrawer} />
440
- </Animated.View>
441
-
442
- {/* Drawer */}
443
- <Animated.View
444
- {...closeSwipeResponder.panHandlers}
445
- style={[
446
- styles.drawer,
447
- { transform: [{ translateX: drawerAnim }] },
448
- ]}
449
- >
450
- <DrawerContent
451
- api={api}
452
- ws={ws}
453
- selectedChatId={selectedChatId}
454
- selectedDefaultCwd={defaultStartCwd}
455
- onSelectDefaultCwd={setDefaultStartCwd}
456
- onSelectChat={handleSelectChat}
457
- onNewChat={handleNewChat}
458
- onNavigate={navigate}
618
+ <View
619
+ pointerEvents={drawerOpen ? 'none' : 'auto'}
620
+ style={styles.edgeSwipeZone}
621
+ {...openSwipeResponder.panHandlers}
459
622
  />
460
- </Animated.View>
461
-
462
- <View
463
- pointerEvents={drawerOpen ? 'none' : 'auto'}
464
- style={styles.edgeSwipeZone}
465
- {...openSwipeResponder.panHandlers}
466
- />
467
- </View>
623
+ </View>
624
+ </SafeAreaProvider>
468
625
  );
469
626
  }
470
627
 
@@ -478,12 +635,16 @@ function getAppSettingsPath(): string | null {
478
635
  }
479
636
 
480
637
  function parseAppSettings(raw: string): {
638
+ bridgeUrl: string | null;
639
+ bridgeToken: string | null;
481
640
  defaultModelId: string | null;
482
641
  defaultReasoningEffort: ReasoningEffort | null;
483
642
  approvalMode: ApprovalMode;
484
643
  } {
485
644
  if (typeof raw !== 'string' || raw.trim().length === 0) {
486
645
  return {
646
+ bridgeUrl: null,
647
+ bridgeToken: null,
487
648
  defaultModelId: null,
488
649
  defaultReasoningEffort: null,
489
650
  approvalMode: 'normal',
@@ -492,12 +653,17 @@ function parseAppSettings(raw: string): {
492
653
 
493
654
  try {
494
655
  const parsed = JSON.parse(raw);
656
+ const parsedVersion = (parsed as { version?: unknown }).version;
495
657
  if (
496
658
  !parsed ||
497
659
  typeof parsed !== 'object' ||
498
- parsed.version !== APP_SETTINGS_VERSION
660
+ (parsedVersion !== 1 &&
661
+ parsedVersion !== 2 &&
662
+ parsedVersion !== APP_SETTINGS_VERSION)
499
663
  ) {
500
664
  return {
665
+ bridgeUrl: null,
666
+ bridgeToken: null,
501
667
  defaultModelId: null,
502
668
  defaultReasoningEffort: null,
503
669
  approvalMode: 'normal',
@@ -505,6 +671,8 @@ function parseAppSettings(raw: string): {
505
671
  }
506
672
 
507
673
  return {
674
+ bridgeUrl: normalizeBridgeUrl((parsed as { bridgeUrl?: unknown }).bridgeUrl),
675
+ bridgeToken: normalizeBridgeToken((parsed as { bridgeToken?: unknown }).bridgeToken),
508
676
  defaultModelId: normalizeModelId(
509
677
  (parsed as { defaultModelId?: unknown }).defaultModelId
510
678
  ),
@@ -517,6 +685,8 @@ function parseAppSettings(raw: string): {
517
685
  };
518
686
  } catch {
519
687
  return {
688
+ bridgeUrl: null,
689
+ bridgeToken: null,
520
690
  defaultModelId: null,
521
691
  defaultReasoningEffort: null,
522
692
  approvalMode: 'normal',
@@ -524,6 +694,23 @@ function parseAppSettings(raw: string): {
524
694
  }
525
695
  }
526
696
 
697
+ function normalizeBridgeUrl(value: unknown): string | null {
698
+ if (typeof value !== 'string') {
699
+ return null;
700
+ }
701
+
702
+ return normalizeBridgeUrlInput(value);
703
+ }
704
+
705
+ function normalizeBridgeToken(value: unknown): string | null {
706
+ if (typeof value !== 'string') {
707
+ return null;
708
+ }
709
+
710
+ const trimmed = value.trim();
711
+ return trimmed.length > 0 ? trimmed : null;
712
+ }
713
+
527
714
  function normalizeModelId(value: unknown): string | null {
528
715
  if (typeof value !== 'string') {
529
716
  return null;
@@ -562,6 +749,12 @@ const styles = StyleSheet.create({
562
749
  flex: 1,
563
750
  backgroundColor: colors.bgMain,
564
751
  },
752
+ loadingRoot: {
753
+ flex: 1,
754
+ alignItems: 'center',
755
+ justifyContent: 'center',
756
+ backgroundColor: colors.bgMain,
757
+ },
565
758
  screen: {
566
759
  flex: 1,
567
760
  },
@@ -2,23 +2,37 @@
2
2
  "expo": {
3
3
  "name": "Clawdex Mobile",
4
4
  "slug": "clawdex-mobile",
5
- "version": "1.0.0",
5
+ "version": "2.0.0",
6
6
  "orientation": "portrait",
7
7
  "icon": "./assets/brand/app-icon.png",
8
8
  "splash": {
9
- "image": "./assets/brand/splash-icon.png",
9
+ "image": "./assets/brand/splash-icon-white.png",
10
10
  "resizeMode": "contain",
11
11
  "backgroundColor": "#000000"
12
12
  },
13
13
  "ios": {
14
- "supportsTablet": true
14
+ "supportsTablet": true,
15
+ "infoPlist": {
16
+ "NSCameraUsageDescription": "Clawdex uses the camera to scan bridge pairing QR codes.",
17
+ "NSMicrophoneUsageDescription": "Clawdex uses the microphone for voice-to-text input.",
18
+ "ITSAppUsesNonExemptEncryption": false
19
+ },
20
+ "bundleIdentifier": "com.mohitpatil973.clawdexmobile"
15
21
  },
16
22
  "android": {
23
+ "usesCleartextTraffic": true,
17
24
  "adaptiveIcon": {
18
25
  "foregroundImage": "./assets/brand/adaptive-icon.png",
19
26
  "backgroundColor": "#000000"
20
27
  },
21
- "edgeToEdgeEnabled": true
28
+ "permissions": [
29
+ "android.permission.CAMERA",
30
+ "android.permission.RECORD_AUDIO",
31
+ "android.permission.MODIFY_AUDIO_SETTINGS",
32
+ "android.permission.FOREGROUND_SERVICE",
33
+ "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
34
+ ],
35
+ "package": "com.mohitpatil973.clawdexmobile"
22
36
  },
23
37
  "web": {
24
38
  "favicon": "./assets/brand/favicon.png"
@@ -28,6 +42,18 @@
28
42
  "android",
29
43
  "web"
30
44
  ],
31
- "jsEngine": "hermes"
45
+ "experiments": {
46
+ "reactCompiler": true
47
+ },
48
+ "jsEngine": "hermes",
49
+ "plugins": [
50
+ "./plugins/withAndroidCleartextTraffic",
51
+ "expo-audio"
52
+ ],
53
+ "extra": {
54
+ "eas": {
55
+ "projectId": "07937be8-e2f7-4731-8ea4-cefae952df79"
56
+ }
57
+ }
32
58
  }
33
59
  }
@@ -0,0 +1,30 @@
1
+ {
2
+ "cli": {
3
+ "version": ">= 18.0.6",
4
+ "appVersionSource": "remote"
5
+ },
6
+ "build": {
7
+ "development": {
8
+ "developmentClient": true,
9
+ "distribution": "internal",
10
+ "env": {
11
+ "EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH": "true"
12
+ }
13
+ },
14
+ "preview": {
15
+ "distribution": "internal",
16
+ "env": {
17
+ "EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH": "true"
18
+ }
19
+ },
20
+ "production": {
21
+ "autoIncrement": true,
22
+ "env": {
23
+ "EXPO_PUBLIC_ALLOW_QUERY_TOKEN_AUTH": "true"
24
+ }
25
+ }
26
+ },
27
+ "submit": {
28
+ "production": {}
29
+ }
30
+ }