●MAX — Rork Max handles code signing and provisioning so you can submit to the App Store in two clicks, with no local Mac required●SIMULATOR — Try your app in an in-browser streaming simulator, then scan a QR code to install straight to your device without TestFlight●NATIVE — Coverage reaches SwiftUI, ARKit, HealthKit, HomeKit, Core ML, and Metal, going where React Native cannot●SEED — Rork raised a $15M seed led by Left Lane Capital in April 2026, joined by Peak XV and a16z Speedrun●GROWTH — Reported at 743,000 monthly visits with 85% growth, widening access to AI-built mobile apps●NOCODE — Gartner expects 75% of new applications to be built with low-code or no-code tools by the end of 2026●MAX — Rork Max handles code signing and provisioning so you can submit to the App Store in two clicks, with no local Mac required●SIMULATOR — Try your app in an in-browser streaming simulator, then scan a QR code to install straight to your device without TestFlight●NATIVE — Coverage reaches SwiftUI, ARKit, HealthKit, HomeKit, Core ML, and Metal, going where React Native cannot●SEED — Rork raised a $15M seed led by Left Lane Capital in April 2026, joined by Peak XV and a16z Speedrun●GROWTH — Reported at 743,000 monthly visits with 85% growth, widening access to AI-built mobile apps●NOCODE — Gartner expects 75% of new applications to be built with low-code or no-code tools by the end of 2026
Rork × AdMob UMP × ATT Consent Management for Production — Keeping Revenue Alive in Global Releases
A production playbook for wiring UMP and ATT into your Rork app so you stay GDPR-compliant while protecting AdMob revenue, with working code throughout.
If you run a personal Rork app that earns a few hundred dollars a month from AdMob, one morning you may notice that the revenue coming from the EEA (European Economic Area) has quietly cratered. I've been there myself. The first time I shipped a Rork app globally without thinking hard about consent, I got a polite-but-alarming email from AdMob asking me to fix my GDPR handling. That email leaves a mark.
Dig into the cause and you almost always find gaps in UMP (User Messaging Platform) or ATT (App Tracking Transparency). Rork is a genuinely wonderful tool — it generates UI, auth, data flow — but consent management around monetization is still something you need to wire up by hand today.
This guide walks through wiring UMP and ATT into a Rork-generated app at production quality, so that three things hold at the same time: GDPR compliance, AdMob policy compliance, and no surprise drop in revenue. Every pattern below is backed by working code.
Before diving in, one framing note. I approach consent management not as "a compliance checkbox to survive" but as a design surface that shapes how much users trust your app. A form written in dense legal English, stuffed onto a cramped screen, is compliant but silently depresses your consent rate and your brand in one go. A form that explains what you're asking for, in your user's language, consistently performs better on both axes. That's the spirit behind the patterns below.
Why Rork apps have a built-in blind spot around consent
The AdMob integration that Rork typically scaffolds calls AdMob.initializeAsync() and starts requesting ads. It works. Testing looks fine. That's the trap — the simple path quietly bypasses three layers that matter in production.
ATT (iOS 14.5+): Explicit user permission is required before you can access the IDFA.
UMP (EEA / UK / Switzerland): Under GDPR, you must show a consent dialog before serving personalized ads.
Initialization order: If you initialize the AdMob SDK before you have consent information, your ad requests get locked into non-personalized (NPA) mode for that session, and CPM typically drops 20–30%.
Revenue from EEA users is often 30–50% of an app's total, so letting this blind spot sit can cost real money every month. For an indie developer, that's not an abstract compliance concern — it's rent.
The big picture: three consent layers, one correct order
Here's the conclusion up front. The initialization sequence that keeps you compliant and keeps CPM healthy is:
On app launch, call UMP's requestConsentInfoUpdate to fetch the consent status.
If needed, present the UMP consent form with loadAndShowConsentFormIfRequired.
On iOS only, show the ATT prompt (requestTrackingAuthorization).
Propagate consent to AdMob, Firebase Analytics, RevenueCat, and any other SDK that listens.
Call AdMob.initializeAsync() and start requesting ads.
Break this ordering in a single spot and the first session's ad requests fire in NPA mode. Rork's default scaffolding often places a login screen between steps 3 and 4 — if you initialize AdMob after login, your freshly installed users are locked into NPA for their entire first session. That's a silent revenue leak.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can recover the AdMob revenue in the EEA that quietly dropped after GDPR took effect, by wiring in UMP the right way
✦You'll get production-ready code covering ATT and UMP initialization order, regional detection, and the consent-revocation UI
✦You'll learn the patterns that keep a global Rork app compliant before an AdMob Publisher Policy warning ever lands in your inbox
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Step 1: Installing UMP and wiring the initialization
The smoothest path for Rork is to use the UMP implementation that ships with react-native-google-mobile-ads, since Rork runs on Expo/React Native under the hood.
# Install AdMob (includes UMP)npm install react-native-google-mobile-ads# iOS also needs podscd ios && pod install && cd ..
Update app.json (or app.config.ts) with your AdMob app IDs. Just add these to the Rork-generated config.
{ "expo": { "plugins": [ [ "react-native-google-mobile-ads", { "androidAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY", "iosAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~ZZZZZZZZZZ", "userTrackingUsageDescription": "We use identifiers to personalize ads and improve the app experience for you." } ] ] }}
Now the actual consent initializer. The code below is derived from what I run in production — it includes proper error handling, which many tutorials skip.
// src/consent/initializeConsent.tsimport mobileAds, { AdsConsent, AdsConsentStatus, AdsConsentDebugGeography,} from 'react-native-google-mobile-ads';export type ConsentResult = { canRequestAds: boolean; consentStatus: AdsConsentStatus; isGDPRRegion: boolean; error?: string;};export async function initializeConsent(): Promise<ConsentResult> { try { // Step 1: request consent info. // In __DEV__ we pretend to be an EEA user so we can test the full flow. const consentInfo = await AdsConsent.requestInfoUpdate( __DEV__ ? { debugGeography: AdsConsentDebugGeography.EEA, testDeviceIdentifiers: ['YOUR_TEST_DEVICE_ID'], } : undefined, ); // Step 2: present the consent form if required. if ( consentInfo.isConsentFormAvailable && consentInfo.status === AdsConsentStatus.REQUIRED ) { await AdsConsent.loadAndShowConsentFormIfRequired(); } // Step 3: determine whether we can request personalized ads. const canRequestAds = await AdsConsent.getConsentInfo().then( (info) => info.canRequestAds ?? false, ); return { canRequestAds, consentStatus: consentInfo.status, isGDPRRegion: consentInfo.status === AdsConsentStatus.REQUIRED || consentInfo.status === AdsConsentStatus.OBTAINED, }; } catch (error) { // On failure (e.g. offline at launch) we fail closed by returning // canRequestAds: false. Swallowing the error and serving ads anyway // risks a GDPR breach. Retry on the next launch. console.error('[Consent] Failed to initialize:', error); return { canRequestAds: false, consentStatus: AdsConsentStatus.UNKNOWN, isGDPRRegion: false, error: error instanceof Error ? error.message : 'Unknown error', }; }}
The important decision here is failing closed on error. If consent retrieval fails mid-flight (bad connection, Google outage, corrupt cache) and you respond by serving ads anyway, you've likely just served non-consented personalized ads. Waiting for the next launch is the safer tradeoff.
Expected behavior:
EEA user on first launch → consent form shows, pick "Consent" → canRequestAds: true
Non-EEA user (JP, US…) → no form, canRequestAds: true immediately
Offline at launch → canRequestAds: false, ad requests skipped
Anatomy of a UMP consent form — what's actually happening
It's worth spending a moment to understand what loadAndShowConsentFormIfRequired puts in front of the user. Google pre-builds this form from the settings in your AdMob Privacy & Messaging configuration, so the specific wording you see on the device is driven by choices you made in the console — not by anything in your React Native code.
The form itself is backed by an IAB TCF v2.2 "Transparency and Consent" string. When the user accepts or rejects, UMP writes a consent string to NSUserDefaults (iOS) or SharedPreferences (Android) under a IABTCF_* namespace. AdMob then reads those keys on every ad request. That's why the ordering matters so much: if AdMob reads the keys before UMP has written them, AdMob defaults to NPA — there's no "retry later and personalize" semantics once a request has gone out.
Two practical implications follow. First, once the form has been answered, the keys stay on disk across app launches. You don't need to re-prompt; UMP will just read the cached state and move on. Second, any hand-rolled "clear app data" flow you ship (for example, a "reset everything" button in settings) needs to either preserve these keys or call AdsConsent.reset() afterward, otherwise the next launch treats the user as fresh and re-prompts, which is legal but annoying.
You can inspect the TCF string on a debug device if you ever need to confirm what's been stored. On iOS, read IABTCF_TCString from UserDefaults.standard; on Android, read the same key from the default SharedPreferences. Pasting that string into an online TCF decoder is the fastest way to see exactly which purposes and vendors the user has (or hasn't) consented to.
Step 2: Layering ATT on top of UMP
On iOS you also need ATT permission. The important detail is the order: UMP first, then ATT.
The reasoning is subtle. UMP's consent form asks about personalized advertising. If the user declines personalization in UMP, there's no reason to bother them with ATT at all — the IDFA won't help you anyway. If you show ATT first, you can end up in the awkward state of "the user granted tracking, but UMP still says no personalization," which is technically consistent but feels contradictory to the user.
// src/consent/requestATT.tsimport { Platform } from 'react-native';import { request, PERMISSIONS, RESULTS, check,} from 'react-native-permissions';export async function requestATTIfNeeded(): Promise<boolean> { if (Platform.OS !== 'ios') { return true; // Android doesn't have ATT. } // iOS < 14.5 doesn't have the API at all. const iosVersion = parseFloat(Platform.Version as string); if (iosVersion < 14.5) { return true; } try { // Check before requesting — don't re-prompt. const currentStatus = await check(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY); if (currentStatus === RESULTS.GRANTED) return true; if (currentStatus === RESULTS.DENIED) return false; if (currentStatus === RESULTS.BLOCKED) return false; // Undetermined — prompt. const result = await request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY); return result === RESULTS.GRANTED; } catch (error) { console.error('[ATT] Request failed:', error); return false; }}
Once the user answers ATT, only the system Settings app can reverse the choice — additional request calls won't re-prompt. Calling check first is cheap and keeps you from issuing pointless API calls.
A note on the ATT dialog text
The string you pass to userTrackingUsageDescription in app.json is the only place you get to explain, in your own words, why tracking matters for your app. Apple shows this as a single secondary line below the system dialog title. The title itself is fixed ("Allow "MyApp" to track your activity across other companies' apps and websites?"), so the subtitle is your entire UX surface area.
My rule of thumb: describe the concrete benefit to the user, not the business benefit to you. "We use identifiers to personalize ads and improve the app experience for you" lands better than "We need this to serve relevant ads." It's a small thing, but I've seen opt-in rate swings of a few percentage points based on the wording alone — which, compounded across a million impressions, is meaningful revenue.
One more gotcha: this string is localized through the usual .lproj / InfoPlist.strings mechanism. If you support Japanese and English in your Rork app, the Japanese ATT dialog will fall back to English unless you provide a localized override, which many teams forget.
Step 3: Region-aware branching without rolling your own geo-IP
UMP internally decides whether the user is in a GDPR jurisdiction, so in most cases you don't need your own geo-IP service. If you want to gate a piece of UI on "is this likely an EEA user," you can use UMP's consentStatus as a proxy.
// src/consent/regionAware.tsimport { AdsConsent, AdsConsentStatus } from 'react-native-google-mobile-ads';export async function isLikelyGDPRRegion(): Promise<boolean> { const info = await AdsConsent.getConsentInfo(); // REQUIRED = consent is required (EEA/UK/Switzerland) // OBTAINED = required AND already answered // NOT_REQUIRED = outside GDPR return ( info.status === AdsConsentStatus.REQUIRED || info.status === AdsConsentStatus.OBTAINED );}
Building your own IP lookup sounds easy until you hit VPN users, travelers, carrier NAT, stale databases, and the ongoing cost of maintaining it. Google keeps UMP's region map current, so delegating is the boring-correct choice.
Step 4: Fan the consent out to other SDKs
Having consent info isn't useful unless the downstream SDKs actually honor it. react-native-google-mobile-ads handles UMP internally, but Firebase Analytics and RevenueCat need explicit configuration.
// src/consent/propagateConsent.tsimport analytics from '@react-native-firebase/analytics';import Purchases from 'react-native-purchases';import { AdsConsent } from 'react-native-google-mobile-ads';export async function propagateConsent(): Promise<void> { const consentInfo = await AdsConsent.getConsentInfo(); const canTrack = consentInfo.canRequestAds ?? false; // Firebase Analytics consent mode. // Without this, some background events can still fire in older SDKs // even when collection is "disabled." await analytics().setAnalyticsCollectionEnabled(canTrack); await analytics().setConsent({ ad_storage: canTrack ? 'granted' : 'denied', ad_user_data: canTrack ? 'granted' : 'denied', ad_personalization: canTrack ? 'granted' : 'denied', analytics_storage: canTrack ? 'granted' : 'denied', }); // RevenueCat attributes gated on consent. await Purchases.setAttributes({ $consentGiven: canTrack ? 'true' : 'false', }); if (!canTrack) { // Clear identifier-derived attributes. await Purchases.collectDeviceIdentifiers(); }}
The crucial line is analytics().setConsent(...). Older Firebase SDKs would keep a small set of background events flowing even with collection "disabled." Calling setConsent explicitly is the belt-and-suspenders pattern that keeps you safe across SDK versions.
Why Firebase Analytics needs the belt-and-suspenders pattern
A detail worth dwelling on: Firebase Analytics has had subtle consent-leak bugs in the past, where background session and retention events kept firing despite setAnalyticsCollectionEnabled(false). These have been patched in newer SDKs, but Rork-generated projects often pin older versions, and mixed-version dependency graphs are common. setConsent is the documented, explicit way to tell Analytics that specific data categories are denied, and it survives SDK upgrades gracefully.
If you're using other analytics layers — Mixpanel, Amplitude, PostHog — the same principle applies. Each has its own "opt out" or "consent" API, and each one needs to be called in the same propagation function. I keep a single propagateConsent() that touches every SDK; adding a new analytics dependency is a one-line change there, which prevents silent regressions when a new team member wires in something without knowing the consent contract.
For RevenueCat specifically, the reason to clear device identifiers when consent is denied is that RevenueCat uses those identifiers for attribution (which install drove which purchase). Without consent, you can still receive the purchase webhook, but you shouldn't be tying it back to an advertising identifier. Clearing the attribute is the safest way to maintain that boundary.
Step 5: A consent-revocation screen that won't get your app rejected
AdMob policy and App Store Review Guidelines both require an in-app way for users to change their mind about consent. Missing this path is a common cause of store rejections and AdMob warnings.
// src/screens/SettingsScreen.tsximport React from 'react';import { View, Text, Button, Alert } from 'react-native';import { AdsConsent, AdsConsentStatus } from 'react-native-google-mobile-ads';export function PrivacySettingsSection() { const [isLoading, setIsLoading] = React.useState(false); const handleRevokeConsent = async () => { setIsLoading(true); try { // Reset the cached state. await AdsConsent.reset(); // Re-fetch consent info. await AdsConsent.requestInfoUpdate(); const info = await AdsConsent.getConsentInfo(); if ( info.isConsentFormAvailable && info.status === AdsConsentStatus.REQUIRED ) { await AdsConsent.loadAndShowConsentFormIfRequired(); } Alert.alert( 'Preferences updated', 'Restart the app to apply your new preferences everywhere.', ); } catch (error) { Alert.alert( 'Error', 'We couldn\'t update your preferences. Please check your connection and try again.', ); } finally { setIsLoading(false); } }; return ( <View style={{ padding: 16 }}> <Text style={{ fontSize: 16, fontWeight: '600' }}>Privacy preferences</Text> <Text style={{ marginTop: 8, color: '#666' }}> You can change your ad personalization preferences at any time. </Text> <Button title={isLoading ? 'Updating…' : 'Update preferences'} onPress={handleRevokeConsent} disabled={isLoading} /> </View> );}
I phrase the button as "Update preferences" rather than "Revoke consent" because the second one reads as a scary, one-way action. You're still offering the same capability — you're just not priming the user to avoid it.
Designing the settings entry so users can actually find it
A revocation screen that exists in the settings tree but takes four taps to reach is technically compliant and functionally invisible. In apps I've shipped, I keep "Privacy preferences" as a top-level row in the main settings screen — same depth as "Account" or "Notifications" — with a short subtitle like "Manage ad personalization."
The second-order benefit of making it visible is auditability. If a user writes in asking how to change their preferences, you want to be able to say "Settings → Privacy preferences," one sentence. If that's not the real path, you'll end up walking people through four taps in support email after support email, which eats hours and degrades goodwill.
Six real-world pitfalls, and how to dodge them
These are the issues I've watched people — including me — burn hours on.
1. Calling AdMob.initializeAsync() before showIfRequired
The most common bug. You call requestInfoUpdate, feel productive, skip to AdMob init, and ship. EEA users get ad requests without having seen the consent form. That's the canonical AdMob Publisher Policy violation.
How to verify: In the AdMob console, filter requests by country to EEA. If the canRequestAds signal never went true but you're still seeing impression attempts, the init order is wrong.
2. Showing ATT before UMP
If you call request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY) ahead of UMP, a user who grants tracking but later declines personalization in UMP ends up with contradictory states. Always UMP first.
How to verify: Screen-record your app on iOS. The first dialog you see should be the UMP form (white card, consent options), not the stark system ATT alert. If ATT appears first, swap the order of the await calls in your bootstrapper.
3. Android consent form doesn't show up on the test device
On Android, AdsConsentDebugGeography.EEA alone isn't enough — you also need testDeviceIdentifiers. The device hash is printed to logcat on first launch; copy it into the init options.
4. Skipping the revocation UI and getting rejected
Since 2024, App Store Review Guideline 5.1.1 explicitly requires a way to revoke consent. Play Store matches. No revocation screen in your settings → eventual rejection.
5. Leaking the debug geography into production
If you ship with debugGeography: AdsConsentDebugGeography.EEA not guarded by __DEV__, every non-EEA user gets a consent form they don't need. That depresses consent rates and shaves CPM across the rest of the world.
6. NPA-only first session
Because UMP is async and Rork's default init is synchronous, it's easy to fire ad requests before consent resolves. Always await initializeConsent() and then await mobileAds().initialize(). I've seen a clean fix bump first-session CPM by 20–30%.
Recovering from an AdMob policy warning
If the warning email already arrived, here's the recovery path I've used.
Ship a new build with the UMP flow above. Verify on TestFlight / internal testing first.
In the AdMob console, open "Policy center," find the flagged app, and mark it as fixed.
Google's automated rescan runs within ~48 hours. If the issue is gone, the warning lifts.
After the warning clears, EEA ad delivery ramps back up — usually within a week.
There's a lag between shipping the fix and users installing it, so if the drop is significant it's worth promoting the update — a soft in-app banner nudging people to the latest version, or, for a serious outage, a forced upgrade check.
What to put in the re-submission note
When you mark the app as fixed in the Policy center, there's a free-form note field. Don't leave it blank — reviewers are humans and a one-paragraph summary helps. I include three things: the specific version number that contains the fix, the exact line of code where UMP is now invoked (file path + function name is plenty), and the test procedure I used to verify it works from an EEA IP. That has meaningfully shortened my review loops.
If the rescan still flags you after 48 hours, the most common causes are (a) the cached CDN still serves the old build to some users because your store listing hadn't propagated, or (b) your app-ads.txt has a mismatch that's tripping a separate policy. Check those before assuming a new bug.
Testing — covering geography and state
Cover these four scenarios at a minimum.
EEA user, first launch: Debug geography EEA → form appears → "Consent" → canRequestAds: true.
EEA user, declines: Same form → "Do not consent" → canRequestAds: false.
Non-EEA user: NOT_EEA → no form → canRequestAds: true immediately.
Debugging a user-reported "ads aren't personalizing" case
Once you're live, you'll eventually get a support email that reads like this: "I'm getting the same three generic ads over and over, is your app broken?" Don't panic — nine times out of ten, the user just declined personalization at some point, and they've forgotten. Here's the triage flow I use.
First, ask the user what country they're in and which device. If they're in the EEA, they've almost certainly seen the UMP form and their answer is cached. Walk them through Settings → Privacy preferences → Update preferences, which resets the state and re-prompts.
Second, if they're outside the EEA and still seeing only generic ads, the culprit is usually ATT on iOS. Walk them through iOS Settings → Privacy & Security → Tracking, find your app, and toggle "Allow Tracking" back on. Then they should background and reopen the app so the AdMob SDK re-reads the IDFA state.
Third, if both of those are fine and the problem persists, it's worth verifying that the user isn't on a VPN that's landing them in a different regulatory region than their device locale suggests. A user whose phone is set to US English but whose IP resolves to Germany will get the EEA flow; to them, that looks like a bug.
I keep this triage as a two-paragraph snippet in my support macro library. It answers 90% of these reports without me having to investigate — and when the remaining 10% actually do hit a real bug, I know about it faster because the common cases have been filtered out.
Handling consent for kids' apps and family-friendly content
If your Rork app targets children, or opts into the Play Store's "Designed for Families" program, an entirely different consent regime kicks in. In that context, serving personalized ads to users under 13 is prohibited regardless of the EEA flow, and you need to force NPA mode globally.
The cleanest way to do this in react-native-google-mobile-ads is to pass tagForChildDirectedTreatment and maxAdContentRating in the request configuration. With those flags set, AdMob treats every user as a child, serves only TFCD-compatible ads, and ignores the personalization signals from UMP. Keep in mind that CPM is meaningfully lower under TFCD, so this is a decision you make for compliance reasons, not revenue ones.
If you're in this category, also double-check that your Firebase Analytics project has analytics_storage: 'denied' by default, because child-directed data collection rules (COPPA in the US, the Children's Code in the UK) are substantially stricter than general GDPR rules.
Built-in UMP vs. a Google Certified CMP
The UMP SDK that ships with AdMob is free, and for a Rork app it's the most frictionless option. That said, it's deliberately minimal. Apps with broader needs — "full IAB TCF v2.2 conformance," "shared consent with Meta Audience Network and other mediated networks," "audit trail of consent history for legal evidence" — eventually outgrow it.
Google-certified CMPs include Didomi, OneTrust, Usercentrics, and iubenda. They all ship React Native SDKs, and pricing scales with MAU — somewhere around $150–$500/month at ~100k MAU is typical. Indie-scale apps rarely need this; built-in UMP is usually plenty. Consider a paid CMP if any of the following are true:
You mediate multiple ad networks (Meta Audience Network, Unity Ads) in addition to AdMob.
You have multiple analytics pipes (Mixpanel, Amplitude, custom) that each need their own consent plumbing.
You want one place to handle GDPR plus CCPA, LGPD, and so on.
You need a server-side consent log for legal defensibility.
The honest answer for most Rork apps: stay on built-in UMP until a second monetization partner forces your hand.
Jurisdictions beyond the EEA — CCPA, LGPD, PIPL, and Japan
GDPR gets the attention, but it's far from the only regime to consider once your app lives in multiple regions.
California (CCPA / CPRA): You need a "Do Not Sell My Personal Information" path. UMP doesn't cover this; you add it yourself.
Brazil (LGPD): Close to GDPR but with slightly different timing and wording around consent.
China (PIPL): Cross-border transfer of personal information requires explicit consent. If your Rork app pushes data to Firebase or any other overseas backend, this matters.
Japan (revised Personal Information Protection Act): Since the 2022 amendment, cookie-like identifiers are treated as "personal-related information." Most Japanese apps cover this via terms-of-use acceptance.
An aside on LGPD: Brazil's law explicitly requires that consent be "specific, informed, and unambiguous," which means a generic "by using this app you consent" line buried in your terms of service does not count. If you have meaningful Brazilian traffic, you need an actual opt-in step — a lightweight checkbox on first launch, with a link to your privacy policy, is the minimum workable pattern. UMP will handle this for you automatically if you extend your AdMob privacy messaging configuration to cover Brazil, which is a single toggle in the console.
On PIPL, the thing that catches most Rork developers off guard is that it isn't enough to show a consent dialog — you also need to name the overseas recipients by country and purpose. So "We send crash logs to Google in the United States" is fine; "We send data to third-party services" is not. If you operate a serious user base in mainland China, this is worth spending an hour on with a local advisor, because missteps here have led to actual app takedowns.
In practice I stage the work: cover EEA with built-in UMP first, then add ccpaOptOut: true via react-native-google-mobile-ads, then a "privacy policy + consent checkbox" flow for Japan. Trying to satisfy every regime on day one is how solo projects stall. Watch your user distribution in analytics and expand coverage where the actual users are.
Monitoring the revenue impact
After you ship UMP, build a habit around checking whether revenue has actually recovered. Three weekly signals from the AdMob console catch almost every regression.
Country-level eCPM: If EEA eCPM is dramatically lower than comparable countries, your consent rate is likely low — often a wording or placement problem in the form.
Ad requests vs. match rate: A match rate far below 80% hints at NPA-lock. Inspect your init sequence.
Revenue split by format: Rewarded video holds up better under NPA than banners. A sudden banner drop tells you personalization is off for real.
Consent rate itself can swing 10–20% based on form copy and button placement. In my own experience, adding a single line like "Personalized ads help us show you more relevant content" nudges acceptance up measurably — without being pushy.
A simple dashboard you can build in an afternoon
If you want to go beyond the default AdMob reports, pipe daily country-level eCPM, consent rate (derived from canRequestAds signals), and NPA-share into a Google Sheet via the AdMob Reporting API. Even a single tab with 14 days of rolling history catches 90% of problems early. I've caught a subtle regression — a new feature flag that re-ran the Firebase init before UMP resolved — within a day because the EEA NPA share spiked on the dashboard.
Consent rate visibility is especially important because it's invisible in the default UI. AdMob will happily tell you eCPM per country, but it won't tell you "your EEA consent rate dropped from 72% to 55% last week." Compute that yourself from your own telemetry and you can catch UX regressions — a tone-deaf copy change, a button moved to a less prominent position — before they cost a month of revenue.
A concrete first step for today
Once UMP and ATT are wired in properly, maintenance on this layer becomes close to zero. Leave it alone and you risk an out-of-nowhere AdMob warning that zeroes your EEA revenue for days. Not fun.
The one concrete thing to do today: check whether EEA users actually see a UMP form in your current build. Launch your app on TestFlight or an Android device with AdsConsentDebugGeography.EEA set. No form? You've just found a top-priority task for this week. The code in this article should get you from "no form" to "production-quality flow" in a single afternoon.
One last thought before you close this tab. Consent management is one of those engineering problems where the right answer isn't "more code" — it's "code that runs in the right order, once." Once you have that sequence nailed down in your Rork app, the whole stack becomes boring in the best possible way. Boring means stable revenue and no 3 a.m. emails from AdMob. That's worth the afternoon it takes to get right.
Rork 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.