What happened?
On a physical iPhone development build, RewardedAdEventType.EARNED_REWARD and AdEventType.CLOSED are never delivered to JavaScript for a rewarded ad, even though Google's own native test-mode UI visually confirms the reward was granted ("Reward granted" banner), and even in a controlled reproduction where the app never leaves the foreground and never backgrounds. AdEventType.LOADED, RewardedAdEventType.LOADED, and all interstitial ad events (loaded, shown, dismissed) work correctly on the same build/device using the same wrapper library, so the native↔JS ad-event bridge is not broken in general — the failure appears specific to the rewarded ad's post-load event delivery.
Reproduction (foreground-only, no backgrounding)
- Load and show a rewarded test ad (
ca-app-pub-3940256099942544/1712485313, Google's official iOS rewarded test unit) via RewardedAd.createForAdRequest + addAdEventListener.
- Watch the ad play. Google's native test-mode UI shows the "Reward granted" confirmation banner.
- Do not interact with the end card, do not background the app, do not lock the screen.
- Wait 15+ seconds while fully foregrounded.
Expected: RewardedAdEventType.EARNED_REWARD fires in JS at or shortly after step 2.
Actual: No event of any kind fires after LOADED. Custom diagnostic logging placed inside every addAdEventListener callback confirms zero JS invocations for EARNED_REWARD, CLOSED, or ERROR for the remainder of the session — the app was left running for several minutes with no further events. Sequence (trimmed to relevant lines):
attempt_start (currentAppState: active)
rewarded_ad_loaded
rewarded_ad_started
... app remains foregrounded, no AppState transitions occur ...
(no earned_reward_event_received)
(no closed_event_received)
(no finish_called)
There is also a second reproduction where the end card includes a "GET" button linking to an App-Store house ad, tapping which backgrounds the app; the same non-delivery occurs there too. The foreground-only reproduction above shows backgrounding is not a required factor — it happens even when the app never leaves the foreground at all.
Minimal relevant code
const rewarded = RewardedAd.createForAdRequest(adUnitId);
const unsubscribeLoaded = rewarded.addAdEventListener(RewardedAdEventType.LOADED, () => {
rewarded.show(); // fires reliably
});
const unsubscribeEarned = rewarded.addAdEventListener(
RewardedAdEventType.EARNED_REWARD,
() => { /* never invoked */ },
);
const unsubscribeClosed = rewarded.addAdEventListener(AdEventType.CLOSED, () => {
/* never invoked */
});
rewarded.load();
A fresh RewardedAd instance is created per attempt; all listeners are attached before .load() is called; .show() is only called from inside the LOADED handler, on the same instance whose listeners were just registered.
Source-level investigation already done
Before filing, I traced the library internals to rule out a usage mistake on our side:
addAdEventListener (per-type) and addAdEventsListener (all-events, what useRewardedAd()/useFullScreenAd() uses internally) are both driven by the same single native listener registered in MobileAd's constructor (SharedEventEmitter.addListener('google_mobile_ads_${type}_event:${adUnitId}:${requestId}', this._handleAdEvent.bind(this))), and both are dispatched from the same _handleAdEvent call. So this isn't caused by choosing the per-type listener over the hook/all-events listener — both share the identical delivery path, and useRewardedAd() would be expected to exhibit the same failure.
- On the iOS native side (
RNGoogleMobileAdsFullScreenAd.mm), EARNED_REWARD originates from GADRewardedAd's presentFromRootViewController:userDidEarnRewardHandler: closure, and CLOSED originates from RNGoogleMobileAdsFullScreenContentDelegate's adDidDismissFullScreenContent: (the standard GADFullScreenContentDelegate method).
- Interstitial ads use the same
RNGoogleMobileAdsFullScreenContentDelegate class for their own dismiss/CLOSED event, and that works correctly on this exact build/device (interstitial_ad_dismissed fires reliably). This suggests the wrapper's delegate assignment/dispatch machinery itself is sound, and the difference is specific to whether the native GADRewardedAd object ever calls back into either of its two completion mechanisms for this ad session.
This is offered as diagnostic evidence, not proof that the native SDK itself is at fault — it's equally possible something in how this specific test/house creative's end card interacts with the SDK's completion state is the trigger (see questions below).
Similar existing issue
#500 ("Rewarded callback not working") describes the same symptom class — reward visually granted, callback not firing — but on Android with an older version (12.4.0). Referencing as related context only, not as the same confirmed root cause; that issue was closed as "not planned" with no resolution documented.
Questions for maintainers
- Is there a known issue on iOS with rewarded ads under Google Mobile Ads SDK 13.5.0 where
userDidEarnRewardHandler and the full-screen dismissal delegate are never invoked for a specific ad session?
- Are there known rewarded test creatives/end-card states (e.g. app-install house ads) that can visually display "Reward granted" without the SDK actually completing/delivering that callback?
- Is there any recommended additional native configuration for Google Mobile Ads iOS SDK 13.5.0 relevant to this?
- Is there a newer native SDK version (independent of this wrapper's JS-level releases, which as of 16.5.0 still bundle 13.5.0) known to address this?
- Is there any native-side instrumentation/logging you'd recommend to confirm whether Google's native callback fires at all before the wrapper emits its own event — i.e., is the loss happening in Google's SDK, in the wrapper's Objective-C++ layer, or in the RN bridge?
- Would a minimal standalone reproduction repository be useful, and if so is there a preferred minimal-repro template for this library?
Platforms
Only on iOS
React Native Info
This is an Expo-managed project (expo 57.0.13, EAS Build for the native binary), so `react-native info` isn't directly applicable — Expo-equivalent info below.
expo-cli: 57.0.15
react-native: 0.86.2
node: v22.14.0
npm: 10.9.2
macOS (build machine): 15.4.1 (24E263)
iOS (test device): 26.6 (23G71)
Build: EAS Build (cloud), development profile, internal distribution
Are your using Typescript?
package.json
{
"dependencies": {
"@expo/metro-runtime": "~57.0.10",
"@react-native-async-storage/async-storage": "2.2.0",
"@shopify/react-native-skia": "2.6.2",
"babel-preset-expo": "~57.0.0",
"expo": "~57.0.13",
"expo-asset": "~57.0.9",
"expo-audio": "~57.0.3",
"expo-constants": "~57.0.8",
"expo-dev-client": "~57.0.12",
"expo-haptics": "~57.0.1",
"expo-linking": "~57.0.6",
"expo-router": "~57.0.13",
"expo-status-bar": "~57.0.1",
"expo-tracking-transparency": "~57.0.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.2",
"react-native-gesture-handler": "~2.32.0",
"react-native-google-mobile-ads": "^16.4.0",
"react-native-purchases": "^10.7.0",
"react-native-reanimated": "4.5.1",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0",
"react-native-web": "^0.21.2",
"react-native-worklets": "0.10.1"
}
}
app.json
{
"expo": {
"name": "REDACTED",
"version": "1.0.0",
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.example.app"
},
"plugins": [
"expo-router",
["expo-audio", { "microphonePermission": false, "recordAudioAndroid": false }],
"expo-asset",
["react-native-google-mobile-ads", { "iosAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX", "delayAppMeasurementInit": true }],
["expo-tracking-transparency", { "userTrackingPermission": "This identifier is used to show you more relevant ads and measure ad performance. You can change this anytime in Settings." }]
]
}
}
(RevenueCat keys, AdMob production ad-unit IDs, bundle identifier, and EAS project linkage redacted/placeholder'd — not relevant to this bug.)
ios/Podfile
platform :ios, podfile_properties['ios.deploymentTarget'] || '16.4'
target 'UltimatePingPong' do
use_expo_modules!
config = use_native_modules!(config_command)
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true,
)
# react-native-google-mobile-ads autolinked via Expo modules; no manual pod pin.
# Resolves to Google-Mobile-Ads-SDK 13.5.0 / GoogleUserMessagingPlatform 3.1.0
# per react-native-google-mobile-ads@16.4.0's own podspec (sdkVersions.ios).
end
android/build.gradle
android/app/build.gradle
android/settings.gradle
AndroidManifest.xml
What happened?
On a physical iPhone development build,
RewardedAdEventType.EARNED_REWARDandAdEventType.CLOSEDare never delivered to JavaScript for a rewarded ad, even though Google's own native test-mode UI visually confirms the reward was granted ("Reward granted" banner), and even in a controlled reproduction where the app never leaves the foreground and never backgrounds.AdEventType.LOADED,RewardedAdEventType.LOADED, and all interstitial ad events (loaded,shown,dismissed) work correctly on the same build/device using the same wrapper library, so the native↔JS ad-event bridge is not broken in general — the failure appears specific to the rewarded ad's post-load event delivery.Reproduction (foreground-only, no backgrounding)
ca-app-pub-3940256099942544/1712485313, Google's official iOS rewarded test unit) viaRewardedAd.createForAdRequest+addAdEventListener.Expected:
RewardedAdEventType.EARNED_REWARDfires in JS at or shortly after step 2.Actual: No event of any kind fires after
LOADED. Custom diagnostic logging placed inside everyaddAdEventListenercallback confirms zero JS invocations forEARNED_REWARD,CLOSED, orERRORfor the remainder of the session — the app was left running for several minutes with no further events. Sequence (trimmed to relevant lines):There is also a second reproduction where the end card includes a "GET" button linking to an App-Store house ad, tapping which backgrounds the app; the same non-delivery occurs there too. The foreground-only reproduction above shows backgrounding is not a required factor — it happens even when the app never leaves the foreground at all.
Minimal relevant code
A fresh
RewardedAdinstance is created per attempt; all listeners are attached before.load()is called;.show()is only called from inside theLOADEDhandler, on the same instance whose listeners were just registered.Source-level investigation already done
Before filing, I traced the library internals to rule out a usage mistake on our side:
addAdEventListener(per-type) andaddAdEventsListener(all-events, whatuseRewardedAd()/useFullScreenAd()uses internally) are both driven by the same single native listener registered inMobileAd's constructor (SharedEventEmitter.addListener('google_mobile_ads_${type}_event:${adUnitId}:${requestId}', this._handleAdEvent.bind(this))), and both are dispatched from the same_handleAdEventcall. So this isn't caused by choosing the per-type listener over the hook/all-events listener — both share the identical delivery path, anduseRewardedAd()would be expected to exhibit the same failure.RNGoogleMobileAdsFullScreenAd.mm),EARNED_REWARDoriginates fromGADRewardedAd'spresentFromRootViewController:userDidEarnRewardHandler:closure, andCLOSEDoriginates fromRNGoogleMobileAdsFullScreenContentDelegate'sadDidDismissFullScreenContent:(the standardGADFullScreenContentDelegatemethod).RNGoogleMobileAdsFullScreenContentDelegateclass for their own dismiss/CLOSEDevent, and that works correctly on this exact build/device (interstitial_ad_dismissedfires reliably). This suggests the wrapper's delegate assignment/dispatch machinery itself is sound, and the difference is specific to whether the nativeGADRewardedAdobject ever calls back into either of its two completion mechanisms for this ad session.This is offered as diagnostic evidence, not proof that the native SDK itself is at fault — it's equally possible something in how this specific test/house creative's end card interacts with the SDK's completion state is the trigger (see questions below).
Similar existing issue
#500 ("Rewarded callback not working") describes the same symptom class — reward visually granted, callback not firing — but on Android with an older version (12.4.0). Referencing as related context only, not as the same confirmed root cause; that issue was closed as "not planned" with no resolution documented.
Questions for maintainers
userDidEarnRewardHandlerand the full-screen dismissal delegate are never invoked for a specific ad session?Platforms
Only on iOS
React Native Info
Are your using Typescript?
package.json
{ "dependencies": { "@expo/metro-runtime": "~57.0.10", "@react-native-async-storage/async-storage": "2.2.0", "@shopify/react-native-skia": "2.6.2", "babel-preset-expo": "~57.0.0", "expo": "~57.0.13", "expo-asset": "~57.0.9", "expo-audio": "~57.0.3", "expo-constants": "~57.0.8", "expo-dev-client": "~57.0.12", "expo-haptics": "~57.0.1", "expo-linking": "~57.0.6", "expo-router": "~57.0.13", "expo-status-bar": "~57.0.1", "expo-tracking-transparency": "~57.0.1", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.86.2", "react-native-gesture-handler": "~2.32.0", "react-native-google-mobile-ads": "^16.4.0", "react-native-purchases": "^10.7.0", "react-native-reanimated": "4.5.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.26.0", "react-native-web": "^0.21.2", "react-native-worklets": "0.10.1" } }app.json
{ "expo": { "name": "REDACTED", "version": "1.0.0", "ios": { "supportsTablet": true, "bundleIdentifier": "com.example.app" }, "plugins": [ "expo-router", ["expo-audio", { "microphonePermission": false, "recordAudioAndroid": false }], "expo-asset", ["react-native-google-mobile-ads", { "iosAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX", "delayAppMeasurementInit": true }], ["expo-tracking-transparency", { "userTrackingPermission": "This identifier is used to show you more relevant ads and measure ad performance. You can change this anytime in Settings." }] ] } } (RevenueCat keys, AdMob production ad-unit IDs, bundle identifier, and EAS project linkage redacted/placeholder'd — not relevant to this bug.)ios/Podfile
android/build.gradle
android/app/build.gradle
android/settings.gradle
AndroidManifest.xml