Expo Mobile Application Development Guide
IMPORTANT: This is a SKILL file, NOT a project. NEVER run npm/bun install in this folder. NEVER create code files here. When creating a new project, ALWAYS ask the user for the project path first or create it in a separate directory (e.g.,
~/Projects/app-name).
This guide is created to provide context when working with Expo projects using Claude Code.
MANDATORY REQUIREMENTS
When creating a new Expo project, you MUST include ALL of the following:
Required Screens (ALWAYS CREATE)
-
src/app/onboarding.tsx- Swipe-based onboarding with fullscreen background video and gradient overlay -
src/app/paywall.tsx- RevenueCat paywall screen (shown after onboarding) -
src/app/settings.tsx- Settings screen with language, theme, notifications, and reset onboarding options
Onboarding Video Implementation (REQUIRED)
The onboarding screen MUST have a fullscreen background video. Use a URL, not a local file:
import { useVideoPlayer, VideoView } from "expo-video";
const VIDEO_URL =
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
const player = useVideoPlayer(VIDEO_URL, (player) => {
player.loop = true;
player.muted = true;
player.play();
});
// In render:
<VideoView
player={player}
style={StyleSheet.absoluteFill}
contentFit="cover"
nativeControls={false}
/>;
Do NOT just import expo-video without actually using the VideoView component.
Required Navigation (ALWAYS USE)
- Use
NativeTabsfromexpo-router/unstable-native-tabsfor tab navigation - NEVER use@react-navigation/bottom-tabsorTabsfrom expo-router
Required Context Providers (ALWAYS WRAP)
import { ThemeProvider } from "@/context/theme-context";
import {
DarkTheme,
DefaultTheme,
ThemeProvider as NavigationThemeProvider,
} from "@react-navigation/native";
<ThemeProvider>
<OnboardingProvider>
<AdsProvider>
<NavigationThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<Stack />
</NavigationThemeProvider>
</AdsProvider>
</OnboardingProvider>
</ThemeProvider>;
Required Libraries (ALWAYS INSTALL)
Use npx expo install to install libraries (NOT npm/yarn/bun install):
npx expo install react-native-purchases react-native-google-mobile-ads expo-notifications i18next react-i18next expo-localization react-native-reanimated expo-video expo-audio expo-sqlite expo-linear-gradient
Libraries:
react-native-purchases(RevenueCat)react-native-google-mobile-ads(AdMob)expo-notificationsi18next+react-i18next+expo-localizationreact-native-reanimatedexpo-video+expo-audioexpo-sqlite(for localStorage)expo-linear-gradient(for gradient overlays)
AdMob Configuration (REQUIRED in app.json)
You MUST add this to app.json for AdMob to work:
{
"expo": {
"plugins": [
[
"react-native-google-mobile-ads",
{
"androidAppId": "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy",
"iosAppId": "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"
}
]
]
}
}
For development/testing, use test App IDs:
- iOS:
ca-app-pub-3940256099942544~1458002511 - Android:
ca-app-pub-3940256099942544~3347511713
Do NOT skip this configuration or the app will crash with GADInvalidInitializationException.
Banner Ad Implementation (REQUIRED)
You MUST implement banner ads in the Tab layout. Use this pattern:
import { View, StyleSheet } from 'react-native';
import { NativeTabs } from 'expo-router/unstable-native-tabs';
import { useTranslation } from 'react-i18next';
import { BannerAd, BannerAdSize, TestIds } from 'react-native-google-mobile-ads';
import { useAds } from '@/context/ads-context';
const adUnitId = __DEV__
? TestIds.BANNER
: 'ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy';
export default function TabLayout() {
const { t } = useTranslation();
const { shouldShowAds } = useAds();
return (
<View style={styles.container}>
<NativeTabs>
<NativeTabs.Trigger name="index">
<NativeTabs.Trigger.Label>{t('tabs.home')}</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="house.fill" md="home" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="settings">
<NativeTabs.Trigger.Label>{t('tabs.settings')}</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
</NativeTabs.Trigger>
</NativeTabs>
{shouldShowAds && (
<View style={styles.adContainer}>
<BannerAd
unitId={adUnitId}
size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER}
requestOptions={{
requestNonPersonalizedAdsOnly: true,
}}
/>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
adContainer: {
alignItems: 'center',
paddingBottom: 10,
},
});
- ALWAYS use
TestIds.BANNERin development - Banner ad is placed below NativeTabs in the Tab layout
- Use
useAdscontext to checkshouldShowAds(hides for premium users)
TURKISH LOCALIZATION (IMPORTANT)
When writing tr.json, you MUST use correct Turkish characters:
- ı (lowercase dotless i) - NOT i
- İ (uppercase dotted I) - NOT I
- ü, Ü, ö, Ö, ç, Ç, ş, Ş, ğ, Ğ
Example:
- ✅ "Ayarlar", "Giriş", "Çıkış", "Başla", "İleri", "Güncelle"
- ❌ "Ayarlar", "Giris", "Cikis", "Basla", "Ileri", "Guncelle"
FORBIDDEN (NEVER USE)
- ❌ AsyncStorage - Use
expo-sqlite/localStorage/installinstead - ❌ lineHeight style - Use padding/margin instead
- ❌
Tabsfrom expo-router - UseNativeTabsinstead - ❌
@react-navigation/bottom-tabs- UseNativeTabsinstead - ❌
expo-av- Useexpo-videofor video,expo-audiofor audio instead - ❌
expo-ads-admob- Usereact-native-google-mobile-adsinstead - ❌ Any other ads library - ONLY use
react-native-google-mobile-ads - ❌ Reanimated hooks inside callbacks - Call at component top level
Reanimated Usage (IMPORTANT)
NEVER call useAnimatedStyle, useSharedValue, or other reanimated hooks inside callbacks, loops, or conditions.
❌ WRONG:
const renderItem = () => {
const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 })); // ERROR!
return <Animated.View style={animatedStyle} />;
};
✅ CORRECT:
function MyComponent() {
const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 })); // Top level
return <Animated.View style={animatedStyle} />;
}
For lists, create a separate component for each item:
function AnimatedItem({ item }) {
const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 }));
return <Animated.View style={animatedStyle}>{item.name}</Animated.View>;
}
// In FlatList:
renderItem={({ item }) => <AnimatedItem item={item} />}
POST-CREATION CLEANUP (ALWAYS DO)
After creating a new Expo project, you MUST:
- If using
(tabs)folder, DELETEsrc/app/index.tsxto avoid route conflicts:
rm src/app/index.tsx
- Check and remove
lineHeightfrom these files:
src/components/themed-text.tsx(comes with lineHeight by default - REMOVE IT)- Any other component using
lineHeight
Search and remove all lineHeight occurrences:
grep -r "lineHeight" src/
Replace with padding or margin instead.
AFTER COMPLETING CODE (ALWAYS RUN)
When you finish writing/modifying code, you MUST run these commands in order:
npx expo install --fix
npx expo prebuild --clean
install --fixfixes dependency version mismatchesprebuild --cleanrecreates ios and android folders
Do NOT skip these steps.
Project Creation
When user asks to create an app, you MUST:
- FIRST ask for the bundle ID (e.g., "What is the bundle ID? Example: com.company.appname")
- Create the project in the CURRENT directory using:
bun