/SKILL.md
Expo React Native mobile app development with RevenueCat payments, AdMob ads, i18n localization, onboarding flow, paywall, and NativeTabs navigation
npx skillsauth add mrshrey007/skills zafer-skillsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
When creating a new Expo project, you MUST include ALL of the following:
src/app/onboarding.tsx - Swipe-based onboarding with fullscreen background video and gradient overlaysrc/app/paywall.tsx - RevenueCat paywall screen (shown after onboarding)src/app/settings.tsx - Settings screen with language, theme, notifications, and reset onboarding optionsThe 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.
NativeTabs from expo-router/unstable-native-tabs for tab navigation - NEVER use @react-navigation/bottom-tabs or Tabs from expo-routerimport { 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>;
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)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:
ca-app-pub-3940256099942544~1458002511ca-app-pub-3940256099942544~3347511713Do NOT skip this configuration or the app will crash with GADInvalidInitializationException.
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,
},
});
TestIds.BANNER in developmentuseAds context to check shouldShowAds (hides for premium users)When writing tr.json, you MUST use correct Turkish characters:
Example:
expo-sqlite/localStorage/install insteadTabs from expo-router - Use NativeTabs instead@react-navigation/bottom-tabs - Use NativeTabs insteadexpo-av - Use expo-video for video, expo-audio for audio insteadexpo-ads-admob - Use react-native-google-mobile-ads insteadreact-native-google-mobile-adsNEVER 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} />}
After creating a new Expo project, you MUST:
(tabs) folder, DELETE src/app/index.tsx to avoid route conflicts:rm src/app/index.tsx
lineHeight from these files:src/components/themed-text.tsx (comes with lineHeight by default - REMOVE IT)lineHeightSearch and remove all lineHeight occurrences:
grep -r "lineHeight" src/
Replace with padding or margin instead.
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 mismatchesprebuild --clean recreates ios and android foldersDo NOT skip these steps.
When user asks to create an app, you MUST:
bunx create-expo -t default@next app-name
app.json with the bundle ID:{
"expo": {
"ios": {
"bundleIdentifier": "com.company.appname"
},
"android": {
"package": "com.company.appname"
}
}
}
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")); // 'value'
WARNING: NEVER USE
lineHeight! It causes layout issues in React Native. Use padding or margin instead.
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
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>
);
}
| 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 |
bun install
bun start
bun ios
bun android
bun lint
npx expo install --fix
npx expo prebuild --clean
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
lib/purchases.tsapp/paywall.tsxsrc/lib/ads.tssrc/lib/notifications.ts, src/hooks/use-notifications.tssrc/app/onboarding.tsx, src/app/paywall.tsx// In onboarding.tsx - when user completes onboarding:
const handleComplete = async () => {
await setOnboardingCompleted(true);
router.replace('/paywall'); // Navigate to paywall immediately
};
// In paywall.tsx - after purchase or skip:
const handleContinue = () => {
router.replace('/(tabs)'); // Navigate to main app
};
Flow: Onboarding → Paywall → Main App (tabs)
Paywall MUST have two subscription options:
// 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
Settings screen MUST include:
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 && (
<SettingsItem
title={t('settings.removeAds')}
icon="crown.fill"
onPress={handleRemoveAds}
/>
)}
<SettingsItem
title={t('settings.resetOnboarding')}
icon="arrow.counterclockwise"
onPress={handleResetOnboarding}
/>
lib/i18n.tslocales/<ThemeProvider>
<OnboardingProvider>
<AdsProvider>
<Stack />
</AdsProvider>
</OnboardingProvider>
</ThemeProvider>
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';
}
app.jsonapp.jsonnewArchEnabled: trueexperiments.typedRoutesnpx expo prebuild --clean
bun ios
bun android
NOTE:
prebuild --cleanrecreates ios and android folders. Run it after modifying native modules or app.json.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.