Closing an ad tapped the game underneath: a tap guard for AdMob in Capacitor games
While playing Backpack Battle on my phone, I closed a rewarded video, and the game immediately did something I hadn't asked for. A button under where the ad's close X had been got pressed.
I checked every other game. None of them had any protection against this, so all of them had the bug.
What's happening
A full-screen AdMob ad is a separate native Activity on top of your app. The player taps the X to close it. The finger goes down on the ad, and the ad starts closing on that touch. By the time the finger lifts, your WebView is visible again, so the lift and the resulting click land on whatever your game has drawn at that spot.
Mobile browsers also synthesise a click after a touch sequence, and sometimes a delayed mouse* sequence too, so even a clean tap can produce a "ghost" event a few hundred milliseconds later.
In Backpack Battle that might buy an item. In an idle game it might trigger a reset. In a word game it might start a new puzzle. Every time, it feels to the player like the game has a mind of its own.
The fix: a tap guard
The fix is a small module, the same file in every game. It works in the capture phase, so it sees events before any game code does:
- When an ad (or a consent form, or a privacy options form) is about to open, the game calls
adOpening(). - From then on, the guard swallows every pointer, touch, mouse and click event, and stops it before the game sees it.
- When the ad closes, the game calls
adClosed(). - The guard keeps swallowing until it sees a new
pointerdownthat starts at least 350 ms after the ad closed. A fresh, deliberate touch, not the tail of the one that closed the ad.
"Closed" means whichever comes first: the SDK's Dismissed event, or the page becoming visible again. That matters, because I'd learned in an earlier game that Dismissed travels over the Capacitor bridge and can arrive seconds late. Waiting for it alone would freeze the game for no reason.
There's also a 150-second cap. If an ad call hangs and never reports closing, the guard gives up and lets input through, so a broken ad can't lock the game.
Testing it
The guard was tested two ways:
- Simulated events: a ghost lift and click after close, a double tap on the X, a late
Dismissed, and a hung ad call that never resolves. - A real ad on an emulator: a test rewarded ad in Backpack Battle. I double-tapped the X, the reward arrived, and the next tap did exactly what it should.
Shipping it everywhere at once
Because every game shares the same platform layer (and most are forks of the Word Search engine), the fix went into all twelve live games the same day, each as a full release. The release check now confirms three things in every bundle: the tap guard is present, there are no test ad IDs, and there's no Play Games init provider (that one's a story of its own).
The rule for future code: every ad or form show goes through adOpening() and adClosed(). New forks inherit it from the engine, so the next game gets it for free.
Why it's worth writing about
This is the kind of bug analytics never shows you. There's no crash and no error, just a player who closed an ad, saw something unexpected happen, and maybe decided the game was janky. If you show full-screen ads in a web-based game, check it now: close an ad with a slow tap and see what your game does.