zaferayan

zafer-skills

44
2
# Install this skill:
npx skills add zaferayan/skills

Or install specific skill: npx add-skill https://github.com/zaferayan/skills

# Description

Expo React Native mobile app development with RevenueCat payments, AdMob ads, i18n localization, onboarding flow, paywall, and NativeTabs navigation

# SKILL.md


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:

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 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)

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-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.

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
  • ü, Ü, ö, Ö, ç, Ç, ş, Ş, ğ, Ğ

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:

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:

  1. If using (tabs) folder, DELETE src/app/index.tsx to avoid route conflicts:
rm src/app/index.tsx
  1. Check and remove lineHeight from these files:

  2. src/components/themed-text.tsx (comes with lineHeight by default - REMOVE IT)

  3. 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
  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:
bunx create-expo -t default@next app-name
  1. Update app.json with the bundle ID:
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.company.appname"
    },
    "android": {
      "package": "com.company.appname"
    }
  }
}
  1. Then cd into the project and start implementing all required screens
  2. 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
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:

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
// 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 Subscription Options (REQUIRED)

Paywall MUST have two subscription options:

  1. Weekly - Default option
  2. Yearly - With "50% OFF" badge (recommended, should be highlighted)
// 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)
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}
/>

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

  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

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.

# README.md

Expo Mobile App Skill

A Claude Code skill for building production-ready Expo React Native mobile applications.

Features

  • RevenueCat - In-app purchases with weekly/yearly subscriptions
  • AdMob - Banner ads with premium user detection
  • i18n - Multi-language support (Turkish/English)
  • Onboarding - Fullscreen video background with swipe screens
  • Paywall - Subscription options with 50% OFF yearly badge
  • NativeTabs - Native tab navigation
  • Theme - Light/Dark/System mode support

Installation

npx skills add zaferayan/skills

What it does

When you ask Claude Code to create an Expo app, this skill ensures:

  1. Required Screens: Onboarding, Paywall, Settings
  2. Proper Flow: Onboarding → Paywall → Main App
  3. Monetization: RevenueCat + AdMob integration
  4. Best Practices: No AsyncStorage, no lineHeight, NativeTabs only

Usage

After installing, simply ask Claude Code:

/zafer-skills Create a water reminder app

Claude will automatically:
- Set up the project structure
- Configure RevenueCat and AdMob
- Create onboarding with video background
- Build paywall with subscription options
- Add settings with language, theme, and reset options

Tech Stack

  • Expo SDK
  • Expo Router (file-based routing)
  • NativeTabs navigation
  • RevenueCat (react-native-purchases)
  • AdMob (react-native-google-mobile-ads)
  • i18next + react-i18next
  • expo-video
  • expo-sqlite (localStorage polyfill)

License

MIT

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.