<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Pradyumn Chaudhary]]></title><description><![CDATA[Articles, tutorials, and notes on software engineering, data structures, and web development by Pradyumn Chaudhary.]]></description><link>https://pradyumnchaudhary.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/680648b9306471af77430b83/e7475adc-579a-4aa9-9715-5b7e225c86ed.jpg</url><title>Pradyumn Chaudhary</title><link>https://pradyumnchaudhary.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 03 Sep 2026 06:48:49 GMT</lastBuildDate><atom:link href="https://pradyumnchaudhary.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Monetizing React Native Apps with Mobile Ads: Advanced Integration, Compliance, & Troubleshooting]]></title><description><![CDATA[Monetizing a React Native app with mobile ads is not as simple as dropping a banner component into a screen. Beyond basic layout code, developers need to handle package updates, regional data privacy ]]></description><link>https://pradyumnchaudhary.hashnode.dev/monetizing-react-native-apps-with-mobile-ads-advanced-integration-compliance-troubleshooting</link><guid isPermaLink="true">https://pradyumnchaudhary.hashnode.dev/monetizing-react-native-apps-with-mobile-ads-advanced-integration-compliance-troubleshooting</guid><category><![CDATA[Reactnative]]></category><category><![CDATA[React Native]]></category><category><![CDATA[ads]]></category><category><![CDATA[Android]]></category><category><![CDATA[Monetization]]></category><category><![CDATA[admob]]></category><dc:creator><![CDATA[Pradyumn Chaudhary]]></dc:creator><pubDate>Wed, 22 Jul 2026 05:15:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/680648b9306471af77430b83/a6cec6ed-0aa4-431c-ab6f-b835271cf7ba.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Monetizing a React Native app with mobile ads is not as simple as dropping a banner component into a screen. Beyond basic layout code, developers need to handle package updates, regional data privacy rules, Play Console audience settings, backend webhook security, and ad auction dynamics.</p>
<p>Here is a practical guide covering the technical decisions, strategy, and common pitfalls involved in integrating Google AdMob into a production React Native app.</p>
<h2>1. Choosing the Right Package</h2>
<p>A common mistake is picking an outdated library from an old tutorial.</p>
<p><strong>The deprecated option — react-native-admob:</strong> This package was abandoned around 2022. It doesn't support modern formats like Native Advanced Ads, doesn't work with Google's updated User Messaging Platform (UMP) SDK v2+, and breaks on the modern iOS App Tracking Transparency (ATT) framework.</p>
<p><strong>The modern standard — react-native-google-mobile-ads:</strong> Actively maintained by Invertase, this package wraps the latest Google-Mobile-Ads-SDK (iOS) and play-services-ads (Android). It includes built-in support for UMP consent management, native banners, interstitials, rewarded ads, and Native Advanced rendering.</p>
<h2>2. Setting Up SDK Initialization at the App Root</h2>
<p>Initializing the Google Mobile Ads SDK takes time, so it shouldn't be triggered separately inside every screen.</p>
<p><strong>Best practice: initialize once, reuse the promise.</strong></p>
<p>Initialize the SDK once at the root level (App.tsx), store the initialization promise, and have other components wait for it before loading ads.</p>
<pre><code class="language-typescript">// services/admob.ts
import mobileAds from 'react-native-google-mobile-ads';

let initPromise: Promise&lt;any&gt; | null = null;

export const initializeAdMob = () =&gt; {
  if (!initPromise) {
    // Returns the existing promise if initialization is already running
    initPromise = mobileAds()
      .initialize()
      .then(adapterStatuses =&gt; {
        console.log('AdMob Initialized:', adapterStatuses);
        return adapterStatuses;
      });
  }
  return initPromise;
};
</code></pre>
<p>Any component that needs an ad should call <code>await initializeAdMob()</code>. If initialization is already done or in progress, this reuses the same promise instead of calling the SDK again.</p>
<h2>3. Development Safety: Test IDs &amp; Test Devices</h2>
<p>Clicking or requesting real ads on your own device during development can trigger Google's fraud detection, leading to ad-serving limits or an account ban. There are two safe ways around this: use AdMob's official test ad units, or register your device as a trusted test device.</p>
<h3>Method 1: Use AdMob's Official Test Ad Unit IDs</h3>
<p>The simplest option — no device registration needed. <code>react-native-google-mobile-ads</code> ships ready-made test IDs for every ad format:</p>
<table>
<thead>
<tr>
<th>Ad Format</th>
<th>Test ID Constant</th>
</tr>
</thead>
<tbody><tr>
<td>Banner</td>
<td><code>TestIds.BANNER</code></td>
</tr>
<tr>
<td>Interstitial</td>
<td><code>TestIds.INTERSTITIAL</code></td>
</tr>
<tr>
<td>Rewarded</td>
<td><code>TestIds.REWARDED</code></td>
</tr>
<tr>
<td>Rewarded Interstitial</td>
<td><code>TestIds.REWARDED_INTERSTITIAL</code></td>
</tr>
<tr>
<td>App Open</td>
<td><code>TestIds.APP_OPEN</code></td>
</tr>
<tr>
<td>Native Advanced</td>
<td><code>TestIds.NATIVE</code></td>
</tr>
</tbody></table>
<p>Use these in place of your real Ad Unit IDs during development — they always return a placeholder ad and carry zero risk to your account.</p>
<h3>Method 2: Register Your Device as a Test Device</h3>
<p>Use this if you want to preview your actual production Ad Unit IDs safely (e.g., to check real formatting or fill behavior).</p>
<p><strong>Step 1 — Find your device's advertising ID:</strong></p>
<ul>
<li><p><strong>Android (GAID):</strong> Go to Settings &gt; Google &gt; Ads and copy your Advertising ID. (If it's all zeros, reset or enable it.)</p>
</li>
<li><p><strong>iOS (IDFA):</strong> Accessing the IDFA requires explicit permission through App Tracking Transparency (ATT). Alternatively, run the app in debug mode — the Google Mobile Ads SDK will print your device's hashed ID in the Xcode/Metro console on startup.</p>
</li>
</ul>
<p><strong>Step 2 — Register it, either from the dashboard or in code:</strong></p>
<ul>
<li><p><strong>Via the AdMob dashboard:</strong></p>
<ol>
<li><p>Go to <a href="https://apps.admob.com">apps.admob.com</a> and sign in.</p>
</li>
<li><p>Open <strong>Settings</strong> (gear icon) in the left sidebar.</p>
</li>
<li><p>Select your app under <strong>Apps</strong>, or go to the general <strong>Account settings</strong>.</p>
</li>
<li><p>Find the <strong>Test devices</strong> section.</p>
</li>
<li><p>Click <strong>Add test device</strong>, paste the hashed device ID from your console log, and save.</p>
</li>
</ol>
</li>
<li><p><strong>Via code:</strong></p>
</li>
</ul>
<pre><code class="language-typescript">import mobileAds from 'react-native-google-mobile-ads';

mobileAds().setRequestConfiguration({
  testDeviceIdentifiers: ['33BE2250B43518CCDA7DE426D04EE232'],
});
</code></pre>
<p>Either method works — once registered, this device always receives test ads instead of live ads, even if you forget to use <code>TestIds</code> somewhere in your code.</p>
<p><strong>Rules for keeping environments separate:</strong></p>
<ul>
<li><p>Always use Google's Test Ad Unit IDs during development.</p>
</li>
<li><p>Never hardcode production Ad Unit IDs in client code without an environment check (<code>__DEV__</code>).</p>
</li>
</ul>
<h2>4. Privacy &amp; Consent (UMP SDK)</h2>
<p>To show ads to a global audience, your app needs to comply with GDPR (Europe) and US state privacy laws.</p>
<pre><code class="language-plaintext">App Root Initialization
         │
         ▼
Check UMP Consent Status
         │
   ┌─────┴─────┐
   ▼           ▼
Granted      Denied
</code></pre>
<p><strong>Personalized vs. non-personalized ads:</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Consent Granted</th>
<th>Consent Denied</th>
</tr>
</thead>
<tbody><tr>
<td>Ad type</td>
<td>Personalized</td>
<td>Non-personalized</td>
</tr>
<tr>
<td>Inventory</td>
<td>Targeted</td>
<td>Contextual only</td>
</tr>
<tr>
<td>Fill rate</td>
<td>Higher</td>
<td>Lower</td>
</tr>
<tr>
<td>eCPM</td>
<td>Higher</td>
<td>Lower</td>
</tr>
</tbody></table>
<ul>
<li><p><strong>Personalized ads</strong> use device identifiers (GAID/IDFA) and browsing behavior to target users, which is why they earn a higher eCPM and have better fill rates.</p>
</li>
<li><p><strong>Non-personalized ads</strong> rely only on context, like app content or general location. If a user declines consent, the app falls back to non-personalized ads — which means lower advertiser demand and lower fill rates.</p>
</li>
</ul>
<p><strong>Testing regional consent forms outside the EEA/UK:</strong></p>
<p><strong>Prerequisite — Create a consent message first:</strong> None of the methods below will show anything unless you've already created a message in the AdMob console. Go to <a href="https://apps.admob.com">apps.admob.com</a> → <strong>Privacy &amp; messaging</strong> → create a GDPR (EEA) and/or US states message for your app. Without this step, the consent form simply won't appear, no matter which testing method you use.</p>
<p><strong>Method 1 — SDK Debug Geography (recommended):</strong> Use <code>debugGeography</code> in development to make your device simulate an EEA or US user, regardless of where you actually are.</p>
<ul>
<li><em>If you already have a registered test device ID</em> (see Section 3): plug it straight in —</li>
</ul>
<pre><code class="language-typescript">import { AdsConsent, AdsConsentDebugGeography } from 'react-native-google-mobile-ads';

if (__DEV__) {
  await AdsConsent.requestInfoUpdate({
    debugGeography: AdsConsentDebugGeography.EEA,
    testDeviceIdentifiers: ['YOUR_HASHED_DEVICE_ID'],
  });
}
</code></pre>
<ul>
<li><em>If you don't have one yet:</em> run the app once in debug mode, copy the hashed device ID printed in your Xcode/Metro console, and paste it into <code>testDeviceIdentifiers</code> above.</li>
</ul>
<p><strong>Method 2 — Temporary Dashboard Override:</strong> In the AdMob console, under Privacy &amp; Messaging, you can set geographic targeting to "Everywhere" for quick testing — just remember to revert it to your actual target regions before releasing to production.</p>
<p><strong>Method 3 — Commercial VPNs (not recommended):</strong> You might be tempted to use a VPN to appear as an EEA/US user, but this usually doesn't work: Google's UMP endpoints can detect data-center IP addresses used by commercial VPNs, so they often fail to trigger the regional consent dialog at all. Stick to Methods 1 or 2 instead.</p>
<p><strong>Letting users change their mind:</strong> GDPR requires that users can revoke or update consent at any time. Add a "Manage Privacy Settings" button in your app's settings screen that calls <code>AdsConsent.showConsentForm()</code>.</p>
<h2>5. Play Console Setup &amp; Audience Matching</h2>
<p>AdMob requires your app's declared content rating to match your ad content settings.</p>
<p><strong>Key rules:</strong></p>
<ul>
<li><p>Under Play Console &gt; Policy &gt; App Content, declare "Yes, my app contains ads."</p>
</li>
<li><p>If your app targets children or is rated 3+/Everyone, your AdMob "Max Ad Content Rating" must match (e.g., G or PG). Serving mature (17+) ads in an app aimed at younger users can get your app removed from the Play Store.</p>
</li>
</ul>
<p>You can set the Max Ad Content Rating either from the AdMob dashboard or in code — the dashboard setting acts as your account-wide default, while the code setting lets you enforce it explicitly at runtime.</p>
<p><strong>Method 1 — Via the AdMob dashboard:</strong></p>
<ol>
<li><p>Go to <a href="https://apps.admob.com">apps.admob.com</a> and sign in.</p>
</li>
<li><p>Select your app, then open <strong>Blocking controls</strong>.</p>
</li>
<li><p>Under <strong>Content rating</strong>, choose the <strong>Max Ad Content Rating</strong> that matches your app's audience (e.g., G, PG, T, or MA).</p>
</li>
<li><p>Save your changes.</p>
</li>
</ol>
<p><strong>Method 2 — Via code:</strong></p>
<pre><code class="language-typescript">import mobileAds, { MaxAdContentRating } from 'react-native-google-mobile-ads';

await mobileAds().setRequestConfiguration({
  maxAdContentRating: MaxAdContentRating.G,
  tagForChildDirectedTreatment: false,
});
</code></pre>
<h2>6. Comparing Ad Formats (and the "No Fill" Trap)</h2>
<table>
<thead>
<tr>
<th>Ad Format</th>
<th>Visual Integration</th>
<th>User Engagement</th>
<th>Average eCPM</th>
<th>Fill Rate</th>
</tr>
</thead>
<tbody><tr>
<td>Banner</td>
<td>Standard top/bottom rectangle</td>
<td>Low</td>
<td>Lower</td>
<td>Very High</td>
</tr>
<tr>
<td>Interstitial</td>
<td>Full-screen, shown at natural transitions</td>
<td>Moderate</td>
<td>Higher</td>
<td>High</td>
</tr>
<tr>
<td>Rewarded</td>
<td>Full-screen video, user opts in for a reward</td>
<td>High</td>
<td>Highest</td>
<td>Moderate</td>
</tr>
<tr>
<td>Rewarded Interstitial</td>
<td>Full-screen video, shown at natural breaks</td>
<td>Moderate/High</td>
<td>High</td>
<td>Moderate</td>
</tr>
<tr>
<td>App Open</td>
<td>Full-screen, shown on app launch/foreground</td>
<td>Moderate</td>
<td>Moderate</td>
<td>High</td>
</tr>
<tr>
<td>Native Advanced</td>
<td>Custom layout matching your app's UI</td>
<td>High</td>
<td>Higher</td>
<td>Moderate/Lower</td>
</tr>
</tbody></table>
<p><strong>Why Native Advanced ads sometimes don't show up:</strong></p>
<p>Native Advanced ads return raw assets — headline, icon, call-to-action text, media — which your app renders using its own layout components. Since advertisers need to supply custom assets for these slots, demand is lower than for standard banners.</p>
<p>If a user opts out of personalized tracking, the pool of available Native Advanced ads shrinks further, which often leads to "No Fill" errors (Error Code 3).</p>
<p><strong>Never spam AdMob with retry requests:</strong> If an ad fails to load, don't create an infinite retry loop or repeatedly call <code>.load()</code> — this triggers rate-limiting from AdMob's servers. Instead, use exponential backoff (e.g., retry after 30s, 60s, 2m, up to 3 attempts), or fall back gracefully to a standard banner placement.</p>
<h2>7. Account Approval, Tax Forms, &amp; Propagation Delays</h2>
<p>If your live ads keep showing Error Code 3 (No Fill), check these account settings:</p>
<ul>
<li><p><strong>App approval:</strong> AdMob manually reviews new app links. Your app must be published on the store and linked in the AdMob console.</p>
</li>
<li><p><strong>US tax information (required for everyone):</strong> Regardless of where you operate, you must submit US tax info (W-8BEN for non-US entities, W-9 for US entities) in the AdMob payments tab before Google will serve live ads.</p>
</li>
<li><p><strong>Ad unit propagation time:</strong> New Ad Unit IDs aren't active immediately — it usually takes 2 to 24 hours to propagate across ad servers.</p>
</li>
</ul>
<h2>8. Rewarded Ads: Server-Side Verification &amp; Cloudflare Webhooks</h2>
<p>When rewarding users for watching a video ad (e.g., in-app currency), relying on a client-side callback is risky since it can be reverse-engineered locally. Server-Side Verification (SSV) sends a signed webhook directly from Google to your backend to confirm the reward is legitimate.</p>
<pre><code class="language-plaintext">┌──────────────┐  1. Completes video   ┌──────────────┐
│  Client App  ├──────────────────────►│  AdMob Server │
└──────────────┘                       └──────┬───────┘
                                               │ 2. Signed webhook
                                               ▼
┌──────────────┐  3. Pass/fail reward  ┌───────────────┐
│ Backend App  │◄───────────────────────┤ Cloudflare WAF│
└──────────────┘                        └───────────────┘
</code></pre>
<p><strong>The Cloudflare WAF / Bot Fight Mode issue:</strong> Google's SSV webhooks don't run JavaScript or solve CAPTCHAs. If your backend is behind Cloudflare with Bot Fight Mode on, Cloudflare will block Google's webhook with a 403 error.</p>
<p><strong>How to fix it:</strong></p>
<ul>
<li><p><strong>Option A — Add a WAF skip rule:</strong> Create a rule in Cloudflare to bypass security checks for the SSV route:</p>
<ul>
<li><p>Field: URI Path</p>
</li>
<li><p>Operator: equals</p>
</li>
<li><p>Value: <code>/api/webhooks/admob-ssv</code></p>
</li>
<li><p>Action: Skip → All Managed Rules &amp; Bot Fight Mode</p>
</li>
</ul>
</li>
<li><p><strong>Option B — Disable Bot Fight Mode globally</strong>, if your Cloudflare plan doesn't support granular rules.</p>
</li>
</ul>
<h2>9. How Ad Monetization Actually Works: Bidding vs. Waterfall</h2>
<p><strong>Key terms:</strong></p>
<ul>
<li><p><strong>CPM (Cost Per Mille):</strong> Revenue per 1,000 ad impressions.</p>
</li>
<li><p><strong>CPC (Cost Per Click):</strong> Revenue each time a user clicks an ad.</p>
</li>
<li><p><strong>eCPM (Effective CPM):</strong> Total earnings ÷ total impressions × 1,000 — the real measure of revenue performance across ad types.</p>
</li>
<li><p><strong>Fill Rate:</strong> The percentage of ad requests that successfully return an ad.</p>
</li>
</ul>
<p><strong>Waterfall mediation vs. real-time bidding:</strong></p>
<pre><code class="language-plaintext">TRADITIONAL WATERFALL MEDIATION
(networks called one at a time, in a fixed order)

1. Call Network A ($10 avg. eCPM)  →  No Fill
2. Call Network B ($5 avg. eCPM)   →  Success — ad served
3. Call Network C ($2 avg. eCPM)   →  Never called
</code></pre>
<pre><code class="language-plaintext">REAL-TIME BIDDING (IN-APP AUCTION)
(all networks called at once, highest bid wins)

Network A  →  bids $4.50
Network B  →  bids $8.20   ← Winner, serves the ad
Network C  →  bids $2.10
</code></pre>
<ul>
<li><p><strong>Default network (Google demand):</strong> Out of the box, AdMob routes requests through the Google Ad Manager/Google Ads network.</p>
</li>
<li><p><strong>Waterfall mediation:</strong> Calls ad networks one by one, in a fixed order based on historical average eCPM. This is slower and can miss better real-time offers from lower-ranked networks.</p>
</li>
<li><p><strong>Real-time bidding:</strong> Calls all participating networks (e.g., AppLovin, Unity Ads, Meta Audience Network) at the same time in a single auction. The highest bidder wins instantly, maximizing your revenue.</p>
</li>
</ul>
<h2>10. When to Scale: The ~1,000 Clicks Rule</h2>
<p>Adding complex ad mediation too early creates extra maintenance for little financial benefit.</p>
<ul>
<li><p><strong>Early stage (fewer than 1,000 daily ad clicks):</strong> Stick with AdMob + UMP SDK. Managing multiple third-party adapters, build configs, and payout minimums isn't worth it at low traffic.</p>
</li>
<li><p><strong>Growth stage (more than 1,000 daily ad clicks):</strong> Once you hit roughly 1,000 daily ad clicks, set up AdMob Mediation Groups with real-time bidding to increase competition for your ad slots and boost overall eCPM.</p>
</li>
</ul>
<h2>Final Tech Stack Checklist</h2>
<ul>
<li><p>[ ] Core library: react-native-google-mobile-ads</p>
</li>
<li><p>[ ] Initialization: App-root singleton promise pattern (App.tsx)</p>
</li>
<li><p>[ ] Privacy SDK: Google UMP SDK, with in-app consent revocation</p>
</li>
<li><p>[ ] Server security: Rewarded Ads SSV with Cloudflare WAF skip rules</p>
</li>
<li><p>[ ] Store policy: Play Console "Contains Ads" checked, target audience matched</p>
</li>
<li><p>[ ] Scaling strategy: Standalone AdMob setup, migrating to real-time bidding at ~1,000 daily ad clicks</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>