Skip to main content 홈 크리에이터 zaferayan Skills zafer-skills
zafer-skills Expo React Native mobile app development with RevenueCat payments, AdMob ads, i18n localization, onboarding flow, paywall, and NativeTabs navigation
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/zaferayan/skills --skill zafer-skills명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 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)
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" ;
player = ( , {
player. = ;
player. = ;
player. ();
});
;
const
useVideoPlayer
VIDEO_URL
(player ) =>
loop
true
muted
true
play
<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)
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
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:
{
"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.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
ü, Ü, ö, Ö, ç, Ç, ş, Ş, ğ, Ğ
✅ "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.
const renderItem = ( ) => {
const animatedStyle = useAnimatedStyle (() => ({ opacity : 1 }));
return <Animated.View style ={animatedStyle} /> ;
};
function MyComponent ( ) {
const animatedStyle = useAnimatedStyle (() => ({ opacity : 1 }));
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 > ;
}
renderItem={({ item } ) => <AnimatedItem item ={item} /> }
POST-CREATION CLEANUP (ALWAYS DO) After creating a new Expo project, you MUST:
If using (tabs) folder, DELETE src/app/index.tsx to avoid route conflicts:
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:
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 --fix fixes dependency version mismatches
prebuild --clean recreates ios and android folders
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:
bunx create-expo -t default@next app-name
Update app.json with the bundle ID:
{
"expo" : {
"ios" : {
"bundleIdentifier" : "com.company.appname"
} ,
"android" : {
"package" : "com.company.appname"
}
}
}
Then cd into the project and start implementing all required screens
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.
import "expo-sqlite/localStorage/install" ;
globalThis.localStorage .setItem ("key" , "value" );
console .log (globalThis.localStorage .getItem ("key" ));
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:
import { NativeTabs } from "expo-router/unstable-native-tabs" ;
export default function TabLayout ( ) {
return (
<NativeTabs >
<NativeTabs.Trigger name ="index" >
<NativeTabs.Trigger.Label > Home</NativeTabs.Trigger.Label >
<NativeTabs.Trigger.Icon sf ="house.fill" md ="home" />
</NativeTabs.Trigger >
<NativeTabs.Trigger name ="explore" >
<NativeTabs.Trigger.Label > Explore</NativeTabs.Trigger.Label >
<NativeTabs.Trigger.Icon sf ="compass.fill" md ="explore" />
</NativeTabs.Trigger >
<NativeTabs.Trigger name ="settings" >
<NativeTabs.Trigger.Label > Settings</NativeTabs.Trigger.Label >
<NativeTabs.Trigger.Icon sf ="gear" md ="settings" />
</NativeTabs.Trigger >
</NativeTabs >
);
}
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 bun install
bun start
bun ios
bun android
bun lint
npx expo install --fix
npx expo prebuild --clean
EAS Build Commands 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
const handleComplete = async ( ) => {
await setOnboardingCompleted (true );
router.replace ('/paywall' );
};
const handleContinue = ( ) => {
router.replace ('/(tabs)' );
};
Flow: Onboarding → Paywall → Main App (tabs)
Paywall Subscription Options (REQUIRED) Paywall MUST have two subscription options:
Weekly - Default option
Yearly - With "50% OFF" badge (recommended, should be highlighted)
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 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:
Language - Change app language
Theme - Light/Dark/System
Notifications - Enable/disable notifications
Remove Ads - Navigate to paywall (hidden if already premium)
Reset Onboarding - Restart onboarding flow (for testing/demo)
const { isPremium } = usePurchases ();
const handleRemoveAds = ( ) => {
router.push ('/paywall' );
};
const handleResetOnboarding = async ( ) => {
await setOnboardingCompleted (false );
router.replace ('/onboarding' );
};
{!isPremium && (
<SettingsItem
title ={t( 'settings.removeAds ')}
icon ="crown.fill"
onPress ={handleRemoveAds}
/>
)}
<SettingsItem
title={t ('settings.resetOnboarding' )}
icon="arrow.counterclockwise"
onPress={handleResetOnboarding}
/>
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 <ThemeProvider >
<OnboardingProvider >
<AdsProvider >
<Stack />
</AdsProvider >
</OnboardingProvider >
</ThemeProvider >
useColorScheme Hook File: src/hooks/use-color-scheme.ts
import { useThemeContext } from '@/context/theme-context' ;
export function useColorScheme ( ): 'light' | 'dark' | 'unspecified' {
const { isDark } = useThemeContext ();
return isDark ? 'dark' : 'light' ;
}
Important Notes
iOS permissions are defined in app.json
Android permissions are defined in app.json
Enable new architecture via newArchEnabled: true
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 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.