An iOS app that converts oven recipes to air-fryer time + temperature, lets you save recipes, and runs concurrent cook timers with a “shake halfway” reminder.
Built with Expo SDK 54 / React Native, intended to ship via EAS Build to TestFlight.
design/ — the original HTML / JSX prototype (Air Fryer Converter.html
and the screen / device-frame mocks the prototype was built from). Useful
as a visual reference for the warm cream + terracotta aesthetic.app/ — the actual React Native app.app/scripts/make-sounds.js): a warm major-arpeggio bell for “done” and
two bright dings for “shake halfway”. Plays with the iOS silent switch on.cd app
npx expo start
Press i to launch the iOS simulator.
The app sends a small set of anonymous, manually defined product events to PostHog when it is configured. Autocapture, session replay, and user-entered recipe content are excluded.
app/.env.example to app/.env.local.EXPO_PUBLIC_POSTHOG_API_KEY and
EXPO_PUBLIC_POSTHOG_HOST variables to the EAS production environment.ad_revenue_recorded events in addition to ad impressions.Analytics remains a no-op when no project token is configured and is disabled in development by default. Users can turn it off in Settings. Update the App Store privacy disclosure before releasing this version.
Active timers are persisted (@afc/timers-v1) and their alerts are
scheduled with the OS via expo-notifications (src/notifications.js), so
a locked phone still gets the “shake halfway” and “done” alerts with the
app’s own chimes. Notification permission is requested the first time a
timer starts. In the foreground the system notification is silenced and
the in-app chime / toast handles it. Timers that finished more than an
hour ago are dropped on the next launch instead of ringing.
app.json declares the iOS time-sensitive entitlement and Android’s
USE_EXACT_ALARM so alerts land on time; Play Console asks you to
justify the latter (the app runs cook timers).
initAds() runs Google’s User Messaging Platform consent flow before
initialising AdMob, then the ATT prompt on iOS. For the form to appear you
must publish a GDPR message (and, optionally, a US states message) under
AdMob → Privacy & messaging for this app; without one, EEA/UK users get no
ads. Settings shows an “Ad privacy options” row only where the SDK says one
is required.
Both platforms have real AdMob IDs wired in (app.json for the app IDs,
src/ads.js for the banner units). The Android app is registered in AdMob
but not yet linked to a Play listing, so it shows as “Requires review” and
ad serving stays limited until the app is published to Play and approved.
Development builds always serve Google’s test banner regardless.
Dedicated Pro paywall (app/src/screens/PaywallSheet.js): opens from Settings
→ Remove ads, and at most once per app version as a soft prompt after three
completed timers (never mid-cook). Copy lives in app/src/i18n/locales/en.json
via i18n-js + expo-localization. Add de.json / es.json / … and register
them in app/src/i18n/index.js to localize. Button price uses RevenueCat’s
localized priceString.
src/review.js asks for an App Store / Play rating after three completed
timers, once per app version, only while the app is in the foreground.
The share button on the result card sends
425°F oven for 25 min → 400°F air fryer for 20 min with the App Store link.
cd app
npx eas-cli login
npx eas-cli init # one-time, populates extra.eas.projectId
npx eas-cli build --platform ios --profile production
# fill in submit.production.ios in eas.json (appleId, ascAppId, appleTeamId)
npx eas-cli submit --platform ios --latest
cd app
node scripts/make-icon.js # icon.png, adaptive-icon.png, splash.png, favicon.png
node scripts/make-sounds.js # done.wav, shake.wav