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 see | What 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 city | NativeTabs, native stack headers, Stack.SearchBar, Link.Menu from Expo Router |
| City search in the app language, °C / °F | Open-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 layout | the 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 sheet | Sheet from @/ui (@expo/ui's SwiftUI / Material 3 sheet) |
| System toasts, presses that track your finger at 120 Hz | toast.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,
Intlformatting -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
ProThese 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.
| Example | What it shows | Platforms | Tier |
|---|---|---|---|
| Field Notes | Local-first journal with Supabase sync, reminders, place + weather, a paid tier | iOS, Android | Pro |
| Snap Recipe | On-device food recognition, recipe matching, two payment rails | iOS; Android without photo recognition | Pro |
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 testbun 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
persistonzustandStorage) -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 testbun 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-manipulatorbehind 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 (
urlsinreadynative.config.ts). - Plain-http API on a device: iOS only allows
http://to LAN IPs,localhost,*.localand dot-less hosts. A tunnel or deployed API must behttps://. - The free-scan counter lives on the device. Count scans per user server-side if the free tier must hold.