CLAUDE LABJP
FABLE5 — Claude Fable 5 launches (Jun 9): the first generally available Mythos-class model, beyond Opus, with 1M-token context, 128k output, and always-on adaptive thinkingFREE-WINDOW — Fable 5 is included free on Pro, Max, Team, and Enterprise through Jun 22; usage credits required from Jun 23. API pricing is $10/$50 per MTokSAFEGUARDS — Fable 5 falls back to Opus 4.8 on high-risk topics (under 5% of sessions); the unrestricted Mythos 5 is limited to vetted organizationsIPO — Anthropic confidentially files for an IPO (Jun 1), with a reported $65B raise, $965B valuation, and $47B annualized revenueBILLING — 3 days to the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly creditsPLATFORM — Claude Developer Platform adds Managed Agents scheduled deployments, vault env credentials, and session thread webhook eventsFABLE5 — Claude Fable 5 launches (Jun 9): the first generally available Mythos-class model, beyond Opus, with 1M-token context, 128k output, and always-on adaptive thinkingFREE-WINDOW — Fable 5 is included free on Pro, Max, Team, and Enterprise through Jun 22; usage credits required from Jun 23. API pricing is $10/$50 per MTokSAFEGUARDS — Fable 5 falls back to Opus 4.8 on high-risk topics (under 5% of sessions); the unrestricted Mythos 5 is limited to vetted organizationsIPO — Anthropic confidentially files for an IPO (Jun 1), with a reported $65B raise, $965B valuation, and $47B annualized revenueBILLING — 3 days to the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly creditsPLATFORM — Claude Developer Platform adds Managed Agents scheduled deployments, vault env credentials, and session thread webhook events
Articles/Claude Code
Claude Code/2026-06-12Intermediate

Fixing Overlapping Paywall and Review Dialogs on Android with a Central ModalGate

How I fixed three dialogs — paywall, review prompt, and rewarded-ad promo — stacking on top of each other with a priority-based central gate, plus the three dismiss-leak paths a Claude Code design review uncovered.

claude-code114android9kotlin3dialogapp-dev

In May 2026, near the end of shipping v2.1.0 of the Android version of my wallpaper app, a screenshot from a test device made me do a double take. A review prompt was sitting on top of the paywall, and behind both of them, a rewarded-ad promo dialog was trying to open.

I've been an indie developer since 2014, and for most of that time my approach to dialog coordination was "it works, so it's fine." When the paywall was the only modal in the app, that was true — nothing could collide with it. The moment this update brought the count to three (PaywallDialog, ReviewInduction, and RewardedIntersDialog), the unspoken assumption collapsed.

The fix ended up being a single small class I named ModalGate, but the part that surprised me was how much a design review with Claude Code contributed along the way. If you're seeing symptoms like "dialogs stack on top of each other" or "after closing one dialog, nothing ever appears again," here are the traps I found and the implementation that resolved them.

The three traps behind overlapping modals

What looked like one bug turned out to be three separate traps tangled together.

Trap 1: Each dialog is unaware of the others

The paywall checked "not subscribed and third launch or later." The review prompt checked "seven days since install and not yet reviewed." The rewarded promo checked "ad-free credit not yet consumed." Each dialog evaluated only its own condition. When conditions are independent, a day will inevitably come when all of them are true at once. In my case, the intersection was a user who kept using the app past day seven without subscribing.

Trap 2: Priority silently depends on call order

The three checks lived next to each other in MainActivity's onResume, so "whichever is written first wins" was the de facto priority system. Later, while reorganizing initialization code, I reordered those lines — and a different dialog started appearing. Nowhere in the codebase did it say "the paywall has top priority," so no code review could have caught it.

Trap 3: Dismiss chaining and flag leaks

At one point I added a quick boolean "modal showing" flag. But the OK button is not the only way to close a dialog. Back-key presses and outside taps went through a path where the flag never got cleared, and once that happened, no modal would ever appear again. A paywall that never shows hits revenue directly, so this was worse than the overlap itself.

The root cause: dialogs that decide for themselves

The common root of all three traps is that each dialog decided on its own whether it could appear. Every time a new modal joins the cast, you need more "check everyone else's state" code scattered around, and every missed check becomes a collision.

So I inverted the policy. Each dialog only applies to be shown; the decision of whether it may show is made in exactly one place. That single place is ModalGate.

Designing ModalGate — a priority-based central gate

There are only three design rules.

  • Only ModalGate decides whether a modal may show (dialogs just apply)
  • Priority is explicit and numeric (paywall > rewarded promo > review prompt)
  • A denied modal is not carried over (no chain-showing the next dialog right after one closes)

Here is nearly the entire implementation. It is less a clever algorithm than a small container that concentrates the decision in one place.

// ModalGate.kt — central manager for modal display (singleton)
object ModalGate {
 
    // Tag of the modal currently showing. null means nothing is visible
    private var activeTag: String? = null
 
    // Priority definitions (lower rank wins)
    enum class Priority(val rank: Int) {
        PAYWALL(0),          // paywall
        REWARDED_PROMO(1),   // rewarded-ad promo
        REVIEW_INDUCTION(2)  // review prompt
    }
 
    /**
     * Apply to show a modal. Returns true only when granted.
     * The caller may invoke show() only on a true result.
     */
    @Synchronized
    fun request(tag: String, priority: Priority): Boolean {
        val current = activeTag
        if (current != null) {
            Log.d("ModalGate", "denied: $tag (active=$current)")
            return false // something is showing: deny unconditionally, no chaining
        }
        activeTag = tag
        Log.d("ModalGate", "granted: $tag (priority=${priority.rank})")
        return true
    }
 
    /** Must be called from DialogFragment.onDismiss() */
    @Synchronized
    fun release(tag: String) {
        if (activeTag == tag) {
            activeTag = null
            Log.d("ModalGate", "released: $tag")
        }
    }
 
    /** Restore after process recreation (see the trap below) */
    @Synchronized
    fun restore(tag: String?) {
        activeTag = tag
    }
}

The call site looks like this:

// Display check for the review prompt (caller side)
if (shouldShowReviewInduction() &&
    ModalGate.request("review", ModalGate.Priority.REVIEW_INDUCTION)) {
    ReviewInductionDialog().show(supportFragmentManager, "review")
}

When the paywall and review prompt conditions become true at the same time, the log reads:

D/ModalGate: granted: paywall (priority=0)
D/ModalGate: denied: review (active=paywall)
D/ModalGate: released: paywall

One judgment call worth writing down: I considered a chaining variant where a denied review prompt would appear right after the paywall closes, and decided against it. From the user's side, that design feels like an app where closing one popup just summons the next. A denied modal simply gets re-evaluated on the next launch — and a review prompt arriving a day late costs you almost nothing. The experience is calmer for it.

The three dismiss-leak paths a Claude Code review uncovered

I got as far as calling release() from onDismiss on my own, but trap 3 had made me cautious, so I asked Claude Code for a design review with a deliberately adversarial prompt: "List every path where release() will not be called." It came back with three.

  • Back key and outside taps: relying on setOnDismissListener alone can leak on the onCancel path. Consolidating into an override of DialogFragment's onDismiss() gave me a single choke point that fires no matter how the dialog closes
  • Process recreation: on a device with the developer option to discard activities enabled, the FragmentManager restores the dialog on return — but the singleton's activeTag has been reset to null. The dialog is visible, yet the gate reports open. I added an onCreate pass that scans FragmentManager tags and calls restore()
  • Screen rotation: if the display checks re-run during recreation, the restored dialog and a freshly requested one appear twice. Moving the checks out of onResume and into user-action triggers plus the first-launch flow avoided it

To be honest, the process-recreation case is one I would not have found alone. The mismatch between a singleton's lifetime and FragmentManager's restore timing only reproduced once I enabled the discard-activities option on a real device. My takeaway on review prompts: asking "list the paths where this is never called" finds more than asking "does this look correct?"

What changed after shipping

About a month after the v2.1.0 release, I have not seen a single overlapping-modal report in testing or production. The "nothing ever shows again" state from trap 3 is gone at the root, because flag management now lives in the single release() path. The rewarded-ad promo reliably appearing at its intended moments has also quietly helped on the AdMob revenue side.

A side benefit: adding a new modal is no longer a review burden. A new dialog adds one Priority line and goes through request() — there is no per-pair collision analysis against every existing modal.

This same v2.1.0 release also included a crash-fixing effort that ran in parallel with the modal cleanup. I wrote up that story in How I Fixed Android RecyclerView Crashes in 28 Days Using Claude Code, and the post-release monitoring setup in Automated Play Store Staged Rollout Monitoring with Claude Code — Lessons from 50+ Crashes in v2.0.0.

Where to start

ModalGate itself is under fifty lines; most of the value comes from the policy decision to concentrate display decisions in one place. As a first step, grep your project for .show( and list every site that presents a modal. If none of them check the visibility state of the others, it is worth installing this pattern before your modal count reaches three.

I hope this saves someone the debugging session it cost me.

Share

Thank You for Reading

Claude 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 →

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

Claude Code2026-06-12
Untangling Android Back-Button Ad Gates: A Parallel, Priority-Ordered Redesign with Claude Code
Nested back-button ad gates fired at the wrong moments. The parallel, priority-ordered redesign we shipped in v2.1.0, with Claude Code, Kotlin, and tests.
Claude Code2026-05-16
How I Fixed Android RecyclerView Crashes in 28 Days Using Claude Code
After releasing v2.0.0 of Beautiful HD Wallpapers, RecyclerView IndexOutOfBoundsExceptions hit 50+ users over 28 days. Here's how a conversation with Claude Code uncovered the root cause — a defensive copy pattern.
Claude Code2026-05-27
How Claude Code Helped Me Kill a Glide 5.0.5 Java 8 Crash with One Line
After Beautiful HD Wallpapers v2.0.0 shipped, every Android 6.0.1 user crashed within 3 seconds. The fix turned out to be a single missing line in build.gradle.kts — and Claude Code is what got me there.
📚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 →