ReadyNative

Example apps

The weather demo you see on first run, plus two complete Pro apps built on the same modules - run one, read it, copy what you need.

The weather demo

Starter and Pro open on a small weather app, so the first bun ios shows what the modules do together instead of an empty screen. It is demo content: bun run setup removes it and leaves a plain welcome screen, unless you pass --with-examples.

bun install
bun ios        # or: bun run start, for Expo Go
What you seeWhat it is built on
The forecast where you are, with a "Location is off" fallback@/lib/location (location) - null, never an exception, when permission is denied
Hourly strip, 10-day list with range bars, detail tiles, pull to refresh@/ui primitives only, so it renders the same on every UI stack
A native tab bar (Liquid Glass on iOS 26), large-title Cities stack, header search, context menus on each cityNativeTabs, native stack headers, Stack.SearchBar, Link.Menu from Expo Router
City search in the app language, °C / °FOpen-Meteo (free, no key), the persisted store on createPersistedStore
The last forecast on the first frame, and offline@/lib/cache feeding useForecast(), which each data module ships in its own idiom
English, Spanish, Russian, Chinese, Portuguese and Arabic - Arabic mirrors the layoutthe i18n module and Intl for dates, hours and digits
The temperature on your Home Screen, and live on the Lock Screen / Dynamic Island@/lib/widgets (widgets): widgets.update() and widgets.activity.start() - no-ops until you pick --widgets expo-widgets
Tap a day for its details in a native sheetSheet from @/ui (@expo/ui's SwiftUI / Material 3 sheet)
System toasts, presses that track your finger at 120 Hztoast.show() (Burnt on iOS builds) and Pressable (Pressto) from @/ui
"See all 10 days with Pro", more than 3 saved cities@/lib/payments; with --payments none nothing is locked
Share, "Notify me" and analytics events@/lib/share, @/lib/push, @/lib/analytics - each a no-op when its module is none

Where it lives, if you want to copy a pattern before setup deletes it:

  • Screens - src/screens/home/home-screen.tsx, src/screens/weather/*
  • Routes - src/app/(tabs)/index.tsx, src/app/(tabs)/cities/{_layout,index,[id]}.tsx
  • Open-Meteo client, WMO codes, Intl formatting - src/lib/weather/*
  • Saved cities and units - src/stores/weather.ts
  • The forecast hook per data module - src/hooks/use-forecast.ts

Right-to-left needs a dev build: Expo Go resets the layout direction when it opens a project, so Arabic text shows but the layout stays left-to-right there. The widget needs a dev build too.

The weather data comes from Open-Meteo under CC BY 4.0; the demo credits it under the forecast. Keep that line if you keep the API.

Pro example apps

Pro

These are the apps I use to prove the modules work together. Each one is a standalone app: examples/<name>/ has its own package.json, lockfile and .env.example, and it boots with no .env at all. Every screen is usable, and anything that needs a key you haven't set says so on that screen, before you act, naming the exact variable. Both use the native tab bar (NativeTabs), native stack headers with large titles and inset-grouped settings. Nothing in your app imports examples/, so delete it whenever you like.

ExampleWhat it showsPlatformsTier
Field NotesLocal-first journal with Supabase sync, reminders, place + weather, a paid tieriOS, AndroidPro
Snap RecipeOn-device food recognition, recipe matching, two payment railsiOS; Android without photo recognitionPro

Both apps sell their own paid tier (a RevenueCat entitlement called pro). That's the example's paid tier, a feature of the demo app - not the ReadyNative Pro tier you bought.

Field Notes

A private journal in examples/notes: one note per moment - a title, a few lines, optionally a photo and where you were. "Where I am" stamps the note with a place name (expo-location reverse geocoding) and the weather right now (Open-Meteo), both keyless. Notes are written to the device first and sync to Supabase once you sign in; a daily local notification nudges you to write; the example's paid tier unlocks unlimited notes and export. It's the saas preset plus expo-image-picker, expo-location and @react-native-community/datetimepicker.

cd examples/notes
bun install
bun ios
bun run test

bun ios (or bun android) compiles a dev build with expo run:*, which RevenueCat needs; afterwards bun run start:dev serves it. bun start is expo start --go and runs everything else in Expo Go. The camera only appears on a physical device.

Where each pattern lives

  • Local-first store (zustand persist on zustandStorage) - src/stores/entries.ts
  • Sync algebra: last-writer-wins, tombstones, cursor, with tests - src/features/entries/merge.ts
  • Supabase push/pull on sign-in, foreground and after edits - src/lib/sync.ts, supabase/entries.sql
  • Paid-tier gate that switches itself off while RevenueCat is unconfigured - src/lib/pro.ts (isProGateActive)
  • Keyless place + weather stamp - src/lib/weather.ts, src/features/entries/weather.ts
  • Daily local reminder that deep-links to /entry/new - src/lib/reminders.ts
  • Large-title list with header search and swipe-to-delete - src/screens/entries/entries-screen.tsx, entry-row.tsx
  • Modal editor with Cancel/Add in the navigation bar - src/screens/entries/entry-editor-screen.tsx
  • Stacks nested in native tabs - src/app/(tabs)/**
  • Inset-grouped settings with a native switch and time picker - src/screens/settings/settings-screen.tsx
  • One shared "needs keys" state naming the env vars - src/components/not-configured.tsx
  • Auth screens and a paywall that explain a missing key - src/screens/auth/*, src/screens/paywall/paywall-screen.tsx
  • Deep link fieldnotes://entry/<id> - src/app/(tabs)/(notes)/entry/[id].tsx
  • Empty-env boot test, keyless screen tests (list, settings, the "needs keys" state), Maestro flows - src/__tests__/boot.test.tsx, src/screens/__tests__/*, .maestro/flows/*.yaml

Keys

examples/notes/README.md has the full table. The short version: the Supabase URL and anon key unlock accounts and cloud sync (run supabase/entries.sql first for the table and RLS); the two RevenueCat keys turn the paid-tier gate on (without them nothing is capped, and Settings and the paywall say so); EXPO_PUBLIC_POSTHOG_KEY, EXPO_PUBLIC_SENTRY_DSN and EXPO_PUBLIC_API_URL are optional no-ops when empty. The daily reminder is a local notification, so it works in Expo Go with no EAS project; remote push needs a linked EAS project and a physical device.

Not included: photos stay on the device (no Supabase Storage upload), there is no "choose a new password" screen, conflicts resolve silently in favour of the newer edit, and the weather stamp is sampled when you tap "Where I am" - editing an old note re-stamps it with the weather now.

Snap Recipe

Photograph your fridge in examples/snap-recipe. MobileCLIP recognises the food on the phone - no server, no key, the photo never leaves the device - and a bundled recipe book suggests what to cook, marking which ingredients you already have. Save favourites, share a recipe as text. Three scans are free; the example's paid tier makes them unlimited and sells through two rails: native in-app purchase (App Store / Google Play via RevenueCat) and Stripe Checkout. It's the default preset plus Clerk, API routes, Stripe, RevenueCat, Sentry and PostHog.

cd examples/snap-recipe
bun install
bun run gen:clip
bun ios
bun run test

bun start is expo start --go: everything but photo recognition and RevenueCat purchases runs in Expo Go. bun ios builds the dev build those two need; bun run start:dev serves it.

bun run gen:clip downloads MobileCLIP-S0 (about 110 MB, cached) and writes the class embeddings for the recipe catalog; the weights aren't in git. It needs macOS (Core ML runs the text encoder) and python3. Run it once per machine, and again after changing the ingredient list in src/features/recipes/catalog.ts.

Android and other platforms

Photo recognition is an iOS-only local module, so it runs on an iPhone or the iOS Simulator from a dev build. On Android, the web and in Expo Go the scan screen says "Photo recognition runs on iPhone" in place. The rest works there: Try a sample fridge opens a bundled photo with three pre-baked recipes (src/features/recipes/sample.ts), so results, detail, favourites and share run anywhere with zero keys. Sample scans are marked "Sample" and never count against the free quota. bun android compiles the Android dev build, which the Google Play rail needs.

Without the generated model, an iOS build falls back to Apple's built-in Vision classifier (device only, a fixed vocabulary). For EAS builds, run bun run gen:clip in an eas-build-post-install script, or the cloud build ships the fallback.

Where each pattern lives

  • Local Expo module in Swift running MobileCLIP zero-shot over the photo and a grid of crops - modules/food-vision
  • Label → ingredient → recipe matching, with tests - src/features/recipes/catalog.ts
  • On-device downscale with expo-image-manipulator behind a react-query mutation - src/lib/scan.ts
  • Two payment rails behind one payments contract, entitlements are the union - src/lib/payments.ts, src/lib/store-purchases.ts
  • Stripe Checkout routes (checkout, entitlements, return, webhook) - src/app/api/stripe/*, src/server/stripe.ts
  • Paid-tier gating on payments.useEntitlements() - src/lib/quota.ts
  • One native stack per tab with a shared recipe/[id] route - src/app/(tabs)/**, src/components/app-stack.tsx
  • Scans, favourites and the free counter persisted with zustand - src/stores/scans.ts
  • Empty-env boot test, RevenueCat and Stripe tests with the SDKs mocked - src/__tests__/boot.test.tsx, src/lib/__tests__/*, src/server/__tests__/stripe-routes.test.ts
  • Maestro: fresh install, no keys → sample fridge → results - .maestro/flows/scan-empty.yaml

Paywall and keys

Hitting the scan limit opens RevenueCat's native paywall as a modal (Apple's purchase sheet on buy); /paywall lists the store plans with "Compare all plans", and Stripe as "Or pay by card" only where App Review allows it (US storefront on iOS). Settings has "Manage" on the paid-tier row and, in dev builds, Developer → Preview paywall. Add web checkout next to the App Store walks through the same setup for your own app.

examples/snap-recipe/README.md lists every key and what happens without it. Server-only keys (STRIPE_SECRET_KEY, STRIPE_PRICE_ID) have no EXPO_PUBLIC_ prefix and must never gain one; on EAS Hosting set them with --visibility sensitive - secret variables can't be deployed to EAS Hosting (see Environment variables). The RevenueCat keys are public SDK keys. examples/snap-recipe/docs/RECOGNITION-MODELS.md compares the models you can swap in (MobileCLIP, YOLO26, RF-DETR, Create ML, barcode and text reading, cloud vision) with their licences.

Before you ship anything based on Snap Recipe

  • MobileCLIP licence: the Hugging Face weights point at a permissive Apple licence, but Apple's repo moved MobileCLIP to a research-only licence in August 2025. Treat the model as research-only until you've settled it: read modules/food-vision/MODEL-LICENSE.md, and swap in a permissively licensed model for a commercial release.
  • The store rail is in-app purchase, not Apple Pay. Apple's sheet charges the Apple ID's payment method. Apple Pay proper is only for physical goods and services.
  • App Review 3.1.1: digital subscriptions in an iOS app must be offered through in-app purchase. A Stripe link-out is allowed on the US storefront; elsewhere it can get the build rejected.
  • Disclosure: show price, period, auto-renewal and how to cancel next to the buy button, and link Terms and Privacy (urls in readynative.config.ts).
  • Plain-http API on a device: iOS only allows http:// to LAN IPs, localhost, *.local and dot-less hosts. A tunnel or deployed API must be https://.
  • The free-scan counter lives on the device. Count scans per user server-side if the free tier must hold.

On this page

Get ReadyNative