--- name: zafer-skills description: Expo React Native mobile app development with RevenueCat payments, AdMob ads, i18n localization, onboarding flow, paywall, and NativeTabs navigation --- # 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: ```tsx 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: ; ``` Do NOT just import expo-video without actually using the VideoView component. ### Required Navigation (ALWAYS USE) - [ ] Use `NativeTabs` from `expo-router/unstable-native-tabs` for tab navigation - NEVER use `@react-navigation/bottom-tabs` or `Tabs` from expo-router ### Required Context Providers (ALWAYS WRAP) ```tsx import { ThemeProvider } from "@/context/theme-context"; import { DarkTheme, DefaultTheme, ThemeProvider as NavigationThemeProvider, } from "@react-navigation/native"; ; ``` ### Required Libraries (ALWAYS INSTALL) Use `npx expo install` to install libraries (NOT npm/yarn/bun install): ```bash 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-notifications` - `i18next` + `react-i18next` + `expo-localization` - `react-native-reanimated` - `expo-video` + `expo-audio` - `expo-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: ```json { "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: ```tsx 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 ( {t('tabs.home')} {t('tabs.settings')} {shouldShowAds && ( )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, adContainer: { alignItems: 'center', paddingBottom: 10, }, }); ``` - ALWAYS use `TestIds.BANNER` in development - Banner ad is placed below NativeTabs in the Tab layout - Use `useAds` context to check `shouldShowAds` (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/install` instead - ❌ lineHeight style - Use padding/margin instead - ❌ `Tabs` from expo-router - Use `NativeTabs` instead - ❌ `@react-navigation/bottom-tabs` - Use `NativeTabs` instead - ❌ `expo-av` - Use `expo-video` for video, `expo-audio` for audio instead - ❌ `expo-ads-admob` - Use `react-native-google-mobile-ads` instead - ❌ 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: ```tsx const renderItem = () => { const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 })); // ERROR! return ; }; ``` ✅ CORRECT: ```tsx function MyComponent() { const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 })); // Top level return ; } ``` For lists, create a separate component for each item: ```tsx function AnimatedItem({ item }) { const animatedStyle = useAnimatedStyle(() => ({ opacity: 1 })); return {item.name}; } // In FlatList: renderItem={({ item }) => } ``` ### POST-CREATION CLEANUP (ALWAYS DO) After creating a new Expo project, you MUST: 1. If using `(tabs)` folder, DELETE `src/app/index.tsx` to avoid route conflicts: ```bash rm src/app/index.tsx ``` 2. Check and remove `lineHeight` from 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: ```bash 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: ```bash npx expo install --fix npx expo prebuild --clean ``` 1. `install --fix` fixes dependency version mismatches 2. `prebuild --clean` recreates ios and android folders Do NOT skip these steps. --- ## Project Creation When user asks to create an app, you MUST: 1. FIRST ask for the bundle ID (e.g., "What is the bundle ID? Example: com.company.appname") 2. Create the project in the CURRENT directory using: ```bash bunx create-expo -t default@next app-name ``` 3. Update `app.json` with the bundle ID: ```json { "expo": { "ios": { "bundleIdentifier": "com.company.appname" }, "android": { "package": "com.company.appname" } } } ``` 4. Then cd into the project and start implementing all required screens 5. Do NOT ask for project path - always use current directory ## Technology Stack - **Framework**: Expo, React Native - **Navigation**: Expo Router (file-based routing), NativeTabs - **State Management**: React Context API - **Translations**: i18next, react-i18next - **Purchases**: RevenueCat (react-native-purchases) - **Advertisements**: Google AdMob (react-native-google-mobile-ads) - **Notifications**: expo-notifications - **Animations**: react-native-reanimated - **Storage**: localStorage via expo-sqlite polyfill > **WARNING**: DO NOT USE AsyncStorage! Use expo-sqlite polyfill instead. - Example usage ```js import "expo-sqlite/localStorage/install"; globalThis.localStorage.setItem("key", "value"); console.log(globalThis.localStorage.getItem("key")); // 'value' ``` > **WARNING**: NEVER USE `lineHeight`! It causes layout issues in React Native. Use padding or margin instead. ## Project Structure ``` project-root/ ├── src/ │ ├── app/ │ │ ├── _layout.tsx │ │ ├── index.tsx │ │ ├── explore.tsx │ │ ├── settings.tsx │ │ ├── paywall.tsx │ │ └── onboarding.tsx │ ├── components/ │ │ ├── ui/ │ │ ├── themed-text.tsx │ │ └── themed-view.tsx │ ├── constants/ │ │ ├── theme.ts │ │ └── [data-files].ts │ ├── context/ │ │ ├── onboarding-context.tsx │ │ └── ads-context.tsx │ ├── hooks/ │ │ ├── use-notifications.ts │ │ └── use-color-scheme.ts │ ├── lib/ │ │ ├── notifications.ts │ │ ├── purchases.ts │ │ ├── ads.ts │ │ └── i18n.ts │ └── locales/ │ ├── tr.json │ └── en.json ├── assets/ │ └── images/ ├── ios/ ├── android/ ├── app.json ├── eas.json ├── package.json └── tsconfig.json ``` ## Tab Navigation (NativeTabs) Expo Router uses NativeTabs for native tab navigation: ```tsx import { NativeTabs } from "expo-router/unstable-native-tabs"; export default function TabLayout() { return ( Home Explore Settings ); } ``` ### NativeTabs Properties - **sf**: SF Symbols icon name (iOS) - **md**: Material Design icon name (Android) - **name**: Route file name - Tab order follows trigger order ### Common Icons | Purpose | SF Symbol | Material Icon | | ------------- | --------------- | ------------- | | Home | house.fill | home | | Explore | compass.fill | explore | | Settings | gear | settings | | Profile | person.fill | person | | Search | magnifyingglass | search | | Favorites | heart.fill | favorite | | Notifications | bell.fill | notifications | ## Development Commands ```bash bun install bun start bun ios bun android bun lint npx expo install --fix npx expo prebuild --clean ``` ## EAS Build Commands ```bash eas build --profile development --platform ios eas build --profile development --platform android eas build --profile production --platform ios eas build --profile production --platform android eas submit --platform ios eas submit --platform android ``` ## Important Modules ### RevenueCat - File: `lib/purchases.ts` - Used for premium access - Paywall: `app/paywall.tsx` ### AdMob - File: `src/lib/ads.ts` - Ads disabled for premium users - Test IDs must be used in development ### Notifications - Files: `src/lib/notifications.ts`, `src/hooks/use-notifications.ts` - iOS requires push notification entitlement ### Onboarding & Paywall Flow (CRITICAL) - Files: `src/app/onboarding.tsx`, `src/app/paywall.tsx` - Swipe-based screens with fullscreen background video - Gradient overlay on video - **IMPORTANT**: Paywall MUST appear immediately after onboarding completes ```tsx // In onboarding.tsx - when user completes onboarding: const handleComplete = async () => { await setOnboardingCompleted(true); router.replace('/paywall'); // Navigate to paywall immediately }; ``` ```tsx // In paywall.tsx - after purchase or skip: const handleContinue = () => { router.replace('/(tabs)'); // Navigate to main app }; ``` Flow: `Onboarding → Paywall → Main App (tabs)` ### Paywall Subscription Options (REQUIRED) Paywall MUST have two subscription options: 1. **Weekly** - Default option 2. **Yearly** - With "50% OFF" badge (recommended, should be highlighted) ```tsx // Subscription option component example: const subscriptionOptions = [ { id: 'weekly', title: t('paywall.weekly'), price: '$4.99/week', selected: selectedPlan === 'weekly', }, { id: 'yearly', title: t('paywall.yearly'), price: '$129.99/year', badge: '50% OFF', selected: selectedPlan === 'yearly', }, ]; // Yearly option should be visually highlighted as the best value ``` - Yearly option should show the discount badge prominently - Default selection can be weekly, but yearly should be visually recommended - Use RevenueCat package identifiers to match these options ### Settings Screen Options (REQUIRED) Settings screen MUST include: 1. **Language** - Change app language 2. **Theme** - Light/Dark/System 3. **Notifications** - Enable/disable notifications 4. **Remove Ads** - Navigate to paywall (hidden if already premium) 5. **Reset Onboarding** - Restart onboarding flow (for testing/demo) ```tsx const { isPremium } = usePurchases(); // Remove Ads - navigates to paywall const handleRemoveAds = () => { router.push('/paywall'); }; // Reset onboarding const handleResetOnboarding = async () => { await setOnboardingCompleted(false); router.replace('/onboarding'); }; // In settings list: {!isPremium && ( )} ``` ## Localization - File: `lib/i18n.ts` - Languages stored in `locales/` - App restarts on language change ## Coding Standards - Use functional components - Strict TypeScript - Avoid hardcoded strings - Use padding instead of lineHeight - Use memoization when necessary ## Context Providers ```tsx ``` ## useColorScheme Hook File: `src/hooks/use-color-scheme.ts` ```tsx import { useThemeContext } from '@/context/theme-context'; export function useColorScheme(): 'light' | 'dark' | 'unspecified' { const { isDark } = useThemeContext(); return isDark ? 'dark' : 'light'; } ``` ## Important Notes 1. iOS permissions are defined in `app.json` 2. Android permissions are defined in `app.json` 3. Enable new architecture via `newArchEnabled: true` 4. Enable typed routes via `experiments.typedRoutes` ## App Store & Play Store Notes - iOS ATT permission required - Restore purchases must work correctly - Target SDK must be up to date ## Testing Checklist - UI tested in all languages - Dark / Light mode - Notifications - Premium flow - Restore purchases - Offline support - Multiple screen sizes ## After Development ```bash npx expo prebuild --clean bun ios bun android ``` > NOTE: `prebuild --clean` recreates ios and android folders. Run it after modifying native modules or app.json.