Skip to content

Commit 60c0e50

Browse files
msluszniakclaude
andcommitted
fix: update demo apps for Expo 55 / RN 0.83 compatibility
- Bump @react-navigation/native to ^7.2.2 to align @react-navigation/core versions and fix "Element type is invalid" crash in Drawer/Stack navigators - Replace DrawerToggleButton with custom SVG hamburger icon (broken PNG assets in @react-navigation/drawer 7.9.x) - Replace react-native-device-info with expo-constants in CV app (crashes on RN 0.83 New Architecture due to module-level NativeEventEmitter) - Move createSynchronizable calls inside component in vision_camera screen (fails at module scope before worklets runtime is ready) - Remove react-native-audio-api/metro-config from speech and llm metro configs (not published in 0.11.7) - Fix react-native-audio-api 0.11.7 API changes in voice_chat screen - Upgrade @shopify/react-native-skia to 2.6.2 in CV app - Add POST-PREBUILD.md documenting required native patches after expo prebuild (Buildship stub cleanup, stale pods) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 31bdd99 commit 60c0e50

14 files changed

Lines changed: 1803 additions & 844 deletions

File tree

apps/POST-PREBUILD.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!-- cspell:ignore prebuild Buildship getenv Podfile -->
2+
3+
# Post-prebuild patches
4+
5+
After running `npx expo prebuild --clean`, the following manual patches must be applied to the generated native directories. These work around issues in the Expo 55 / RN 0.83 monorepo setup that cannot be solved through config plugins yet.
6+
7+
## Android: Buildship stub cleanup in `settings.gradle`
8+
9+
**Affected apps:** computer-vision, llm, speech, text-embeddings
10+
11+
Gradle's Buildship plugin creates stub directories inside each app's `node_modules/` during project sync. These stubs shadow real packages hoisted to the workspace root, breaking autolinking.
12+
13+
In each app's `android/settings.gradle`, add the following block inside `extensions.configure(com.facebook.react.ReactSettingsExtension)`, **before** the `autolinkLibrariesFromCommand` call:
14+
15+
```groovy
16+
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
17+
// Remove Buildship/Gradle stubs that shadow real packages in the workspace-root node_modules.
18+
def localNM = new File(rootDir, "../node_modules")
19+
if (localNM.exists()) {
20+
localNM.eachDir { dir ->
21+
if (dir.name.startsWith("@")) {
22+
dir.eachDir { scopedDir ->
23+
if (!new File(scopedDir, "package.json").exists()) {
24+
scopedDir.deleteDir()
25+
}
26+
}
27+
} else if (!new File(dir, "package.json").exists()) {
28+
dir.deleteDir()
29+
}
30+
}
31+
}
32+
33+
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
34+
ex.autolinkLibrariesFromCommand()
35+
} else {
36+
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
37+
}
38+
}
39+
```
40+
41+
## iOS: Stale pods after prebuild
42+
43+
**Affected apps:** all Expo apps
44+
45+
After prebuild, the generated `Podfile.lock` may reference stale versions (e.g. `React-Core-prebuilt 0.81.5` instead of `0.83.4`). Run a clean pod install:
46+
47+
```bash
48+
cd apps/<app-name>/ios
49+
rm -rf Pods Podfile.lock
50+
pod install
51+
```
52+
53+
## iOS: Buildship stubs breaking pod install
54+
55+
**Affected apps:** all Expo apps
56+
57+
The same Buildship stubs that affect Android also break iOS autolinking during `pod install`. Before running `pod install`, clean them:
58+
59+
```bash
60+
# From the repo root
61+
for app in apps/computer-vision apps/speech apps/llm apps/text-embeddings; do
62+
find "$app/node_modules" -maxdepth 2 -type d ! -exec test -f '{}/package.json' \; -print 2>/dev/null | while read dir; do
63+
# Skip the node_modules dir itself and scoped package parents
64+
if [ "$(basename "$dir")" != "node_modules" ]; then
65+
rm -rf "$dir"
66+
fi
67+
done
68+
done
69+
```
70+
71+
Alternatively, delete the entire `apps/<app-name>/node_modules/` directory before running `pod install` — yarn workspace hoisting means the app-local `node_modules` should only contain symlinks, not real packages.

apps/computer-vision/app/_layout.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,59 @@ import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetch
44

55
import ColorPalette from '../colors';
66
import React, { useState } from 'react';
7-
import { Text, StyleSheet, View } from 'react-native';
7+
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';
88

99
import {
1010
DrawerContentComponentProps,
1111
DrawerContentScrollView,
1212
DrawerItemList,
1313
} from '@react-navigation/drawer';
14+
import { DrawerActions } from '@react-navigation/native';
15+
import { useNavigation } from 'expo-router';
16+
import Svg, { Rect } from 'react-native-svg';
1417
import { GeneratingContext } from '../context';
1518

1619
initExecutorch({
1720
resourceFetcher: ExpoResourceFetcher,
1821
});
1922

23+
function HamburgerIcon({ tintColor }: { tintColor?: string }) {
24+
const navigation = useNavigation();
25+
return (
26+
<TouchableOpacity
27+
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
28+
style={{ marginLeft: 12 }}
29+
>
30+
<Svg width={24} height={24} viewBox="0 0 24 24">
31+
<Rect
32+
x={2}
33+
y={4}
34+
width={20}
35+
height={2}
36+
rx={1}
37+
fill={tintColor ?? '#000'}
38+
/>
39+
<Rect
40+
x={2}
41+
y={11}
42+
width={20}
43+
height={2}
44+
rx={1}
45+
fill={tintColor ?? '#000'}
46+
/>
47+
<Rect
48+
x={2}
49+
y={18}
50+
width={20}
51+
height={2}
52+
rx={1}
53+
fill={tintColor ?? '#000'}
54+
/>
55+
</Svg>
56+
</TouchableOpacity>
57+
);
58+
}
59+
2060
interface CustomDrawerProps extends DrawerContentComponentProps {
2161
isGenerating: boolean;
2262
}
@@ -57,6 +97,7 @@ export default function _layout() {
5797
drawerInactiveTintColor: '#888',
5898
headerTintColor: ColorPalette.primary,
5999
headerTitleStyle: { color: ColorPalette.primary },
100+
headerLeft: (props) => <HamburgerIcon tintColor={props.tintColor} />,
60101
}}
61102
>
62103
<Drawer.Screen

apps/computer-vision/app/vision_camera/index.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,13 @@ const TASKS: Task[] = [
118118
},
119119
];
120120

121-
const frameKillSwitch = createSynchronizable(false);
122-
const cameraPositionSync = createSynchronizable<'front' | 'back'>('back');
123-
124121
export default function VisionCameraScreen() {
125122
const insets = useSafeAreaInsets();
126123
const router = useRouter();
124+
const [frameKillSwitch] = useState(() => createSynchronizable(false));
125+
const [cameraPositionSync] = useState(() =>
126+
createSynchronizable<'front' | 'back'>('back')
127+
);
127128
const [activeTask, setActiveTask] = useState<TaskId>('classification');
128129
const [activeModel, setActiveModel] = useState<ModelId>('classification');
129130
const [canvasSize, setCanvasSize] = useState({ width: 1, height: 1 });
@@ -163,11 +164,11 @@ export default function VisionCameraScreen() {
163164
frameKillSwitch.setBlocking(false);
164165
}, 300);
165166
return () => clearTimeout(id);
166-
}, [activeModel]);
167+
}, [activeModel, frameKillSwitch]);
167168

168169
useEffect(() => {
169170
cameraPositionSync.setBlocking(cameraPosition);
170-
}, [cameraPosition]);
171+
}, [cameraPosition, cameraPositionSync]);
171172

172173
const handleFpsChange = useCallback((newFps: number, newMs: number) => {
173174
setFps(newFps);

apps/computer-vision/components/BottomBar.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ColorPalette from '../colors';
22
import FontAwesome from '@expo/vector-icons/FontAwesome';
3-
import DeviceInfo from 'react-native-device-info';
3+
import Constants from 'expo-constants';
44
import { View, TouchableOpacity, StyleSheet, Text } from 'react-native';
55
import { useSafeAreaInsets } from 'react-native-safe-area-context';
66

@@ -25,14 +25,12 @@ export const BottomBar = ({
2525
<FontAwesome name="photo" size={24} color={ColorPalette.primary} />
2626
</TouchableOpacity>
2727
<TouchableOpacity
28-
onPress={() =>
29-
!DeviceInfo.isEmulatorSync() && handleCameraPress(true)
30-
}
28+
onPress={() => Constants.isDevice && handleCameraPress(true)}
3129
>
3230
<FontAwesome
3331
name="camera"
3432
size={24}
35-
color={DeviceInfo.isEmulatorSync() ? '#888' : ColorPalette.primary}
33+
color={Constants.isDevice ? ColorPalette.primary : '#888'}
3634
/>
3735
</TouchableOpacity>
3836
</View>

apps/computer-vision/package.json

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,41 @@
1111
"lint": "eslint . --ext .ts,.tsx --fix"
1212
},
1313
"dependencies": {
14-
"@react-native/metro-config": "^0.81.5",
15-
"@react-navigation/drawer": "^7.8.1",
16-
"@react-navigation/native": "^7.1.28",
17-
"@shopify/react-native-skia": "2.4.21",
18-
"expo": "^54.0.27",
19-
"expo-build-properties": "~1.0.10",
20-
"expo-constants": "~18.0.11",
21-
"expo-font": "~14.0.10",
22-
"expo-linking": "~8.0.10",
23-
"expo-router": "~6.0.17",
24-
"expo-status-bar": "~3.0.9",
25-
"metro-config": "^0.81.5",
26-
"react": "19.1.0",
27-
"react-native": "0.81.5",
28-
"react-native-device-info": "^15.0.2",
14+
"@expo/log-box": "~55.0.10",
15+
"@react-native/metro-config": "^0.83.0",
16+
"@react-navigation/drawer": "^7.9.4",
17+
"@react-navigation/native": "^7.2.2",
18+
"@shopify/react-native-skia": "2.6.2",
19+
"expo": "^55.0.13",
20+
"expo-build-properties": "~55.0.13",
21+
"expo-constants": "~55.0.13",
22+
"expo-font": "~55.0.6",
23+
"expo-linking": "~55.0.12",
24+
"expo-router": "~55.0.11",
25+
"expo-status-bar": "~55.0.5",
26+
"metro-config": "^0.83.0",
27+
"react": "19.2.5",
28+
"react-native": "0.83.4",
2929
"react-native-executorch": "workspace:*",
3030
"react-native-executorch-expo-resource-fetcher": "workspace:*",
31-
"react-native-gesture-handler": "~2.28.0",
32-
"react-native-image-picker": "^7.2.2",
31+
"react-native-gesture-handler": "~2.31.1",
32+
"react-native-image-picker": "^8.2.1",
3333
"react-native-loading-spinner-overlay": "^3.0.1",
3434
"react-native-nitro-image": "0.13.0",
3535
"react-native-nitro-modules": "0.35.2",
36-
"react-native-reanimated": "~4.2.2",
37-
"react-native-safe-area-context": "~5.6.0",
38-
"react-native-screens": "~4.16.0",
39-
"react-native-svg": "15.15.3",
36+
"react-native-reanimated": "~4.3.0",
37+
"react-native-safe-area-context": "~5.7.0",
38+
"react-native-screens": "~4.24.0",
39+
"react-native-svg": "15.15.4",
4040
"react-native-svg-transformer": "^1.5.3",
4141
"react-native-vision-camera": "5.0.0-beta.7",
42-
"react-native-worklets": "0.7.4"
42+
"react-native-worklets": "0.8.1"
4343
},
4444
"devDependencies": {
4545
"@babel/core": "^7.29.0",
4646
"@types/pngjs": "^6.0.5",
47-
"@types/react": "~19.2.0"
47+
"@types/react": "~19.2.0",
48+
"babel-preset-expo": "~55.0.16"
4849
},
4950
"private": true
5051
}

apps/llm/app/_layout.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,54 @@ import { initExecutorch } from 'react-native-executorch';
33
import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetcher';
44
import ColorPalette from '../colors';
55
import React, { useState } from 'react';
6-
import { Text, StyleSheet, View } from 'react-native';
6+
import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';
77
import {
88
DrawerContentComponentProps,
99
DrawerContentScrollView,
1010
DrawerItemList,
11-
DrawerToggleButton,
1211
} from '@react-navigation/drawer';
12+
import { DrawerActions } from '@react-navigation/native';
13+
import { useNavigation } from 'expo-router';
14+
import Svg, { Rect } from 'react-native-svg';
1315
import { GeneratingContext } from '../context';
1416

17+
function HamburgerIcon({ tintColor }: { tintColor?: string }) {
18+
const navigation = useNavigation();
19+
return (
20+
<TouchableOpacity
21+
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
22+
style={{ marginLeft: 12 }}
23+
>
24+
<Svg width={24} height={24} viewBox="0 0 24 24">
25+
<Rect
26+
x={2}
27+
y={4}
28+
width={20}
29+
height={2}
30+
rx={1}
31+
fill={tintColor ?? '#000'}
32+
/>
33+
<Rect
34+
x={2}
35+
y={11}
36+
width={20}
37+
height={2}
38+
rx={1}
39+
fill={tintColor ?? '#000'}
40+
/>
41+
<Rect
42+
x={2}
43+
y={18}
44+
width={20}
45+
height={2}
46+
rx={1}
47+
fill={tintColor ?? '#000'}
48+
/>
49+
</Svg>
50+
</TouchableOpacity>
51+
);
52+
}
53+
1554
initExecutorch({
1655
resourceFetcher: ExpoResourceFetcher,
1756
});
@@ -56,9 +95,7 @@ export default function _layout() {
5695
drawerInactiveTintColor: '#888',
5796
headerTintColor: ColorPalette.primary,
5897
headerTitleStyle: { color: ColorPalette.primary },
59-
headerLeft: () => (
60-
<DrawerToggleButton tintColor={ColorPalette.primary} />
61-
),
98+
headerLeft: (props) => <HamburgerIcon tintColor={props.tintColor} />,
6299
}}
63100
>
64101
<Drawer.Screen

apps/llm/app/voice_chat/index.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,7 @@ function VoiceChatScreen() {
6969
useState<STTModelSources>(WHISPER_TINY_EN);
7070
const [error, setError] = useState<string | null>(null);
7171

72-
const [recorder] = useState(
73-
() =>
74-
new AudioRecorder({
75-
sampleRate: 16000,
76-
bufferLengthInSamples: 1600,
77-
})
78-
);
72+
const [recorder] = useState(() => new AudioRecorder());
7973

8074
const { setGlobalGenerating } = useContext(GeneratingContext);
8175

@@ -92,7 +86,7 @@ function VoiceChatScreen() {
9286
AudioManager.setAudioSessionOptions({
9387
iosCategory: 'playAndRecord',
9488
iosMode: 'spokenAudio',
95-
iosOptions: ['allowBluetooth', 'defaultToSpeaker'],
89+
iosOptions: ['allowBluetoothHFP', 'defaultToSpeaker'],
9690
});
9791
AudioManager.requestRecordingPermissions();
9892
}, []);
@@ -106,9 +100,12 @@ function VoiceChatScreen() {
106100
setIsRecording(true);
107101
setLiveTranscription('');
108102

109-
recorder.onAudioReady(({ buffer }) => {
110-
speechToText.streamInsert(buffer.getChannelData(0));
111-
});
103+
recorder.onAudioReady(
104+
{ sampleRate: 16000, bufferLength: 1600, channelCount: 1 },
105+
(event) => {
106+
speechToText.streamInsert(event.buffer.getChannelData(0));
107+
}
108+
);
112109
recorder.start();
113110

114111
let finalResult = '';

apps/llm/metro.config.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
const { getDefaultConfig } = require('expo/metro-config');
2-
const {
3-
wrapWithAudioAPIMetroConfig,
4-
} = require('react-native-audio-api/metro-config');
52

63
const config = getDefaultConfig(__dirname);
74

@@ -19,4 +16,4 @@ config.resolver = {
1916

2017
config.resolver.assetExts.push('pte');
2118

22-
module.exports = wrapWithAudioAPIMetroConfig(config);
19+
module.exports = config;

0 commit comments

Comments
 (0)