Every Expo SDK upgrade broke something in the wallpaper app's native modules. Half a day untangling dependencies, confirming the app still ran, and nothing new shipped. After the third round of that, rebuilding started to look cheaper than repairing.
Rork Max was the tool I reached for. It claimed to cover the whole path — UI generation, AdMob wiring, App Store submission — which made it a reasonable candidate for moving an app that was already live and earning. I went in skeptical.
So I rebuilt the app from scratch and put a timer on every stage. What follows is the measured result: the hours that disappeared, the hours that didn't, the spec details the generated code got wrong, and how the existing paying subscribers survived the swap.
Why I Chose to Rebuild Rather Than Add Features
The trigger wasn't curiosity. It was maintenance pain.
My React Native wallpaper app had accumulated what I'd call "dependency debt" — each Expo SDK upgrade created native module conflicts that took half a day to untangle. I was spending more time confirming the app still worked than building anything new. That's the point where maintenance cost exceeds new-build cost, and rebuilding becomes the rational call.
The conditions I set for the experiment:
- Target app: Wallpaper browser with categories, pinch-zoom detail view, favorites, and subscription monetization
- Baseline: My actual logged hours from building the same feature set from scratch in React Native + TypeScript previously
- Measurements: Per-task dev time, code quality, App Store review outcome, AdMob display, and 30-day revenue metrics post-launch
Baseline: How Long the Traditional Build Actually Took
These are real numbers from a previous project of equivalent scope. This is experienced-developer time — someone who already knows the stack.
- Project setup, Expo initialization, library selection: 4 hours
- UI design (Figma wireframes): 8 hours
- Wallpaper grid and category screens: 12 hours
- Image optimization and caching (expo-image): 6 hours
- Favorites feature (AsyncStorage): 3 hours
- RevenueCat subscription integration: 8 hours
- AdMob implementation (banner, interstitial, ATT compliance): 6 hours
- App Store submission prep (Privacy Manifest, screenshots): 4 hours
- Testing and bug fixes: 10 hours
Total: ~61 hours
Even moving quickly, that's a minimum of 8 full working days. For someone less familiar with the stack, multiply by two or three.
The Rork Max Build: A Day-by-Day Log
Day 1: Project Setup to Working UI (6 hours)
My first prompt was intentionally straightforward:
Build a wallpaper app.
Requirements:
- Category browsing (nature, city, abstract, anime)
- Pinch-to-zoom detail view
- Favorites toggle (heart icon)
- iOS and Android support
- Dark mode support
Design: minimal, dark background
The first thing that caught my attention was FlashList. Rork Max generated code using @shopify/flash-list rather than FlatList — a choice that matters significantly for image-heavy scroll performance. In my traditional builds, I've always started with FlatList and migrated to FlashList after hitting performance problems. Getting this right from the start is not a small thing.
Generated wallpaper grid component:
// WallpaperGrid.tsx — generated by Rork Max
import { FlashList } from '@shopify/flash-list';
import { Pressable, Dimensions, StyleSheet } from 'react-native';
import { Image } from 'expo-image';
import { useRouter } from 'expo-router';
const { width } = Dimensions.get('window');
const ITEM_WIDTH = (width - 3) / 2;
interface WallpaperItem {
id: string;
thumbnailUrl: string;
category: string;
}
export default function WallpaperGrid({ category }: { category: string }) {
const router = useRouter();
return (
<FlashList
data={[]} // populated from Zustand store
numColumns={2}
estimatedItemSize={ITEM_WIDTH * 1.5}
renderItem={({ item }: { item: WallpaperItem }) => (
<Pressable
style={styles.item}
onPress={() => router.push(`/wallpaper/${item.id}`)}
>
<Image
source={{ uri: item.thumbnailUrl }}
style={styles.thumbnail}
contentFit="cover"
transition={200}
cachePolicy="memory-disk" // memory + disk cache, no extra config needed
/>
</Pressable>
)}
keyExtractor={(item: WallpaperItem) => item.id}
/>
);
}
const styles = StyleSheet.create({
item: { width: ITEM_WIDTH, height: ITEM_WIDTH * 1.5, margin: 0.5 },
thumbnail: { width: '100%', height: '100%' },
});The cachePolicy="memory-disk" default and the transition={200} fade are exactly the choices I'd make. This isn't beginner code.
Day 2: RevenueCat Subscription Integration (4 hours)
This was the phase I was most worried about. RevenueCat integration requires project-specific configuration — App IDs, Entitlement names, Offering structures — that an AI can't know in advance. I expected generic boilerplate.
What I got was more useful than that:
// hooks/useSubscription.ts
import Purchases, { CustomerInfo, PurchasesPackage } from 'react-native-purchases';
import { useEffect, useState } from 'react';
export function useSubscription() {
const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
const [packages, setPackages] = useState<PurchasesPackage[]>([]);
const [loading, setLoading] = useState(true);
// Check active entitlement named 'premium'
const isPremium = customerInfo?.entitlements.active['premium'] !== undefined;
useEffect(() => {
async function initialize() {
try {
const info = await Purchases.getCustomerInfo();
setCustomerInfo(info);
const offerings = await Purchases.getOfferings();
if (offerings.current?.availablePackages) {
setPackages(offerings.current.availablePackages);
}
} catch (error) {
console.error('RevenueCat init error:', error);
} finally {
setLoading(false);
}
}
initialize();
}, []);
async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
try {
const { customerInfo: updated } = await Purchases.purchasePackage(pkg);
setCustomerInfo(updated);
return true;
} catch (error: any) {
if (!error.userCancelled) {
console.error('Purchase failed:', error);
}
return false;
}
}
async function restorePurchases() {
const updated = await Purchases.restorePurchases();
setCustomerInfo(updated);
}
return { isPremium, packages, loading, purchasePackage, restorePurchases };
}The purchase hook, restore flow, and error handling for user-cancelled purchases were all correct on first generation. The dashboard-side configuration in RevenueCat and App Store Connect still requires manual work — that part hasn't changed. For a detailed RevenueCat setup walkthrough, see RevenueCat Subscription Monetization Guide for Rork.
Days 3–4: AdMob and App Store Submission Prep (5 hours)
I asked Rork Max to include ATT (App Tracking Transparency) handling as part of the AdMob setup. It automatically proposed expo-tracking-transparency and generated UMP consent form integration alongside AdMob initialization — handling the full compliance flow that's now effectively required for App Store approval.
// app/_layout.tsx — ATT before AdMob initialization
import { useEffect } from 'react';
import { Platform } from 'react-native';
import mobileAds from 'react-native-google-mobile-ads';
async function requestTrackingPermission() {
if (Platform.OS !== 'ios') return;
try {
const { requestTrackingPermissionsAsync } =
await import('expo-tracking-transparency');
await requestTrackingPermissionsAsync();
// AdMob initializes regardless of result; targeting adjusts based on consent
} catch {
// Fail gracefully — ads still work without tracking permission
}
}
export default function RootLayout() {
useEffect(() => {
async function initAds() {
await requestTrackingPermission();
await mobileAds().initialize();
}
initAds();
}, []);
// ...
}Getting the ATT prompt timing right is one of the variables that most impacts AdMob opt-in rates — and therefore revenue. The generated code uses the correct initialization order. For how to optimize the prompt itself, Maximizing AdMob ATT Opt-In Rates in Rork Apps goes deeper on that.
The Time Comparison
Rork Max measured against the 61-hour baseline:
- Project setup and core UI: 24 hours → 6 hours (75% reduction)
- Image optimization and caching: 6 hours → 2 hours (67% reduction)
- Favorites feature (Zustand): 3 hours → 0.5 hours (83% reduction)
- RevenueCat integration: 8 hours → 4 hours (50% reduction)
- AdMob implementation: 6 hours → 3 hours (50% reduction)
- App Store submission prep: 4 hours → 3 hours (25% reduction)
- Testing and bug fixes: 10 hours → 4 hours (60% reduction)
Total: 61 hours → 22.5 hours (63% reduction)
The headline "1/10 the time" was an exaggeration. The real number is about one-third. But the feeling of working at 1/10 the cognitive load isn't wrong — the mental overhead of writing boilerplate dropped significantly, which means each decision gets more attention. That qualitative shift is real even if the raw hour count doesn't hit the marketing-friendly ratio.
The biggest gains came from UI implementation. Because I already know how I'd write this code, reviewing generated code is fast. The flip side is real: if you don't yet have your own way of writing it, you can't judge whether the output is good, and you carry that uncertainty forward.
Where Rork Max Fell Short
Three areas that didn't work well.
1. Project-specific business logic with edge cases
The app had a download limit rule: free users get 3 wallpaper downloads per day, resetting at midnight Japan time. The first generated implementation used UTC as the day boundary — a subtle bug that produces wrong behavior for Japanese users.
// ❌ First generated version (UTC-based, wrong for JST users)
const todayKey = new Date().toISOString().split('T')[0];
// ✅ Fixed version (JST-aware)
const getJSTDateKey = () => {
const now = new Date();
const jst = new Date(now.getTime() + 9 * 60 * 60 * 1000); // UTC+9
return jst.toISOString().split('T')[0];
};
const todayKey = getJSTDateKey();The fix is one line, but catching this bug required knowing that UTC-based date boundaries exist in the first place. Locale-specific time logic needs to be explicitly specified in the prompt — Rork Max won't infer it.
2. Adding to an existing codebase
This rebuild was greenfield. For an existing React Native project with established type definitions and state management patterns, I'd expect Rork Max to struggle with consistency. It can't read the context of code it didn't generate.
3. Deep native API integration
CoreML, HealthKit, and other non-Expo native frameworks required manual implementation. Rork Max can scaffold the React Native side, but the native bridge still needs hands-on work when you're outside the Expo managed workflow.
30-Day Revenue Metrics After Relaunch
The rebuilt app ran on TestFlight for two weeks before the App Store release. Comparing 30 days post-launch against the same period on the previous version:
- Crash rate: 0.8% → 0.2% (74% improvement)
- Launch time (P75): 2.1s → 1.3s (38% faster)
- Subscription conversion rate: 2.3% → 2.8% (+0.5 points)
- AdMob eCPM: $0.82 → $0.85 (essentially flat)
Whether the faster launch time directly caused the conversion improvement is hard to isolate. But the correlation makes sense — a more responsive app keeps users engaged through the trial period. The crash rate improvement is attributable to FlashList and the revised image cache configuration.
AdMob eCPM was flat, which is expected. Ad revenue depends far more on ATT opt-in rates and impression volume than on app architecture. From my own attempts to grow ad revenue as a solo developer, the levers that actually move are ad placement design and the ATT prompt UX — not code quality. Those learnings are in AdMob Monthly Revenue Strategy for Indie Developers.
Migrating Paying Subscribers Without Breaking Them
The part that took the most care wasn't code — it was billing continuity. The old version had active monthly subscribers. If entitlements went dark the moment the binary was replaced, the one-star reviews would arrive the same day.
What saved me is how RevenueCat is structured: the subscription itself lives in the App Store transaction record, and the SDK only manages which entitlement maps to which user. Get that mapping right and the subscription survives a full rewrite of everything around it.
Here's what I verified before shipping.
| Check | Why it matters | How to verify |
|---|---|---|
| Bundle ID unchanged | A new Bundle ID means a new app, and past transactions never reach it | Diff the new app.json against the old project character by character |
| Same App User ID scheme | If anonymous ID generation changes, one person becomes two users | Watch the RevenueCat dashboard for a single device splitting into two profiles |
| Entitlement identifier spelling | Generated code will happily invent its own identifier | Compare the dashboard identifier against the generated constant |
| Restore Purchases is reachable | Review always checks for it; missing it means rejection | Place it in the settings screen |
The first generated build used premium as the entitlement identifier. The live dashboard uses pro. That single word turns every existing subscriber into a free user. The build compiles. New-purchase tests pass. The only way I caught it was running a sandbox check with an account that already had an active subscription.
// As generated: silently loses existing entitlements
const isPro = customerInfo.entitlements.active['premium'] !== undefined;
// Fixed: identifier comes straight from the dashboard, stored once
const ENTITLEMENT_ID = 'pro';
const isPro = customerInfo.entitlements.active[ENTITLEMENT_ID] !== undefined;Keeping that identifier in a single constant instead of scattering string literals across screens means the next rebuild only has one place to check. The more of the code you hand to generation, the more valuable it becomes to keep the proper nouns that belong to external services under human control.
The rollout order mattered too: one real, already-subscribed account went through TestFlight first, and only after confirming the subscription still showed as active did I move to a staged release. Skip that and you find out from the review section.
What the 22.5 Hours Doesn't Include
That 22.5-hour figure covers implementation only. The actual path to relaunch included several stretches of work that never made it into the timer. Honestly accounted for:
- Migrating wallpaper assets from the old app and normalizing naming: roughly 5 hours
- Rebuilding App Store screenshots and the description: roughly 3 hours
- Handling one review rejection (missing subscription price disclosure): roughly 2 hours
- Writing release notes for existing users and answering the support mail that followed: roughly 2 hours
That's about 12 hours. So while implementation finished in 22.5 hours, the total time to relaunch was closer to 35. Reading "63% less development time" as "63% faster to ship" will break your estimate.
Knowing that gap changes how I plan. Even when the plan assumes Rork Max, I now reserve roughly as much time for the non-implementation work as for the implementation itself.
Where the Saved Time Actually Went
The awkward part came after the hours dropped: deciding what to do with them. For the first few weeks I simply poured the time into sketching the next app. But when I asked myself whether shipping more apps was the point, the answer was no.
Most of the reclaimed time now goes back into watching the app after release. Reading crash logs every morning. Reading every review, translation included. Watching session recordings of the screens where people drop off. Work that used to get squeezed into "once a month, when there's time."
What Rork Max took over was the question of how to write it. What to build, who to build it for, what to charge — those stay on my side of the desk. The tool starts paying for itself at the moment the saved hours go back into those questions.
Decision Framework for Indie Developers
Based on this experiment, here's how I'd approach the decision:
Good fit:
- Apps with simple data structures (wallpaper, wellness, habit trackers)
- MVPs you need in the market within days to test demand
- Porting an existing app's feature set to a different platform
Poor fit:
- Large feature additions to existing React Native codebases
- Apps requiring complex offline sync or conflict resolution
- Game-engine-style rendering (Unity/Metal)
On cost: Rork Max's monthly fee pays for itself in saved developer hours within the first project for anyone billing their time. The question isn't whether it's worth the cost — for most indie developers, it clearly is. The question is what you do with the saved time.
My next Rork Max build is a content-heavy app that adds new material every day. I want to know whether the workflow that held up for a simple data structure like wallpapers still holds when the content itself keeps growing. I'll report back when it's live.
If you're deciding whether to try it: start with one feature, not a full rebuild. Find a screen you need to add to an existing app, build it in Rork Max, evaluate the output against what you'd write yourself. That's a lower-stakes test than committing a full project.