◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-15Advanced

The Day a Third Reason to Hide Ads Appeared — Folding Rork App Ad-Free Logic Into One Place

Ads show only on one screen for paying users, or ads never show for free users. The usual cause is that the condition for hiding ads is scattered across the code. Here is how I fold three reasons — subscription, lifetime purchase, and a timed reward unlock — into a single state and route every ad through one hook, written as an implementation note from running six apps as an indie developer.

Rork611Expo234AdMob75RevenueCat30State Management8Zustand4Monetization40

✦ Premium Article

I once got an App Store review from a user saying ads still showed even though they had paid. When I dug in, the purchase state itself was read correctly, and ads were properly hidden on the home screen. The only place ads remained was a newer "favorites" screen I had added later.

The cause was easy to find. When I built that screen, I decided whether to show ads with a single line: if (!isSubscribed). But by then my app had not one but three reasons to hide ads — a subscription, a lifetime ad-removal purchase, and a timed unlock where watching a rewarded ad hides ads for a while. The new screen only looked at the subscription, so ads appeared only for users who had bought the lifetime option.

When there is just one condition, if (!isSubscribed) is fine. The trouble starts when monetization quietly adds more reasons to hide ads, and the checks you wrote earlier get left behind. This article is about cutting that drift off at the root: folding the ad-free decision into one piece of state and routing every screen through the same entry point, assuming an Expo app generated by Rork.

Reasons to Hide Ads Pile Up Without You Noticing

When I first shipped, my app had exactly one way to remove ads: a lifetime purchase. Six months later I added a subscription, and then, as a retention play, a timed unlock through rewarded ads. Each arrived at a different time, from a different monetization decision.

This is where the decision logic scatters. You touch the ad code when you add the subscription, and touch it again when you add the reward. Each time, the scope you need to update is supposedly "everywhere ads appear" — but human memory is unreliable. Once you pass ten screens, something always gets missed.

And the miss cuts both ways. Forget to hide ads, and you show ads to people who paid, which leads directly to refunds and lower ratings. Hide them too aggressively, and free users see no ads, so that revenue disappears too. Both drift silently, so you usually notice only after the numbers move.

So the real problem is not "the check is wrong." It is "the same check lives in several places and only one of them gets updated." What needs fixing is not the individual if, but the structure that should gather the decision into a single location.

Folding Three Reasons Into One State

First, make the reasons to hide ads explicit as a type. Code that Rork emits tends to keep logic closed within each screen, so pulling this out into a shared module is the first step.

// lib/adFree/types.ts
export type AdFreeReason =
  | 'subscription'   // an active subscription
  | 'lifetime'       // a one-time ad-removal purchase
  | 'reward'         // a timed unlock from a rewarded ad (has an expiry)
  | null;            // show ads (no unlock)
 
export interface AdFreeState {
  reason: AdFreeReason;
  // only meaningful when reason === 'reward'. Epoch milliseconds.
  rewardExpiresAt: number | null;
}

The key is to not carry an isAdFree: boolean from the start. If all you keep is a boolean, you later lose track of why ads are hidden, and you cannot trace bugs like "the reward expired but ads stayed off." Keeping the reason lets you feed it straight into logs, and lets the UI show things like "ads are off for another 32 minutes."

The three reasons have a precedence. I evaluate them in this order:

  1. lifetime — once unlocked, it is permanent. Highest priority; ads never show regardless of any other state.
  2. subscription — no ads while the entitlement is active.
  3. reward — no ads only while the window has not expired.

This ordering matters when, say, a user who already owns the lifetime purchase happens to watch a rewarded ad. Because lifetime is highest priority, the behavior never gets dragged around by the reward's expiry bookkeeping. Part of the value of gathering the decision in one place is that you write this precedence correctly exactly once.

✦

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
✦The full implementation of a useAdFree selector that composes subscription, lifetime, and a timed reward unlock with explicit precedence
✦A timer design that fires exactly at the next expiry instead of every second, so a reward window flips off the moment it should
✦A measured fix for the bug where rewinding the device clock keeps ads hidden forever, closed with a monotonic-time check
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.

or
Unlock all articles with Membership →
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 $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-06-12
Building a Developer Debug Menu Into Your Rork App — Verify Ads, Purchases, and Remote Config Before Release
A production-safe developer debug menu for Rork apps — switch environments, force test ads, simulate entitlements, and override Remote Config, with working TypeScript code and the pitfalls I hit running six apps.
⬡ Dev Tools2026-04-24
Rork × AdMob UMP × ATT Consent Management for Production — Keeping Revenue Alive in Global Releases
A production playbook for wiring UMP and ATT into your Rork app so you stay GDPR-compliant while protecting AdMob revenue, with working code throughout.
⟐ Business2026-04-04
Rork App Revenue Design: Where Ads and Subscriptions Eat Each Other
How to layer AdMob, subscriptions, and one-time purchases in a Rork app without letting them cannibalize each other. Covers value-density placement, entitlement-first RevenueCat architecture, server-side receipt verification, and behavior-triggered paywalls.