AdMob Next-Gen SDK takes the app ID in code
I moved a Capacitor game to Google's Next-Gen ads SDK. All twelve device tests passed and not one ad was served, because the new SDK no longer gets its app ID from the manifest.
Contents
I moved Picture Word Connect from the legacy Google Mobile Ads library to the AdMob Next-Gen SDK, rebuilt it, and ran my twelve device tests on an Android emulator. Twelve passed. The game then served no banners, no interstitials and no rewarded videos, and nothing in that run said so.
The reason was one line of config the old SDK never read. Three days later I made the same mistake in a second game, Echo Bat, which is the part that made me write it down.
The versions involved
The two SDKs are different Maven artifacts, not two releases of one, so a version number only means something next to its artifact name. These are the ones from the switch:
| Before | After | |
|---|---|---|
| Capacitor plugin | @capacitor-community/admob 8.2.1 |
our shared ads package, a Capacitor plugin at 1.1.0 |
| Android library | com.google.android.gms:play-services-ads 25.4.+ (resolved to 25.4.0) |
com.google.android.libraries.ads.mobile.sdk:ads-mobile-sdk 1.5.0 |
| iOS | Google Mobile Ads | Google Mobile Ads, 13.11.x |
Capacitor itself was 8. The new library needs minSdk 24 and compileSdk 35 or later; this game was already on 24, so nothing changed there. The AdMob Next-Gen SDK version I ran was 1.5.0, the only version of that artifact Gradle ever downloaded on this machine. For comparison, the Unity plugin I tested in September knew about 1.4.0 and did not use it by default, as I noted in the post about its empty App ID check.
Two things I took on trust from our wrapper's notes rather than testing. First, Google's deprecation notice dates the legacy Android library: deprecated on 2027-06-30, sunset on 2028-06-30. Second, the two libraries share classes, so Gradle stops on duplicates if both are in one build. I uninstalled the old plugin before adding the new one and never tried having both.
On iOS there is nothing to switch yet. The wrapper's notes say Google has no AdMob Next Gen SDK iOS release, so it keeps the regular Google Mobile Ads SDK there. If you are reading this later, the AdMob Next-Gen SDK release notes are the place to check whether 1.5.0 is still current and whether that has changed.
Twelve passes, zero ads
My device tests drive the real game on an emulator: launch it, win a level, open the shop, buy a hint pack. They had all passed on the legacy build minutes earlier, and they all passed again on the Next-Gen build.
None of them checked that an ad appeared. The game is built never to wait for an ad, so from the outside a missing ad and a slow one look the same, and every scenario carried on past both.
I only noticed because the run's report did not mention ads at all, so I wrote a thirteenth
scenario that asks the game how tall its banner is. Its first step, that the game
calls showBanner('BOTTOM'), passed. The second failed:
❌ Ads serve, on Google's Next-Gen SDK (39.8s): step 2
`expectState: { ads.banner.position: BOTTOM, ads.banner.height: { gt: 0 } }`:
ads.banner.height is 0, expected gt 0 (after 30s)
Consent had been given and ads were allowed, so I read logcat:
I/Capacitor/Console: Msg: {"height":0}
I/Capacitor/Console: Msg: {"code":"INVALID_REQUEST","message":"An invalid application ID
was provided during initialization, it should be of the form
ca-app-pub-################~##########."}
W/Capacitor/Console: Msg: AdMob: banner failed to load [object Object]
The second line is the answer. The third is what the game's own logging made of it.
Why the app ID broke now and not before
The legacy library reads the app ID from AndroidManifest.xml, from the
com.google.android.gms.ads.APPLICATION_ID meta-data. That is why the Unity build in that
earlier post died at launch with no ID there: the SDK boots itself from the manifest before
any of your code runs.
The Next-Gen SDK takes the app ID as an argument when you start it:
MobileAds.initialize(getContext(), new InitializationConfig.Builder(appId).build());
Where appId comes from is up to whoever calls that. Our wrapper hands it the ID from the
game's JavaScript config. It can also read the manifest at launch, but in every failing run
the ID that reached the SDK was the JS one, because that was the only invalid ID in the app.
The manifest had carried Google's sample app ID all along.
The JS config still held what the game template ships with:
appId: 'ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY',
iosAppId: 'ca-app-pub-XXXXXXXXXXXXXXXX~ZZZZZZZZZZ',
// ...
testMode: true,
I had left it that way on purpose. The template's comment says test mode "handles test-vs-real ads for you — no need to swap ids by hand", and for ad units that is true: test mode swaps in Google's test units. It does nothing to the app ID. With the legacy library that did not matter, since nothing read that field on Android. Under the Next-Gen SDK it is the field.
I never confirmed the legacy build showed ads either. None of the twelve scenarios could have told me, on either build.
The fix, and the tests that hold it
The config now uses Google's sample app IDs, the same ones the manifest and Info.plist
already had:
// Never a placeholder: the Next-Gen SDK starts from this id, not the manifest's.
appId: 'ca-app-pub-3940256099942544~3347511713',
iosAppId: 'ca-app-pub-3940256099942544~1458002511',
When the game gets its own AdMob app, the ID goes into all three places together. A unit test now fails on any app ID that is not in the form the error message asks for, so the old placeholder cannot come back quietly:
for (const id of [admob.appId, admob.iosAppId ?? admob.appId]) {
expect(id, 'the Next-Gen SDK refuses this app id, and serves no ad')
.toMatch(/^ca-app-pub-\d{16}~\d{10}$/);
}
The rebuilt game got past the banner step and failed on the next one:
ads.showInterstitial was never called (after 30s). That was the test's fault, not the
game's. On this emulator a full-screen ad took about 10 seconds to load, and the game, rightly,
does not hold up a screen change waiting for one. I added an adsLoaded field to the bridge
the tests read, and the scenario waits for it:
steps:
- expectCall: { port: ads, method: showBanner, args: [BOTTOM] }
- expectState: { ads.banner.position: BOTTOM, ads.banner.height: { gt: 0 } }
- expectState: { adsLoaded.interstitial: true, adsLoaded.rewarded: true }
- tap: home.levels
- expectScreen: levels
- expectCall: { port: ads, method: showInterstitial, result: true }
- expectEvent: { name: ad_shown, params: { ad_type: interstitial, placement: home_to_levels } }
Scenario 13 failed on the old config and passed on the new one, and all thirteen passed on the final build.
The same bug, three days later
Echo Bat was made from the same game template, placeholder included. When I switched its ads on, every format failed to preload about 0.2 seconds after the SDK started. The log even confirmed test mode was working:
I GoogleMobileAds: GMA(BG) 4: This RequestConfiguration is configured as a test device.
V Capacitor/<ads plugin>: Notifying listeners for event adFailedToPreload
W Capacitor/Console: Msg: AdMob: interstitial failed to load [object Object]
W Capacitor/Console: Msg: AdMob: rewarded failed to load [object Object]
W Capacitor/Console: Msg: AdMob: appOpen failed to load [object Object]
This time I could not find the reason in logcat. I had to attach to the WebView with Chrome's DevTools, listen for the plugin's failure event and retry a preload to see it:
{ "format": "interstitial", "code": "INVALID_REQUEST",
"message": "An invalid application ID was provided during initialization, ..." }
Same error, same fix. On the emulator afterwards an interstitial played, a rewarded video continued a run, and another paid 25 coins (94 to 119). Echo Bat got the same guard as the first game: a test fails if test mode is turned off while the IDs are still placeholders or Google's samples.
The [object Object] cost me more time than the bug did, twice. If your ad error handler
concatenates the error into a string, it is throwing away the one line you need.
How this differs from the Unity post
The Unity post is about the legacy SDK with no app ID
in the manifest. That fails as loudly as Android allows: the app dies before the first frame
with Missing application ID in a fatal exception.
This one is the AdMob GMA Next-Gen SDK with a malformed app ID passed in code. The app
launches, plays, and passes every test that does not look at an ad. The only signal is an
INVALID_REQUEST the game is free to swallow, and the cost is the whole of the ad revenue.
I would take the crash.
What I did not test
Real AdMob IDs, since both games were still on Google's samples. iOS on a device, where this story has no Next-Gen SDK to tell. The two plugins in one build. And any other route into the AdMob Next-Gen SDK: Flutter, Unity or plain native Android. The error string should be the same wherever the app ID is malformed, but I only saw it through Capacitor.
Takeaway
When you move to the AdMob Next-Gen SDK, find where your code calls MobileAds.initialize and
check what app ID it actually passes. The manifest being right no longer proves anything. Then
write one test that asserts an ad is on screen, not that one was asked for. Twelve green
scenarios told me less than the one that asked how tall the banner was.