RORK LABJP
DEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeRULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passesEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changesHERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them firstDEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeRULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passesEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changesHERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them first
Articles/Business
Business/2026-05-15Advanced

Rebuilding a Live Wallpaper App with Rork Max: Measured Hours and Post-Relaunch Metrics

I rebuilt a wallpaper app that was already live using Rork Max, timing every stage. Measured hours per phase, the spec details the generated code got wrong, how existing subscribers were migrated safely, and 30-day post-relaunch metrics.

Rork Max232indie development36wallpaper app22AdMob70RevenueCat30App Store88monetization47

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.

CheckWhy it mattersHow to verify
Bundle ID unchangedA new Bundle ID means a new app, and past transactions never reach itDiff the new app.json against the old project character by character
Same App User ID schemeIf anonymous ID generation changes, one person becomes two usersWatch the RevenueCat dashboard for a single device splitting into two profiles
Entitlement identifier spellingGenerated code will happily invent its own identifierCompare the dashboard identifier against the generated constant
Restore Purchases is reachableReview always checks for it; missing it means rejectionPlace 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Business2026-04-14
Achieving $7,000/Month with Rork Max: A Complete Blueprint for Indie App Revenue
A systematic guide to building a $7,000/month indie app business with Rork Max — covering market selection, revenue model design, RevenueCat implementation, user acquisition, and KPI-driven optimization.
Business2026-06-22
A Wallpaper App's Real Work Starts After Launch — Content, Retention, and Revenue Notes from Running One on Rork
A wallpaper app is decided less by its launch and more by how you tend it afterward. Building on a Rork implementation, here are field notes on scheduled publishing that keeps content fresh, onboarding that survives the first week, and a revenue setup whose per-download return doesn't thin out over time.
Business2026-05-12
I Built a Wallpaper App with Rork — An Honest Review from a Developer with 50M App Downloads
After running wallpaper and healing apps with 50M+ downloads since 2014, I prototype-tested Rork on the same genre. Here's my honest take on where it shines, where it struggles, and what it means for solo developers.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →