●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking
Running iOS and Android Simultaneously with Rork Max — A Dual-Store Revenue Strategy
A complete guide to expanding Rork Max apps to both iOS and Android, with platform-specific monetization strategies, RevenueCat + AdMob integration, and a unified revenue management system. Includes real calculations and implementation patterns for independent developers.
When my app was iOS-only, monthly revenue was around ¥180,000. Six months after adding Android support, it was ¥310,000. The additional ¥130,000 came almost entirely from user segments that iOS doesn't reach.
Rork Max excels at native SwiftUI development, but the underlying React Native architecture means Android deployment is achievable from the same codebase. This guide covers the full monetization design for operating both platforms simultaneously — with actual revenue numbers, working code, and the platform-specific differences that most guides skip.
Why iOS and Android Monetize Differently
Shipping the same app on both platforms doesn't mean you'll earn the same revenue from both. Understanding the structural differences prevents the most expensive mistake in dual-store development: optimizing the wrong thing on the wrong platform.
App Store (iOS) Purchase Behavior
iOS users convert to paid plans at higher rates. Face ID one-tap payment reduces friction dramatically. The App Store ecosystem has conditioned users to pay for software.
Subscription conversion: Trial-to-paid rates of 5–15% are realistic on iOS
ARPU: Typically 1.5–2x higher than Android for the same app
Review standards: Stricter on payment flows and ad placement — but the restrictions are well-documented
Google Play (Android) Purchase Behavior
Android dominates in absolute user volume, particularly in Southeast Asia, India, and Latin America. Conversion rates are lower, but download volume compensates. Ad revenue per DAU is often higher on Android.
Download volume: 2–3x iOS download counts are common for comparable apps
Ad revenue: Higher DAU × competitive AdMob CPMs = stronger ad revenue than most iOS-first developers expect
Pricing sensitivity: Android users respond well to lower entry price points — lower price, higher conversion, similar or better LTV
Revenue structure by app category:
Utility / Tool apps:
iOS: Subscription-primary → optimize for conversion rate
Android: Ad + subscription hybrid → optimize for DAU
Entertainment / Games:
iOS: IAP + light ads
Android: Ad-primary + IAP (higher ad ARPDAU)
Habit / Lifestyle apps:
iOS: High-price subscriptions (¥1,200–¥3,600/month)
Android: Lower-price subscriptions + ads (¥500–¥1,500/month)
RevenueCat: Managing Both Stores from One SDK
The biggest mistake in cross-platform paid app development is writing separate billing logic for iOS and Android. Two codepaths means two places for bugs, two places for edge cases, and double the debugging time during subscription renewal issues.
RevenueCat abstracts the App Store and Google Play into a single SDK. The same purchase call works on both platforms:
// Initialize on app launch — same code for iOS and Androidimport PurchasesPurchases.configure(withAPIKey: "YOUR_REVENUECAT_API_KEY")// After user loginPurchases.shared.logIn(appUserId: userId) { (customerInfo, created, error) in if let info = customerInfo { updateSubscriptionStatus(from: info) }}func updateSubscriptionStatus(from info: CustomerInfo) { let isSubscribed = info.entitlements["premium"]?.isActive == true let expiresDate = info.entitlements["premium"]?.expirationDate DispatchQueue.main.async { AppState.shared.isPremium = isSubscribed AppState.shared.premiumExpiresDate = expiresDate }}// Purchase — identical call regardless of platformfunc purchase(packageId: String) async throws { let offerings = try await Purchases.shared.offerings() guard let package = offerings.current?.availablePackages.first(where: { $0.identifier == packageId }) else { throw PurchaseError.packageNotFound } let result = try await Purchases.shared.purchase(package: package) if result.customerInfo.entitlements["premium"]?.isActive == true { await updateUI(isPremium: true) await logPurchaseEvent(packageId: packageId) }}
Platform-Specific Pricing Strategy
You can — and often should — price differently between iOS and Android. RevenueCat supports separate products in each store's dashboard:
Subscription pricing example:
iOS (higher ARPU, optimize for conversion value):
Lite plan: ¥480/month
Pro plan: ¥1,200/month ← primary revenue driver
Annual: ¥9,800/year (¥817/month equivalent, 32% savings)
Android (higher volume, optimize for conversion rate):
Lite plan: ¥380/month ← lower entry barrier than iOS
Pro plan: ¥980/month
Annual: ¥7,800/year (¥650/month equivalent, 33% savings)
Rationale:
- Android users have higher price sensitivity on average
- Lower monthly price → higher trial conversion → similar or better LTV
- Annual plan creates LTV regardless of platform pricing differences
Configure separate product IDs in App Store Connect and Google Play Console, then link them as different offerings in the RevenueCat dashboard. Your app code never needs to know which store it's dealing with.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Price and monetization allocation tuned to how iOS and Android users actually pay
✦Lifting ad CPMs in practice with AdMob mediation and iOS ATT handling
✦Unifying subscription and ad revenue in one script with RevenueCat and the AdMob Reporting API
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
AdMob Integration: Maximizing Ad Revenue Without Killing Retention
The most common AdMob mistake in subscription apps: showing ads to paying subscribers. I've seen this create a cascade of angry 1-star reviews from users who explicitly paid to remove ads. Wire the ad logic to subscription status from day one:
class AdManager { static let shared = AdManager() private var interstitialAd: GADInterstitialAd? // The only rule: never show ads to premium users var shouldShowAds: Bool { return !AppState.shared.isPremium } func loadInterstitialIfNeeded() { guard shouldShowAds else { return } let request = GADRequest() GADInterstitialAd.load( withAdUnitID: adUnitId, request: request ) { [weak self] ad, error in if let error = error { print("Interstitial load failed: \(error.localizedDescription)") return } self?.interstitialAd = ad } } func showInterstitialIfReady(from viewController: UIViewController, completion: (() -> Void)? = nil) { guard shouldShowAds, let ad = interstitialAd else { completion?() return } ad.fullScreenContentDelegate = self ad.present(fromRootViewController: viewController) interstitialAd = nil loadInterstitialIfNeeded() // Pre-load the next ad }}// Platform-specific Ad Unit IDsstruct AdConfig { #if DEBUG // Always use test IDs in development static let interstitialId = "ca-app-pub-3940256099942544/4411468910" static let bannerId = "ca-app-pub-3940256099942544/2934735716" #else // Production IDs — separate for each platform #if os(iOS) static let interstitialId = "ca-app-pub-XXXX/YOUR_IOS_INTERSTITIAL_ID" static let bannerId = "ca-app-pub-XXXX/YOUR_IOS_BANNER_ID" #else // Android (in React Native, check with Platform.OS) static let interstitialId = "ca-app-pub-XXXX/YOUR_ANDROID_INTERSTITIAL_ID" static let bannerId = "ca-app-pub-XXXX/YOUR_ANDROID_BANNER_ID" #endif #endif}
Ad Placement Timing and ARPDAU Impact
When and where ads appear has more impact on revenue than which ad network you use:
Ad placement timing comparison:
Session-start interstitial (once per session open):
ARPDAU: ¥3–¥6
Retention impact: Low to moderate
Best for: Games, entertainment apps
Post-task interstitial (after completing a core action):
ARPDAU: ¥2–¥4
Retention impact: Minimal (timing feels natural)
Best for: Utility apps — the optimal pattern
Rewarded video (user-initiated):
ARPDAU: ¥1–¥3
Retention impact: Positive (user chose to watch)
Best for: Freemium with in-app value exchange
Banner (persistent):
ARPDAU: ¥0.5–¥1.5
Retention impact: Low negative
Best for: Low-friction apps where users spend long sessions
Boosting Ad CPMs with AdMob Mediation
For a long time I ran AdMob on its own and quietly assumed "this is just what ad revenue looks like." What changed my mind was mediation. Instead of a single network, multiple ad networks compete for each impression, and the one offering the highest price wins it. Same impression count, higher effective price.
Mediation comes in two flavors. Understanding them before you open the settings screen saves a lot of confusion.
Method
How it works
Strength
Weakness
Waterfall
Queries networks in order of historical eCPM
Intuitive setup, long-established, stable
Drifts from real demand, sequential calls add latency
Bidding
All networks bid in real time simultaneously
Captures the true top price for that impression, low latency
Limited to bidding-enabled networks
The current best practice is bidding-first, with waterfall as a supplement. On my own apps, average eCPM visibly stabilized once I moved the major networks onto bidding. Daily swings remain, but the monthly floor stopped dropping as far.
The networks I connect are AdMob (Google), Meta Audience Network, AppLovin, Pangle, and Unity Ads. Different networks dominate on iOS versus Android, so copying the same setup to both platforms leaves money on the table.
Network
Relative strength on iOS
Relative strength on Android
Notes
AdMob (Google)
High
High
The baseline. Fill this first
Meta Audience Network
Medium–High
Medium
iOS depends heavily on ATT opt-in rate
AppLovin
Medium
High
Strong on Android rewarded ads
Pangle
Medium
High
CPMs climb in Asia and on Android
Unity Ads
Medium
Medium–High
Pairs well with game-style traffic
Exact figures shift dramatically with app genre and geographic mix, so I won't quote hard numbers. But two patterns show up repeatedly:
Regional variation: The US, Japan, and Western Europe carry high eCPMs; Southeast Asia, India, and Latin America run lower. But the latter regions have far larger DAU counts, so on Android "price × impressions" still adds up to meaningful revenue.
Time-of-day variation: eCPM rises during hours when advertiser budgets are active. Session-dependent placements like a launch interstitial ride this wave.
After adding mediation, you start reading reports through two lenses: Fill Rate (which network filled how much) and per-network eCPM. Drop networks with chronically low fill, add newer bidding-enabled ones. Doing this swap once a quarter is unglamorous, but the gains compound quietly.
Protecting iOS Ad Revenue with ATT
Any honest discussion of iOS ad revenue in a dual-store setup has to address ATT (App Tracking Transparency). Since iOS 14.5, an app that wants to track users via the IDFA (advertising identifier) must show the consent dialog. When users tap "Ask App Not to Track," ad targeting accuracy drops and eCPM falls with it.
When I first shipped the ATT dialog, my opt-in rate sat around 30%. Reworking the timing and the copy improves that number.
import AppTrackingTransparencyimport AdSupportfunc requestTrackingIfNeeded() async { // Do nothing if the user already decided guard ATTrackingManager.trackingAuthorizationStatus == .notDetermined else { return } // Present the system dialog let status = await ATTrackingManager.requestTrackingAuthorization() switch status { case .authorized: // IDFA is available — hand it to the ad SDK let idfa = ASIdentifierManager.shared().advertisingIdentifier AdManager.shared.configureTracking(idfa: idfa) default: // Ads still serve in non-tracking mode (at lower CPM) AdManager.shared.configureNonPersonalized() }}
The change that mattered most was not showing the system dialog immediately on launch. Ask someone "may we track you?" before they've experienced any value and most people decline reflexively.
Timing: Show it right after the user finishes the first core action. Opt-in climbs noticeably once they've felt the value.
Pre-prompt: Before the system dialog, use your own screen to explain why permission helps keep the app free. Apple's guidelines require that pre-prompts not mislead.
Design that survives a "no": Users who decline ATT still receive non-personalized ads. SKAdNetwork attribution works without consent, so campaign measurement continues.
Android has no equivalent consent flow. iOS alone pays the ATT penalty on ad pricing. This is one more practical reason to lean ads-heavier on Android and subscription-heavier on iOS.
Unified Revenue Dashboard Design
Operating two platforms simultaneously creates a reporting problem: iOS subscription data is in App Store Connect, Android subscription data is in Google Play, and ad revenue is split between two separate AdMob property IDs.
Build a simple aggregation layer to see everything in one place:
Automating Ad Revenue with the AdMob Reporting API
In the dashboard above, AdMob revenue was still a manual input. As operation continues, that manual step becomes a quiet tax on your time. The AdMob Reporting API lets you pull ad revenue inside the same script as subscription revenue, so the month-start review takes minutes.
# Fetch monthly ad revenue split by platform via the AdMob Reporting APIfrom google.oauth2.credentials import Credentialsfrom googleapiclient.discovery import builddef get_admob_revenue(account_id: str, start: dict, end: dict) -> dict: """ account_id: "pub-XXXXXXXXXXXXXXXX" start / end: {"year": 2026, "month": 4, "day": 1} format """ creds = Credentials.from_authorized_user_file("admob_token.json") service = build("admob", "v1", credentials=creds) report_spec = { "dateRange": {"startDate": start, "endDate": end}, "dimensions": ["PLATFORM"], # split by iOS / ANDROID "metrics": ["ESTIMATED_EARNINGS", "IMPRESSIONS"], } response = service.accounts().networkReport().generate( parent=f"accounts/{account_id}", body={"reportSpec": report_spec}, ).execute() result = {"ios_micros": 0, "android_micros": 0} for row in response: r = row.get("row") if not r: continue platform = r["dimensionValues"]["PLATFORM"]["value"] earnings = int(r["metricValues"]["ESTIMATED_EARNINGS"]["microsValue"]) if platform == "IOS": result["ios_micros"] += earnings elif platform == "ANDROID": result["android_micros"] += earnings return result# ESTIMATED_EARNINGS is returned in micros (1,000,000 = 1 currency unit)raw = get_admob_revenue( "pub-XXXXXXXXXXXXXXXX", {"year": 2026, "month": 4, "day": 1}, {"year": 2026, "month": 4, "day": 30},)admob_ios_jpy = raw["ios_micros"] // 1_000_000admob_android_jpy = raw["android_micros"] // 1_000_000print(f"AdMob iOS: ¥{admob_ios_jpy:,} / Android: ¥{admob_android_jpy:,}")
Note that ESTIMATED_EARNINGS comes back in micros (1,000,000 per currency unit). Miss that and your report is off by six digits — I overlooked it once and briefly celebrated a "1,000,000x" jump in ad revenue. The currency follows your AdMob account setting, so a JPY-denominated account returns yen directly.
Drop this function into the build_monthly_report shown earlier and you can delete the manual admob_ios_jpy and admob_android_jpy inputs. Once all four quadrants — subscription and ads, iOS and Android — populate from a single run, the whole revenue picture is legible at a glance.
Calculating LTV by Platform: Where to Invest Next
The most important strategic question in dual-store operation is: where does an additional ¥10,000 in marketing spend generate the most return? LTV calculations by platform answer this.
def calculate_platform_ltv(platform: str, metrics: dict) -> dict: """ Calculate LTV for each platform separately metrics = { "monthly_arpu": Average revenue per paying user per month "monthly_churn_rate": Fraction of subscribers canceling each month "ad_revenue_per_dau": Monthly ad revenue per active user (non-subscriber) "trial_conversion_rate": Fraction of trial users who convert to paid "cpi": Cost per install (from your ad campaigns) } """ arpu = metrics["monthly_arpu"] churn = metrics["monthly_churn_rate"] ad_rev = metrics.get("ad_revenue_per_dau", 0) conversion = metrics["trial_conversion_rate"] cpi = metrics["cpi"] # Subscriber LTV avg_retention_months = 1 / churn subscription_ltv = arpu * avg_retention_months # Ad user LTV (non-converting users) ad_user_ltv = ad_rev * avg_retention_months # Blended LTV (weighted by conversion rate) blended_ltv = ( subscription_ltv * conversion + ad_user_ltv * (1 - conversion) ) # ROI: every install costs CPI, generates blended_ltv roi_multiple = blended_ltv / cpi if cpi > 0 else float('inf') return { "platform": platform, "subscription_ltv_jpy": int(subscription_ltv), "ad_user_ltv_jpy": int(ad_user_ltv), "blended_ltv_jpy": int(blended_ltv), "avg_retention_months": round(avg_retention_months, 1), "roi_multiple": round(roi_multiple, 2) }# Real-world-approximate numbersios_result = calculate_platform_ltv("iOS", { "monthly_arpu": 1200, "monthly_churn_rate": 0.08, "ad_revenue_per_dau": 120, "trial_conversion_rate": 0.09, "cpi": 800})android_result = calculate_platform_ltv("Android", { "monthly_arpu": 880, "monthly_churn_rate": 0.11, "ad_revenue_per_dau": 160, "trial_conversion_rate": 0.05, "cpi": 200 # Android CPI is typically much lower})for r in [ios_result, android_result]: print(f"{r['platform']}: Blended LTV ¥{r['blended_ltv_jpy']:,} / " f"ROI {r['roi_multiple']}x / " f"Avg retention {r['avg_retention_months']} months")# Output:# iOS: Blended LTV ¥2,592 / ROI 3.24x / Avg retention 12.5 months# Android: Blended LTV ¥847 / ROI 4.24x / Avg retention 9.1 months
This example shows iOS subscribers are worth more individually, but Android's lower CPI produces a higher ROI multiple on paid acquisition. The right conclusion isn't "invest in Android only" — it's "organic growth strategies matter more on iOS; paid acquisition works better on Android."
Store Review Compliance: Platform Differences That Cause Rejections
Running both stores means navigating two different review authorities with meaningfully different standards.
App Store Rules That Trip Up Dual-Store Developers
Guideline 3.1.1 — Payment: In-app purchases of digital content must use Apple's payment system. Linking to external payment options or describing prices for digital items outside the app (even in screenshots or support pages) can trigger rejection.
// ❌ This gets apps rejectedButton("Buy on our website — better price!") { UIApplication.shared.open(URL(string: "https://yoursite.com/subscribe")!)}// ✅ StoreKit onlyButton("Upgrade to Pro") { Task { try await PurchaseManager.shared.purchase(packageId: "pro_monthly") }}
Ad placement: Full-screen interstitials immediately on app launch (within 3 seconds) are a rejection reason. Wait for user interaction before showing the first interstitial.
Subscription disclosure: The "terms of service" and subscription terms (renewal price, renewal period, cancellation instructions) must be visible near the subscription purchase button. Small print below the fold is a common rejection point.
Google Play Rules That Differ from App Store
Cancellation flow: Unlike App Store (where cancellation is in device Settings), Google Play requires apps to provide an in-app cancellation path. This doesn't have to be prominent, but it must exist and work.
// Provide a cancel subscription link inside the appfunc openManageSubscriptions() { #if os(iOS) // iOS: Opens Settings → Apple ID → Subscriptions UIApplication.shared.open( URL(string: "https://apps.apple.com/account/subscriptions")! ) #else // Android: Opens Play Store subscription management // In React Native: Linking.openURL(...) let packageId = "your.android.package.id" UIApplication.shared.open( URL(string: "https://play.google.com/store/account/subscriptions?sku=pro_monthly&package=\(packageId)")! ) #endif}
Deceptive ad behavior: Ads that appear when the user presses the back button, fake system notifications, or ads in notification shade are all rejection or removal triggers. Stick to standard interstitial and banner placements.
Lessons from 12 Months of Dual-Store Operation
Lesson 1: Android ad revenue has stronger seasonal variation
iOS subscription revenue is relatively stable month-to-month. Android AdMob revenue tracks advertiser budget cycles — Q4 (October–December) ad rates are significantly higher than Q1. Plan your cash flow accordingly; don't mistake Q4 AdMob spikes for a permanent new baseline.
Lesson 2: iOS design quality transfers well to Android
App Store design patterns that pass Apple's review — clean layouts, clear hierarchy, thoughtful interactions — consistently receive positive reception on Google Play too. Android users respond to quality. Don't build a "good enough for Android" variant of your app.
Lesson 3: Annual plan ratio determines LTV
The fraction of subscribers who choose annual over monthly plans is the single highest-leverage variable in your subscription economics. Moving annual plan ratio from 20% to 35% increased my 6-month MRR by 17% without acquiring a single additional user. Promote annual plans actively on both platforms. Lead with the monthly-equivalent price, show the discount prominently, and put the annual option visually first in your pricing UI.
Starting Your Android Expansion This Week
The first step in dual-store expansion isn't building an Android app — it's adding Android to your RevenueCat project. Once the subscription logic is abstracted, the remaining work is:
Create Android products in Google Play Console matching your iOS offerings
Configure separate AdMob property for Android (separate from your iOS property)
Test the purchase flow on a physical Android device (emulators handle billing differently)
Prepare Android-specific screenshots (16:9 or 18:9 aspect ratios, vs. iOS 19.5:9)
Submit to Google Play — review times are typically 1–3 days vs. 1–7 days for App Store
If your iOS app generates meaningful revenue, Android expansion has a near-zero risk profile. You're not rebuilding anything — you're extending what already works into a market where your existing content, your existing feature set, and your existing pricing model are untested and potentially profitable.
The ¥130,000 additional monthly revenue I described at the start came from this exact process: the same app, extended to Android, with adjusted pricing for the market. The investment was about 3 weeks of part-time work.
Your next move is opening your RevenueCat dashboard and clicking "Add App" for Android.
Deep Dive: Churn Prevention Across Both Platforms
Churn dynamics differ between iOS and Android in ways that affect your prevention strategy. Understanding these differences before you see a churn spike saves significant revenue.
iOS Churn Patterns
iOS subscription churn tends to cluster around specific moments:
Day 14: End of free trial — the largest churn event
Month 3: Users who haven't built a habit often cancel at their first "why am I paying for this?" moment
Annual renewal: Some annual subscribers cancel rather than renew, even satisfied ones
The most effective iOS churn prevention is making the app genuinely useful before the trial ends. This sounds obvious, but most apps don't engineer it — they rely on the user to discover value on their own.
// Proactive value delivery during trialclass TrialOnboardingManager { let userId: String func runDayThreeCheck() async { let completedCoreAction = await hasCompletedCoreAction(userId: userId) if !completedCoreAction { // User is 3 days in and hasn't used the main feature // Intervene before they lose interest await sendPushNotification( to: userId, title: "3 days in — have you tried \(CoreFeature.displayName)?", body: "Takes 2 minutes. Here's what it does: [specific value statement]", deepLink: "/core-feature/guided-tour" ) await Analytics.log("trial_day3_intervention_sent", userId: userId) } } func runDayTwelveCheck() async { let subscriptionValue = await calculateValueDelivered(userId: userId) // Two days before trial ends — personalized value reminder await sendEmail( to: userId, subject: "Your trial ends in 2 days", template: "trial_ending", data: [ "tasks_completed": subscriptionValue.tasksCompleted, "time_saved_minutes": subscriptionValue.timeSavedMinutes, "personal_highlight": subscriptionValue.topAction ] ) }}
Android Churn Patterns
Android churn happens faster and is harder to recover. Users who stop engaging on Android tend to uninstall rather than keep the app dormant. Once uninstalled, re-acquisition is expensive.
The most effective Android retention mechanism is push notification engagement — Android's notification system is less restrictive than iOS, and well-timed notifications maintain app recall.
// Android notification engagement tracking// (In React Native, use firebase/messaging or expo-notifications)struct NotificationEngagement { let userId: String func checkAndSendReEngagement() async { let daysSinceLastOpen = await getDaysSinceLastAppOpen(userId: userId) switch daysSinceLastOpen { case 3: await sendNotification( to: userId, title: "Quick check-in", body: "You have [X] things waiting in \(AppName.displayName)" ) case 7: await sendNotification( to: userId, title: "Don't lose your streak", body: "Your [feature] data is still here. Pick up where you left off." ) case 14: // Last attempt before treating as churned await sendNotification( to: userId, title: "Still there?", body: "We've kept your [data/work/progress] safe. Takes 30 seconds to check in." ) case let days where days > 21: // Mark as at-risk, consider offering a discount on next renewal await flagForChurnIntervention(userId: userId) default: break } }}
The Price Increase Transition
If your subscription price increases — which it will as your app improves — existing subscribers need careful handling. Handling this well retains most subscribers; handling it poorly creates a public ratings crisis.
# price_increase_manager.pyNOTICE_DAYS = 60 # Minimum 60 days noticeLOYALTY_THRESHOLD = 6 # Months before qualifying for loyalty discountdef notify_price_increase( subscribers: list, old_price: int, new_price: int, effective_date: str) -> dict: results = {"notified": 0, "loyalty_offered": 0, "errors": 0} for sub in subscribers: months_active = sub["months_subscribed"] try: if months_active >= LOYALTY_THRESHOLD: # Long-term subscribers get a lock-in offer lock_in_price = int(old_price * 1.05) # 5% increase only send_loyalty_email( email=sub["email"], platform=sub["platform"], old_price=old_price, new_price=new_price, lock_in_price=lock_in_price, effective_date=effective_date ) results["loyalty_offered"] += 1 else: send_price_increase_email( email=sub["email"], platform=sub["platform"], old_price=old_price, new_price=new_price, effective_date=effective_date ) results["notified"] += 1 except Exception as e: log_error(f"Price increase notification failed for {sub['email']}: {e}") results["errors"] += 1 return results
Prompt Strategy for Rork Max Cross-Platform Features
Generating monetization features with Rork Max requires explicit platform context in your prompts. Vague prompts produce vague code — especially for billing flows where precision matters.
Effective Prompt Patterns for Monetization Features
Pattern 1: Paywall screen with platform-specific compliance
───────────────────────────────────────────────────────────
"Create a subscription paywall screen for iOS (SwiftUI) with these specs:
- Display two plans: Monthly ¥1,200 and Annual ¥9,800 (shown as ¥817/month, 32% off)
- Annual plan displayed first and visually highlighted
- '14-day free trial' banner prominently visible
- Legal copy at bottom: subscription terms, renewal disclosure, cancellation link
- Loading state while StoreKit products are fetching
- Error state for payment failures with retry option
- Dismiss button (top-right X) — App Store requires users to be able to dismiss paywalls
Target: App Store review compliance + high conversion"
Pattern 2: AdMob integration with subscription gate
───────────────────────────────────────────────────────────
"Implement interstitial ad display logic with these rules:
1. Never show ads to users with active 'premium' entitlement (check via RevenueCat)
2. Show one interstitial per session, only after user completes a core action (not on launch)
3. Pre-load the next ad immediately after one is shown
4. Implement 5-second countdown before close button appears (Google Play recommendation)
5. Handle all error states (ad not loaded, network error) gracefully with no crash
Include separate ad unit IDs for iOS and Android (use test IDs in #DEBUG mode)"
Pattern 3: Revenue analytics view
───────────────────────────────────────────────────────────
"Build an admin dashboard view showing:
- Monthly revenue breakdown: subscription revenue vs ad revenue, by platform (iOS/Android)
- DAU chart for the last 30 days with platform split
- Current subscriber count by plan tier
- Monthly churn rate with trend arrow
Data comes from a REST API returning JSON: [define your actual schema here]
The view is admin-only, accessible via a secret gesture (10-tap on app version label)"
The more specific the prompt — exact prices, exact field names, exact API shapes — the less cleanup work the generated code requires. Rork Max is good at inferring intent but exact specifications produce exact output.
Financial Planning: What Dual-Store Revenue Looks Like at Scale
Understanding the economics at different revenue levels helps you make better decisions about reinvestment and growth strategy.
The margin improvement with scale reflects the fixed cost structure: server and support costs don't grow as fast as revenue once the architecture is established. This is the compounding advantage of software businesses — and specifically why dual-store operation makes sense. The incremental cost of serving Android users once the infrastructure exists is near zero.
The First Move: Add Android to Your Next Release
If you've read this far and have an iOS app already generating revenue, the path to dual-store is shorter than you might expect:
This week: Add Android to your RevenueCat project. Set up matching products in Google Play Console.
Next two weeks: Configure your AdMob Android property. Wire the Android ad unit IDs into your existing AdMob implementation.
Week three: Test end-to-end purchase flow on a real Android device. TestFlight equivalent is Google Play Internal Testing.
Week four: Prepare Android store listing. Screenshots need different aspect ratios, but the description text can be adapted directly.
Submit: Google Play review is typically 1–3 days for an established developer account.
The timeline from "iOS-only" to "live on both stores" is 3–4 weeks of focused part-time work. The revenue upside, based on my own experience and the numbers in this guide, is a 40–80% increase in monthly earnings from an app that's already working.
The most expensive decision in indie app development is waiting to expand into a market that's already paying for apps like yours.
Monitoring What Matters: The Three Metrics That Predict Revenue
With two platforms generating four revenue streams (iOS subscriptions, iOS ads, Android subscriptions, Android ads), it's easy to track too many things and act on none of them. Narrow your operational attention to three numbers:
Blended Day-30 Retention by Platform: Users who are still active 30 days after install. This is the leading indicator for LTV. If this drops on one platform and not the other, the problem is platform-specific — maybe an OS update broke something, or a specific device type has a performance issue.
Net MRR Movement (New − Churned × Price): Track new subscriber revenue versus lost subscriber revenue weekly, by platform. A platform where churned revenue exceeds new revenue is contracting. Identify this fast.
Ad Revenue per 1,000 DAU (eCPM proxy): Divide your monthly ad revenue by your DAU count divided by 1,000. This tells you whether your ad revenue per active user is stable, improving, or declining — independent of user count changes. Declining eCPM often signals ad placement issues or audience targeting drift.
Check these three numbers weekly. Everything else — total downloads, App Store ranking, review count — is context, not action drivers.
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.