agentsclimarketplace

React native apps

Skill bertbertov/claude-stack/skills/react-native-apps

Complete skill for building, debugging, and shipping React Native / Expo apps in 2025-2026. Use when the user is building a mobile app, starting a new Expo project, debugging Expo Go crashes, setting up EAS builds, submitting to Google Play / App Store, writing animation patterns, or asking about mobile architecture decisions. Distilled from production Expo SDK 54 apps, official docs, Reddit r/reactnative hot threads, GitHub issues on expo/expo and facebook/react-native, and teardowns of shipped apps (Elirox, Instamobile templates). Covers 13 gotchas, 13 UX patterns, full deployment pipeline, animation stack, and production architecture boilerplate.From its SKILL.md

Install
npx -y skills add bertbertov/claude-stack --skill react-native-apps

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

28.8 KB, ~7.5k tokens by cl100k_base, as published. Nobody here has run it

React Native / Expo App Building — Complete Reference

Use this as the authoritative guide when building any React Native app. When context is small, just follow the key rules. When the user hits a specific problem, jump to the matching section.


Specialist Expo skills (defer to these for specifics)

For deep-dive on a specific Expo concern, defer to the official Expo team skills installed alongside this one:

ConcernUse this specialist
Upgrading SDK (54 → 55, dependency hell)upgrading-expo
Building/distributing dev clientsexpo-dev-client
Build & ship to App Store / Play Store / Webexpo-deployment
EAS Workflows YAML / CI-CDexpo-cicd-workflows
OTA update health (crash rate, install count)eas-update-insights
Native module authoring (Swift/Kotlin/TS)expo-module
API routes via Expo Router + EAS Hostingexpo-api-routes
Universal data fetching, caching, mutationsnative-data-fetching
UI fundamentals + styling + componentsbuilding-native-ui
Tailwind v4 + NativeWind v5 setupexpo-tailwind-setup
@expo/ui Jetpack Compose interopexpo-ui-jetpack-compose
@expo/ui SwiftUI interopexpo-ui-swift-ui
DOM components (run web in webview)use-dom

Use this skill (react-native-apps) for stack defaults, architecture, and the 13 gotchas. Use the specialist skills above when the task is narrow and deep.


0. STACK DEFAULTS (2025-2026)

ConcernDefault pickWhy
FrameworkExpo SDK 54+ with expo-routerFile-based routing, typed routes, auth gating, works on iOS/Android/Web
Navigationexpo-router route groups (auth)/(tabs)/(onboarding)Declarative, collocated with screens
LanguageStrict TypeScript ("strict": true, noUncheckedIndexedAccess)Mandatory for teams, saves you from silent bugs
Client stateZustand (~1KB, selector-based)Tiny, fast, no provider, works outside React
Server stateTanStack Query v5Caching, retries, optimistic updates
Storagereact-native-mmkv (sync, 30× faster than AsyncStorage)Needs dev build. Fallback: AsyncStorage
Secretsexpo-secure-storeKeychain/Keystore — never AsyncStorage for tokens
API clientfetch + thin wrapper OR axiosFetch wins for most cases
ValidationZod at API boundariesRuntime type-safety, infer static types
AuthClerk or Supabase AuthShip fast. Custom JWT only if required
AnimationReanimated 3/4 + Moti (Skia only if canvas needed)See Section 4
Bottom sheet@gorhom/bottom-sheetSettled winner. Built on Reanimated + gesture-handler
Chartsreact-native-wagmi-charts (simple) / victory-native v40 Skia (complex)Native perf
Icons@phosphor-icons/react-native or @expo/vector-iconsTree-shakeable
Formsreact-hook-form + zod + react-native-mask-inputStandard combo
ErrorsSentry via @sentry/react-native/expoUploads sourcemaps on EAS build
E2E testsMaestro (YAML)Far less pain than Detox
Unit testsJest + React Native Testing Library + MSWUser-centric, mock network at fetch
BuildEAS Build (30 free builds/mo)No local Xcode/JDK needed
CIGitHub Actions + expo/expo-github-actionAuto-deploy on push
OTAEAS Update (1000 free MAU)Push JS fixes without rebuilding

1. PROJECT ARCHITECTURE

Folder Structure (Expo Router + Feature-Oriented)

my-app/
├── app/                          # expo-router (file-based routing)
│   ├── _layout.tsx               # Root providers (QueryClient, ErrorBoundary, ThemeProvider)
│   ├── index.tsx                 # Landing / redirect based on auth
│   ├── (onboarding)/             # Route group — onboarding flow
│   │   ├── _layout.tsx
│   │   ├── welcome.tsx
│   │   └── disclaimer.tsx
│   ├── (auth)/                   # Route group — auth stack
│   │   ├── _layout.tsx
│   │   ├── sign-in.tsx
│   │   └── sign-up.tsx
│   ├── (tabs)/                   # Route group — main tab nav
│   │   ├── _layout.tsx
│   │   ├── home.tsx
│   │   ├── bots.tsx
│   │   └── profile.tsx
│   └── +not-found.tsx
├── src/
│   ├── components/
│   │   ├── ui/                   # Button, Input, Card (design system)
│   │   └── features/             # Feature-specific composites
│   ├── hooks/                    # useAuth, useTheme, useDebounce
│   ├── services/
│   │   ├── api/
│   │   │   ├── client.ts         # fetch wrapper
│   │   │   └── endpoints/
│   │   └── queries/              # TanStack Query hooks
│   ├── store/                    # Zustand stores
│   ├── lib/                      # mmkv, constants, formatters
│   ├── types/
│   └── utils/
├── assets/
├── app.config.ts                 # typed over app.json
├── eas.json
└── tsconfig.json                 # strict: true, paths: {"@/*": ["src/*"]}

Keep app/ thin — it's routing only. Real logic lives in src/.

Canonical Boilerplate

src/lib/storage.ts — MMKV wrapper usable with Zustand's persist:

import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV({ id: 'app', encryptionKey: 'rotate-me' });
export const mmkvZustandStorage = {
  getItem: (k: string) => storage.getString(k) ?? null,
  setItem: (k: string, v: string) => storage.set(k, v),
  removeItem: (k: string) => storage.delete(k),
};

src/store/auth.store.ts — Zustand + persist:

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { mmkvZustandStorage } from '@/lib/storage';

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null, token: null,
      signIn: (user, token) => set({ user, token }),
      signOut: () => set({ user: null, token: null }),
    }),
    { name: 'auth', storage: createJSONStorage(() => mmkvZustandStorage) }
  )
);

src/services/api/client.ts — fetch wrapper with auto-logout on 401:

import Constants from 'expo-constants';
import { useAuthStore } from '@/store/auth.store';
const BASE = Constants.expoConfig?.extra?.API_URL as string;

export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
  const token = useAuthStore.getState().token;
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...init.headers,
    },
  });
  if (res.status === 401) useAuthStore.getState().signOut();
  if (!res.ok) throw new ApiError(res.status, await res.text());
  return res.json();
}

app/_layout.tsx — providers at root:

import { Stack } from 'expo-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { ErrorBoundary } from 'react-error-boundary';

const qc = new QueryClient({ defaultOptions: { queries: { retry: 2 } } });

export default function RootLayout() {
  return (
    <ErrorBoundary fallback={<CrashScreen />}>
      <GestureHandlerRootView style={{ flex: 1 }}>
        <SafeAreaProvider>
          <QueryClientProvider client={qc}>
            <BottomSheetModalProvider>
              <Stack screenOptions={{ headerShown: false }} />
            </BottomSheetModalProvider>
          </QueryClientProvider>
        </SafeAreaProvider>
      </GestureHandlerRootView>
    </ErrorBoundary>
  );
}

Environment variables: app.config.ts with extra + expo-constants. Secrets go into EAS Secrets (eas secret:create), never in committed .env. Public client values use EXPO_PUBLIC_* prefix (auto-inlined at build).


2. UX PATTERNS (Premium Mobile 2025-2026)

2.1 Onboarding — "Pre-onboarding first, auth last"

Show value BEFORE asking for email. Reduces drop-off 20-40%. Sequence: Splash (1.5s) → 3-4 value slides (react-native-pager-view + Reanimated) → risk disclaimer (if regulated) → optional signup → personalization quiz → main app.

Persist completion in MMKV. Auth-gate via expo-router groups:

<Stack>
  {!hasOnboarded ? <Stack.Screen name="(onboarding)" /> :
   !isAuthed ? <Stack.Screen name="(auth)" /> :
   <Stack.Screen name="(tabs)" />}
</Stack>

2.2 Bottom Tab Nav — Floating Pill

Telegram/Revolut style. Sits 16-24px above safe-area, rounded-full, dark blur bg, 5 icons max.

<Tabs screenOptions={{
  tabBarStyle: {
    position: 'absolute', bottom: insets.bottom + 16, marginHorizontal: 24,
    borderRadius: 999, height: 64, backgroundColor: 'rgba(20,20,20,0.85)',
    borderTopWidth: 0, elevation: 12,
  }
}} />

Morphing variant (Spotify-style): center tab is FAB that opens a gorhom bottom sheet.

2.3 Empty States

Rule: illustration + one-line empathy + single CTA. NEVER "No data."

  • No bots: "Your trading floor is quiet. Launch your first strategy to wake it up." [Browse →]
  • No trades: "Once Diana spots a sweep, trades show here." [See how →]

Use react-native-svg for crisp illustrations. Centered column, 40% vertical padding.

2.4 Card Patterns

  • Expandable in place: Animated.View with layout={LinearTransition.springify()}
  • Swipeable: react-native-gesture-handler Swipeable
  • Stacked (Tinder): react-native-deck-swiper or custom PanGestureHandler

2.5 Modal / Bottom Sheet

@gorhom/bottom-sheet is the settled winner.

<BottomSheetModal ref={ref} snapPoints={['25%','60%','90%']}
  enableDynamicSizing={false}
  backdropComponent={BottomSheetBackdrop}>
  <BottomSheetView>{/* content */}</BottomSheetView>
</BottomSheetModal>

2.6 Skeleton vs Spinner

  • Known shape → skeleton with shimmer (Reanimated gradient sweep, 1.2s loop)
  • Unknown duration / foreground action → spinner button (disabled, ActivityIndicator inside)

2.7 Haptics (map intent → type)

Haptics.selectionAsync()                                       // tap nav/button
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium)        // confirm action
Haptics.notificationAsync(Haptics.NotificationFeedbackStyle.Success)
Haptics.notificationAsync(Haptics.NotificationFeedbackStyle.Error)

Wrap every Pressable that triggers state change.

2.8 Forms — RHF + Zod

Floating labels. Validate on blur, not on-change (jarring). Submit button disabled until formState.isValid. Errors slide-in via Reanimated FadeInDown.

2.9 Charts — two-tier

  • Micro-chart / spark: react-native-wagmi-charts <LineChart> (simple, haptic cursor)
  • Candlesticks / indicators: victory-native v40+ (Skia-backed) or lightweight-charts via WebView

2.10 Theme Switching — smooth

useColorScheme + Reanimated shared value, 300ms fade. Interpolate via interpolateColor. Don't remount — causes jank.

const progress = useSharedValue(isDark ? 1 : 0);
useEffect(() => { progress.value = withTiming(isDark ? 1 : 0, {duration:300}); }, [isDark]);
const bg = useAnimatedStyle(() => ({
  backgroundColor: interpolateColor(progress.value,[0,1],['#fff','#0a0a0a'])
}));

Or drop in react-native-theme-switch-animation for circular reveal.

2.11 Onboarding Checklist

Drives day-1 retention. Pin a collapsible "Get started" card on home until tasks complete. Each row: circle → checkmark (Reanimated spring), row crosses out. Auto-dismiss at 3/4 done.

2.12 Pull-to-Refresh (custom)

Default RefreshControl feels cheap. Custom:

const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler(e => { scrollY.value = e.contentOffset.y; });
const style = useAnimatedStyle(() => ({
  transform: [{ rotate: `${interpolate(scrollY.value,[0,-80],[0,360])}deg` }],
  opacity: interpolate(scrollY.value,[0,-40],[0,1]),
}));

2.13 Elirox Pattern (trading-app reference)

  • Demo must be default path. Don't require broker before value.
  • "AI Preset" framing converts better than "Pick a strategy."
  • Create-bot as 4-step wizard in bottom sheet pages, not a single long form: asset → strategy → risk → launch.
  • Paywall triggers on action ("Launch Live"), not on app open.
  • Bottom tabs: 4 tabs (Home · Bots · Analytics · Profile) + center "+" as create shortcut.

3. ANIMATION STACK

Library hierarchy (use in this order)

LibraryWhen
Reanimated 3/4 (default)Anything gesture-driven, 60/120fps, worklets on UI thread. Works in Expo Go.
MotiDeclarative wrapper over Reanimated. Great for staggered lists, onboarding, marketing UI.
Skia (@shopify/react-native-skia)Custom canvas — particles, shaders, liquid swipe, premium shimmer. Requires dev build.
LottiePre-made After Effects animations (success checkmarks, mascots). Works in Expo Go.
Animated APILegacy only. Keep for existing code.

Canonical patterns

Press feedback (scale 0.97):

const scale = useSharedValue(1);
const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
// onPressIn: scale.value = withSpring(0.97, { damping: 15, stiffness: 300 });
// onPressOut: scale.value = withSpring(1);

Staggered list fade-in (Moti):

<MotiView from={{ opacity: 0, translateY: 20 }} animate={{ opacity: 1, translateY: 0 }}
  transition={{ type: 'spring', delay: index * 60 }} />

Pulse / breathing:

scale.value = withRepeat(withSequence(
  withTiming(1.05, { duration: 800 }),
  withTiming(1, { duration: 800 })
), -1, true);

Number count-up (TextInput trick):

const value = useSharedValue(0);
useEffect(() => { value.value = withTiming(527.45, { duration: 1200 }); }, []);
const animatedProps = useAnimatedProps(() => ({ text: `$${value.value.toFixed(2)}` }));
// Render with Animated.createAnimatedComponent(TextInput), editable={false}

Parallax scroll:

const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler(e => { scrollY.value = e.contentOffset.y; });
const headerStyle = useAnimatedStyle(() => ({
  transform: [{ translateY: scrollY.value * 0.5 }],
  opacity: interpolate(scrollY.value, [0, 200], [1, 0]),
}));

Performance rules

  1. Reanimated worklets > Animated API for gesture-driven or >2 simultaneous animations
  2. Old Animated API: always useNativeDriver: true + only animate opacity/transform. Never width/height/backgroundColor on native driver — silently fails
  3. Layout animations: Reanimated's Layout prop OR animate scale + translate instead of real layout for 120Hz
  4. Worklets must be pure — no closures over React state. Pass via shared values
  5. runOnJS(cb)() (renamed to scheduleOnRN in Reanimated 4) costs a frame — use sparingly
  6. FlatList rows: use Moti per-item, not Animated.Value mapping — memory cheaper

Expo Go compatibility

LibraryExpo GoDev Build
Animated API
Reanimated
Moti
Lottie
Skia
@gorhom/bottom-sheet
react-native-gesture-handler

4. PRODUCTION GOTCHAS (13 things to check first)

4.1 Expo Go SDK version mismatch

Expo Go ships ONE SDK at a time. Your project SDK must match. Error: "Project is incompatible with this version of Expo Go." Fix: update Expo Go from store, or use expo-dev-client so your build pins its own SDK.

4.2 newArchEnabled / edgeToEdgeEnabled

  • SDK 54 is the last version where New Arch can be disabled. SDK 55+ (RN 0.82) has it forced on.
  • Android 15 (targetSdk 35) forces edge-to-edge regardless of edgeToEdgeEnabled=false — status/nav bars go transparent.
  • Tabs component from expo-router crashes in Expo Go SDK 54. Workaround: build a custom bottom nav or use dev-client.
  • Right: install react-native-edge-to-edge + react-native-safe-area-context, read insets via useSafeAreaInsets(), render with SystemBars component.

4.3 BackHandler (Android)

addEventListener returns a subscription now.

// RIGHT — focus-scoped, cleaned up
useFocusEffect(useCallback(() => {
  const sub = BackHandler.addEventListener('hardwareBackPress', () => true);
  return () => sub.remove();
}, []));

4.4 AsyncStorage vs localStorage

Sync (localStorage) vs async (AsyncStorage) — NOT interchangeable. 6MB cap, plaintext. Use expo-secure-store for tokens. On web, @react-native-async-storage/async-storage polyfills over localStorage as async.

4.5 Font loading race

useFonts can return [true, null] before fonts finish, producing invisible text or infinite splash.

SplashScreen.preventAutoHideAsync();
const [loaded, error] = useFonts({ Inter: require('./Inter.ttf') });
useEffect(() => { if (loaded || error) SplashScreen.hideAsync(); }, [loaded, error]);
if (!loaded && !error) return null;

Always log error. On Android, pre-bundle via expo-font config plugin instead of runtime.

4.6 SafeAreaProvider required

useSafeAreaInsets() silently returns zeros without a provider. Built-in SafeAreaView from react-native is deprecated — use community one.

<SafeAreaProvider><RootNavigator/></SafeAreaProvider>
// screens:
const insets = useSafeAreaInsets();
<View style={{ paddingTop: insets.top }}/>

4.7 Pressable vs TouchableOpacity

TouchableOpacity wraps children in an extra Animated.View, breaking flex layouts inside flex:1 parents. Prefer Pressable in new code.

<Pressable style={({ pressed }) => [styles.btn, pressed && { opacity: 0.6 }]} hitSlop={8} />

4.8 Reanimated 4 migration

  • Requires New Architecture only (no Paper)
  • Worklets moved to react-native-worklets
  • babel.config.js: 'react-native-reanimated/plugin''react-native-worklets/plugin'
  • runOnJS/runOnUIscheduleOnRN/scheduleOnUI
  • Removed: useWorkletCallback, combineTransition, addWhitelistedNativeProps
  • Still on Paper? Stay on 3.x

4.9 Metro cache — when to --clear

Required after editing: babel.config.js, metro.config.js, tsconfig.json paths, SVG transformer, installing/removing native modules. Nuclear: watchman watch-del-all && rm -rf node_modules $TMPDIR/metro-* && npx expo start --clear

4.10 Hot reload stale — r vs rescan

  • r suffices for JS changes
  • MUST rescan QR / rebuild when: changing app.json, adding native module, editing metro.config.js, after expo prebuild
  • Expo Go caches old project URL — scan fresh QR each server restart, don't tap history

4.11 KeyboardAvoidingView

Android 15 + targetSdk 35 breaks it. Use react-native-keyboard-controller's KeyboardAvoidingView in 2025+.

<KeyboardAvoidingView
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  keyboardVerticalOffset={headerHeight}
  style={{ flex: 1 }} />

Set android:windowSoftInputMode="adjustResize" in AndroidManifest.

4.12 FlatList inside ScrollView

Nesting same-orientation VirtualizedList in ScrollView kills virtualization and breaks touch on Android.

// WRONG: <ScrollView><FlatList /></ScrollView>
// RIGHT:
<FlatList
  data={items}
  ListHeaderComponent={<HeroSection/>}
  ListFooterComponent={<Footer/>}
  removeClippedSubviews
  keyExtractor={(i) => i.id}
  renderItem={renderItem} // memoized outside component
/>

4.13 Perf micro-gotchas

  • Inline style arrays style={[s.box, {mt:10}]} create new refs every render — hoist
  • Memoize renderItem and keyExtractor
  • useCallback for list row onPress
  • Expo Updates on iOS: keep :hermes_enabled in Podfile or OTA bundles fail

5. DEPLOYMENT — EAS Build + Google Play

5.1 eas.json (three standard profiles)

{
  "cli": { "version": ">= 12.0.0", "appVersionSource": "remote" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "android": { "buildType": "apk" }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "android": { "buildType": "apk" }
    },
    "production": {
      "channel": "production",
      "autoIncrement": true,
      "android": { "buildType": "app-bundle" }
    }
  },
  "submit": {
    "production": {
      "android": { "serviceAccountKeyPath": "./google-service-account.json", "track": "internal" }
    }
  }
}

5.2 Build commands

npm install -g eas-cli
eas login
eas build:configure
eas build --platform android --profile preview      # APK for sideload
eas build --platform android --profile production   # AAB for Play
eas build --local                                    # free, skip queue

Free tier (2026): 15 Android + 15 iOS builds/month. Queue 10min–2hr on free. Production plan $199/mo for priority queue.

5.3 app.json required fields

{
  "expo": {
    "name": "Your App",
    "slug": "your-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "scheme": "yourapp",
    "userInterfaceStyle": "automatic",
    "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#000" },
    "android": {
      "package": "com.yourorg.yourapp",
      "versionCode": 1,
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFD700",
        "monochromeImage": "./assets/monochrome-icon.png"
      }
    },
    "ios": { "bundleIdentifier": "com.yourorg.yourapp", "buildNumber": "1" },
    "updates": { "url": "https://u.expo.dev/<project-id>" },
    "runtimeVersion": { "policy": "appVersion" },
    "plugins": [
      ["expo-build-properties", { "android": { "compileSdkVersion": 35, "targetSdkVersion": 35 } }],
      "expo-updates"
    ],
    "extra": { "eas": { "projectId": "..." } }
  }
}

Gotcha: Package name and bundleIdentifier are immutable once published.

5.4 Icons

  • Master: 1024×1024 PNG, square, fully opaque, no rounded corners (OS masks)
  • Adaptive (Android): foreground (center 66×66 dp safe), background color, monochrome (Android 13+ themed)
  • Splash: 1200×1200 transparent PNG, imageWidth max 200

5.5 Android signing

EAS auto-manages keystore. On first build it generates one — say yes.

eas credentials    # interactive management
eas credentials -p android --profile production    # export

Save keystore + password + alias + key password to password manager. Losing them bricks your Play listing forever.

5.6 Google Play submission

  • $25 one-time registration
  • Personal vs Organization account — permanent choice. Organization skips 12-tester rule.
  • Target API 35 (Android 15) required after Aug 31 2025 for new apps and updates
  • Data Safety form (mandatory, 2025+): declare every SDK (Sentry, Firebase, Stripe, analytics). Lying = permanent ban
  • Release tracks:
    1. Internal testing (100 testers, instant)
    2. Closed testing (12 opted-in testers × 14 days for personal accounts — strict clock)
    3. Open testing (optional)
    4. Production (requires prior closed test pass)
  • Store listing: short desc 80ch, long desc 4000ch, app icon 512×512, feature graphic 1024×500, 2-8 phone screenshots, privacy policy URL (HTTPS, no PDF)
  • AAB only — APK uploads to production no longer allowed

5.7 OTA via EAS Update

JS-only changes without rebuild:

npx expo install expo-updates
eas update:configure
eas update --branch production --message "Fix button color"
eas channel:edit production --branch production

Runtime version must match binary's. Use "runtimeVersion": {"policy": "appVersion"}. Native module changes still need a new binary.

5.8 Sentry (crash reporting)

npx expo install @sentry/react-native

app.json:

["@sentry/react-native/expo", { "organization": "your-org", "project": "your-app" }]

App.tsx:

import * as Sentry from '@sentry/react-native';
Sentry.init({ dsn: 'https://[email protected]/xxx', tracesSampleRate: 0.2, enableNative: true });
export default Sentry.wrap(App);

Store SENTRY_AUTH_TOKEN as EAS secret. Sourcemaps upload automatically.

5.9 Privacy policy generators (free, Play-accepted)

  • Termly — GDPR/CCPA ready
  • FreePrivacyPolicy.com — mobile template
  • nisrulz/app-privacy-policy-generator (open source, host on GitHub Pages) Must be HTTPS URL, no PDF, no popup.

5.10 Trading / financial app framing

To avoid policy rejection without broker licensing:

  • Frame as "educational / simulation / signal information" not "trading" or "investment advice"
  • Short desc: "Educational trading signals and market insights for learners" — never "Make money trading"
  • Risk disclaimer prominent on first launch AND in settings: "This app does not provide financial advice. Trading involves risk of loss."
  • Don't execute real trades inside app without broker license in target country — link externally
  • Content rating: "References to gambling" = No (avoid 18+ bucket)
  • Premium tiers: use Google Play Billing (required for digital content). External payment (Stripe) only for physical services
  • Elirox-type survives by: educational framing, visible disclaimers, no unlicensed brokerage, Play Billing for subs, accurate Data Safety

5.11 Common rejections

ReasonFix
Privacy policy missing/brokenHTTPS URL, in-app AND store listing, lists every data type
Metadata misleadingMatch description to functionality, no keyword stuffing
Permissions abuseRemove unused with blockedPermissions, justify each
Crashes during reviewTest release build on real low-end Android (API 30, 2GB RAM)
Target API too lowBump to 35 via expo-build-properties
Data Safety mismatchDeclare every SDK exactly
Financial misrepresentationAdd risk disclaimer, frame educational, contact email
Closed testing bypass attemptRun 12 testers × 14 days — no shortcut on personal accounts
Icon violations1024×1024 square, no alpha, no rounded corners

5.12 GitHub Actions CI/CD

.github/workflows/eas-build.yml:

name: EAS Build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - uses: expo/expo-github-action@v8
        with: { eas-version: latest, token: ${{ secrets.EXPO_TOKEN }} }
      - run: npm ci
      - run: eas build --platform android --profile production --non-interactive --no-wait
      - run: eas update --branch production --message "${{ github.event.head_commit.message }}"

6. KEY RULES (the "remember these 10" list)

  1. Match Expo Go SDK to project SDK. Or use dev-client.
  2. Wrap root in SafeAreaProvider always. Hooks fail silently without it.
  3. Use useFocusEffect + subscription .remove() for BackHandler, not raw useEffect.
  4. Scan fresh QR every server restart — Expo Go caches the old URL.
  5. Reanimated > Animated API for new code. Worklets on UI thread beat JS thread every time.
  6. MMKV > AsyncStorage when you can use dev build. expo-secure-store for all secrets/tokens.
  7. Zustand > Context for anything that changes. Context re-renders every consumer.
  8. Package/bundle ID is forever. Don't rush this decision.
  9. Data Safety form: tell the truth. Lying = permanent Play ban.
  10. Premium subs on mobile = Google Play Billing (not Stripe) if content is digital. Physical services can use external payment.

Sources

  • Expo docs (docs.expo.dev), Expo blog, Expo changelogs SDK 50-55
  • GitHub issues: expo/expo, facebook/react-native, software-mansion/react-native-reanimated
  • Reddit r/reactnative hot threads 2024-2026
  • Fernando Rojo (Moti creator) / Catalin Miron / William Candillon writings
  • Gorhom bottom sheet, MMKV, gesture-handler, safe-area-context docs
  • Google Play Console policy docs (Data Safety, API 35, testing tracks)
  • Elirox app teardown (decompiled APK + App Store listing)
  • Real-world ship-it threads: "mistakes I made", "how I shipped my first app"

Last updated: 2026-04.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,537. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.