RORK LABJP
DEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per monthDEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per month
Back to Blog

When Review Prompts, Paywalls, and Ads Pile Up in a Rork App — How 12 Years of Indie Development Taught Me to Direct the Traffic

RorkIndie DevApp DesignUXMonetizationApp Business

The other day I was using one of my own Rork-built apps on my iPhone for the first time in a while. Right after I saved a single image, a review prompt appeared. The moment I dismissed it, a paywall sheet slid up. And behind that, an ad had apparently finished loading, because when I closed the paywall, a full-screen ad took over. Three modals in three seconds. Even though I had built the app myself, I was honestly annoyed.

What struck me was that the version of me who wrote each of those dialogs had placed every one of them with good intentions. You saved something, so you must be satisfied — show the review prompt. You used a feature, so suggest the upgrade. You reached a natural break, so show the ad. Each decision was reasonable on its own. But when each one fires independently, unaware that the others exist, the user simply experiences "an app that throws annoying things at me one after another." Today I want to write down how I directed that traffic, from the perspective of running apps with over 50 million cumulative downloads across 12 years.

Why modals pile up despite good intentions

The root of the problem is that each modal is triggered by a different event.

A review prompt feels right after a satisfaction signal like a save or a favorite. A paywall feels right at the third use, or when the free quota is hit. An ad feels right at a screen transition or a session break. In a real app, these events almost always land in the same instant — because the moment a user accomplishes something is simultaneously a satisfaction signal, a usage milestone, and a screen break.

So as long as the triggers live in separate places, the overlap isn't an occasional accident. It's structurally guaranteed. I only understood this after getting burned repeatedly while running Beautiful HD Wallpapers. There was no bug anywhere in the code, yet the experience was broken. The cause was three correct decisions colliding in the same spot.

Centralize the decision of what to show

The solution I arrived at is simple: take the decision of "which modal to show" away from each feature, and hand it entirely to a single gatekeeper. In my native apps I implement this as a central manager I call ModalGate. Each feature only requests permission to appear; whether it actually appears is decided in one place.

As code, the idea looks like this. Define the priority in one spot, and only when nothing is currently on screen, show exactly one — the highest-priority pending modal.

// modalGate.ts — direct all modal traffic from one place
type ModalKind = "rewardAd" | "paywall" | "reviewPrompt";
 
// Smaller number = higher priority. The order — ad (revenue),
// paywall (business), review (asset) — keeps the last caller from winning
const PRIORITY: Record<ModalKind, number> = {
  rewardAd: 1,
  paywall: 2,
  reviewPrompt: 3,
};
 
let activeModal: ModalKind | null = null;
const pending = new Set<ModalKind>();
 
// Each feature just requests here
export function requestModal(kind: ModalKind) {
  pending.add(kind);
  flush();
}
 
function flush() {
  // If something is already up, never stack on top of it
  if (activeModal !== null) return;
  if (pending.size === 0) return;
 
  // Pick only the single highest-priority pending modal
  const next = [...pending].sort((a, b) => PRIORITY[a] - PRIORITY[b])[0];
  pending.delete(next);
  activeModal = next;
  showModal(next); // your actual presentation logic
}
 
// When a modal closes, consider the next one
export function onModalDismissed() {
  activeModal = null;
  // Showing the next immediately becomes a chain, so leave a gap
  setTimeout(flush, 800);
}

The most important line here is the setTimeout inside onModalDismissed. If you show the next modal the instant one closes, you're back to "an app that throws things at me in a row." I leave at least 0.8 seconds of empty space after a dismissal, and ideally, if the user has moved to a different screen during that gap, I don't show the next one at all. Not stacking and not chaining are two different problems, and the gate has to handle both.

Bringing this into a Rork app

Apps generated by Rork are React Native under the hood, so this traffic control carries over directly. What I found practical was making the request gate a React context, so each screen only says "I'd like to appear."

// useModalGate.tsx — a minimal gate for a Rork (React Native) app
import { createContext, useContext, useRef, useState, useCallback } from "react";
 
type ModalKind = "rewardAd" | "paywall" | "reviewPrompt";
const PRIORITY: Record<ModalKind, number> = { rewardAd: 1, paywall: 2, reviewPrompt: 3 };
 
const Ctx = createContext<(k: ModalKind) => void>(() => {});
 
export function ModalGateProvider({ children }: { children: React.ReactNode }) {
  const [active, setActive] = useState<ModalKind | null>(null);
  const pending = useRef<Set<ModalKind>>(new Set());
 
  const flush = useCallback(() => {
    if (active !== null || pending.current.size === 0) return;
    const next = [...pending.current].sort((a, b) => PRIORITY[a] - PRIORITY[b])[0];
    pending.current.delete(next);
    setActive(next);
  }, [active]);
 
  const request = useCallback((kind: ModalKind) => {
    pending.current.add(kind);
    flush();
  }, [flush]);
 
  const dismiss = useCallback(() => {
    setActive(null);
    setTimeout(flush, 800); // the gap that prevents chaining
  }, [flush]);
 
  return (
    <Ctx.Provider value={request}>
      {children}
      {/* render exactly one modal based on the active value */}
      {active === "paywall" && <Paywall onClose={dismiss} />}
      {active === "reviewPrompt" && <ReviewPrompt onClose={dismiss} />}
      {active === "rewardAd" && <RewardAd onClose={dismiss} />}
    </Ctx.Provider>
  );
}
 
// Screens just call this. The gate decides whether to show anything
export const useRequestModal = () => useContext(Ctx);

When you tell Rork "show a review prompt when the save button is tapped," it often writes the dialog presentation directly into the button's onPress. That works fine on its own — but as you add paywall and ad logic through separate prompts, each ends up with its own independent firing point. So my workflow is to build everything with Rork first, then rewrite only the presentation logic to go through this gate. Generation speed and a design that survives real operation work better when you separate the two steps.

When to show — the priority order itself

Once the gate exists, the last question is: when all three arrive at once, which one wins? My criterion is how easily each can be recovered.

An ad, if not shown in that moment, loses the revenue opportunity — you can't replay "the ad from earlier." A paywall, even if you skip it now, can be shown again at the next milestone. And a review prompt is the most precious asset of all, because the OS caps how many times it can appear per year. So my priority is ad, then paywall, then review, and I save the review prompt for the moment when nothing else is showing and the user is clearly satisfied.

This order changes with the shape of your business. For an app where ad revenue is thin and subscriptions are the core, the paywall should come first. What matters isn't the specific order but the state of having decided it deliberately in one place. In an app like Law of Attraction Everyday that people open daily, I delay the review prompt to a moment I can recommend wholeheartedly — like the day a user opens it seven days in a row. Getting three stars by asking early helps less than getting five stars by asking late.

I'm reminded of my grandfather, a temple carpenter, who always aligned the grain of the wood before driving a single nail. Each nail can go in correctly, but if the grain isn't aligned, the whole thing warps. App modals are the same: each individual decision being correct, and the whole feeling pleasant to use, are two things you have to design separately.

Your next step

If you have an app in production right now, start by writing down where the review prompt, paywall, and ad presentations are called from in your code. If all three are called from different files, that's your source of overlap. As a first step, build one small gate like the requestModal in this article and route just two of the modals through it — even that alone changes the experience noticeably. Thank you for reading.