Example app · Pro

Snap Recipe: recipes from a fridge photo

A standalone Expo app in `examples/snap-recipe`. It boots with no `.env`, and the sample fridge works everywhere. A photo scan needs an iPhone dev build and a recognition model whose licence matters before you ship.

Screens

What it is

A photo runs through MobileCLIP-S0 on the phone, zero-shot: the photo and a grid of crops are scored against the catalog's ingredients. An ingredient counts when its best probability reaches 0.15 (MIN_CONFIDENCE in src/lib/scan.ts).

Without the generated model, an iOS build falls back to Apple's built-in Vision classifier: on the device only, over a fixed vocabulary of about 1,300 labels.

Scans, favourites and the free counter persist with zustand (src/stores/scans.ts). Pro is one pro entitlement fed by two rails, App Store and Play through RevenueCat and Stripe Checkout (src/lib/payments.ts).

Flows included

Built on

Also in the app, with no /stack/ page: MobileCLIP, a local Swift module in modules/food-vision (iOS only), expo-image-manipulator for the downscale, and Maestro flows for end-to-end tests.

What works without keys

Keys and credentials

Env namesWhat it unlocks
EXPO_PUBLIC_REVENUECAT_IOS_KEY, EXPO_PUBLIC_REVENUECAT_ANDROID_KEYStore purchases through App Store and Play. These are public SDK keys.
STRIPE_SECRET_KEY, STRIPE_PRICE_ID (server only)Card checkout. These never take the EXPO_PUBLIC_ prefix.
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY (server only)Sign-in. Without the secret key, the Stripe routes answer 503.
EXPO_PUBLIC_API_URLWhere the device reaches the API routes. Empty falls back to the Metro dev server.
EXPO_PUBLIC_POSTHOG_KEY, EXPO_PUBLIC_SENTRY_DSNAnalytics and crash reports, after consent. Empty means no-op.

Limitations

Tier

Pro only. Its paid tier (unlimited scans through the pro entitlement) belongs to the demo, not to the ReadyNative Pro you buy. Free and Starter do not ship examples/.

From the source

From examples/snap-recipe/src/lib/quota.ts:

export function scansLeft(used: number, pro: boolean): number | null {
  return pro ? null : Math.max(0, FREE_SCANS - used);
}

Read more