Shipping a Cross-Platform App with React Native and Expo
Shipping a Cross-Platform App with React Native and ExpoJuly 8, 2026

Shipping a Cross-Platform App with React Native and Expo

Lessons from building CFREE, a real estate exam prep app, with React Native, Expo, in-app purchases, push notifications, and Firebase Auth — and releasing it to the App Store and Google Play.

Table of Contents

  1. Why Expo for a Small Team
  2. Navigation and App Structure
  3. Auth and Local State
  4. In-App Purchases
  5. Push Notifications
  6. UI That Survives Two Platforms
  7. Store Release Reality

Why Expo for a Small Team {#expo}

CFREE was a solo/small-team product: practice questions for real estate professionals, shipped to both stores. Expo removed native toolchain tax so we could spend time on study flows instead of Xcode rituals.

Expo made sense because we needed:

  • One JavaScript codebase
  • Push, auth, and IAP without a native specialist
  • OTA-friendly updates for content mistakes
  • A path to custom native code later if we outgrew the managed workflow

Over 100 users and usable ratings were enough to prove the stack. The constraint was time, not theoretical purity.

Navigation and App Structure {#nav}

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

export function RootNavigator() {
  const { user } = useAuth();

  return (
    <NavigationContainer>
      <Stack.Navigator>
        {user ? (
          <>
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Quiz" component={QuizScreen} />
            <Stack.Screen name="Paywall" component={PaywallScreen} />
          </>
        ) : (
          <Stack.Screen name="SignIn" component={SignInScreen} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Keep paywall as a real screen, not a modal you cannot deep-link. Reviewers and returning users both hit it.

Auth and Local State {#auth}

Firebase Authentication covered email and social login. Session persistence used Firebase's own storage; quiz progress used AsyncStorage so a student could lose network mid-set and keep answers.

import AsyncStorage from '@react-native-async-storage/async-storage';

const PROGRESS_KEY = 'quiz-progress-v1';

export async function saveProgress(progress: QuizProgress) {
  await AsyncStorage.setItem(PROGRESS_KEY, JSON.stringify(progress));
}

export async function loadProgress(): Promise<QuizProgress | null> {
  const raw = await AsyncStorage.getItem(PROGRESS_KEY);
  return raw ? JSON.parse(raw) : null;
}

Version the key. The first schema will be wrong.

Do not store purchase entitlements only on device. Restore purchases against the store + your backend, then cache the result.

In-App Purchases {#iap}

Subscriptions are where mobile apps go to die in QA.

  • Use RevenueCat or the Expo IAP module with a thin wrapper
  • Test with sandbox accounts on a real device
  • Handle purchase updated, restored, and expired as first-class states
  • Unlock content from a server receipt check, not from "the button succeeded"
type Entitlement = {
  active: boolean;
  expiresAt: string | null;
  productId: string | null;
};

The UI should have three honest states: free, subscribed, and "purchase pending." Silent failures after Apple's sheet closes are the number-one 1-star review.

Push Notifications {#push}

Study apps live on reminders.

import * as Notifications from 'expo-notifications';

export async function registerForPush() {
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') return null;
  const token = (await Notifications.getExpoPushTokenAsync()).data;
  await api.savePushToken(token);
  return token;
}

Ask for permission after the user finishes a quiz, not on first launch. Schedule local reminders for "daily 10 questions" even if remote push is delayed.

UI That Survives Two Platforms {#ui}

We used UI Kitten 5 with a custom theme: one design language, platform-native navigation chrome.

  • Touch targets at least 44px
  • Safe area insets on every screen
  • Dynamic type: do not assume 16px body text
  • Dark mode tested on both stores' review devices

Pixel-perfect between iOS and Android is a trap. Consistent hierarchy is the goal.

Store Release Reality {#release}

  • App Store privacy nutrition labels and Play Data safety are product work
  • IAP review rejects vague subscription copy
  • Screenshots that show the actual quiz beat generic stock photos
  • A crash-free first session matters more than feature count

React Native did not make the app "write once." It made the domain logic write once. Native still owns payments, permissions, and review. Plan for that and Expo stays a multiplier instead of a wall.