Skip to content

Ask for consent before showing an ad in the Lite app - #136

Draft
andiwand wants to merge 1 commit into
mainfrom
add-consent-form-before-ads
Draft

Ask for consent before showing an ad in the Lite app#136
andiwand wants to merge 1 commit into
mainfrom
add-consent-form-before-ads

Conversation

@andiwand

@andiwand andiwand commented Aug 5, 2026

Copy link
Copy Markdown
Member

Draft — the code builds, but the consent messages still have to be configured in AdMob before this can ship. See Before merging.

The gap

The Lite app serves ads to EEA, UK and Swiss users with no consent management platform in front of them. Today DocumentViewController asks for ATT and then loads the banner regardless of the answer.

ATT is not a substitute. It is Apple's question about the IDFA and cross-app tracking; it does not cover the ePrivacy rule on storing and reading identifiers on the device, and it does not satisfy Google's EU user consent policy, which has required a Google-certified CMP integrated with IAB TCF v2.2 since 16 January 2024.

The Android app has had exactly this since f6b942b6 (Dec 2023). This side never got it.

What changed

  • Link GoogleUserMessagingPlatform. It was already resolving transitively via GoogleMobileAds (UMP 3.1.0 is in Package.resolved today); this makes the dependency explicit so the product can be imported.
  • New ConsentManager, deliberately shaped like the Android AdManager consent flow.
  • Ad setup moves from viewWillAppear to viewDidAppear — the form is presented modally, and viewWillAppear is too early because the controller is not yet in the window hierarchy. Guarded so the ask happens once per controller.
  • ATT now follows the consent form instead of replacing it, and is only asked of users who will actually be shown an ad.

Whether an ad may be loaded is canRequestAds and nothing else — never whether the consent calls came back clean. The SDK caches the decision, so a form that fails to present, or an update that times out offline, still leaves an earlier consent standing; and outside the regions where a form is required there is no decision to fail in the first place. This is the same mistake fixed on Android in opendocument-app/OpenDocument.droid#569.

Who actually gets asked

Decided by UMP, not by this code: messages are geo-targeted in AdMob under Privacy & messaging, and the SDK resolves the user's region server-side. Users outside a configured region see no form and get canRequestAds == true, exactly as today.

Before merging

  • Create and publish the GDPR message in AdMob → Privacy & messaging, targeted at EEA/UK/CH, for both the Lite iOS app and its ad unit. Without a published message the SDK returns no form and nothing changes.
  • Consider whether a US states (CCPA/CPRA) message is wanted at the same time.
  • Decide whether to offer a way to re-open the form later (ConsentInformation.shared.reset() in a debug menu, or a privacy-settings entry point). GDPR expects consent to be withdrawable, and there is currently no path back to the form.
  • Confirm the print logging is the right channel, or route it through CrashManager as Android does.
  • Legal review of whether non-personalised ads may be served without consent in your reading — this PR takes the conservative line and serves nothing when canRequestAds is false.

Testing

Both schemes build clean:

xcodebuild -scheme "ODR Lite" -configuration "Debug Lite" -sdk iphonesimulator build   # BUILD SUCCEEDED
xcodebuild -scheme "ODR Full"                             -sdk iphonesimulator build   # BUILD SUCCEEDED

Not yet run on a device or simulator, and the form has never been displayed because no message is published yet. Worth checking once it is:

  • EEA geography (ConsentDebugSettings with DEBUG_GEOGRAPHY_EEA plus a test device id) → form appears before ATT, banner loads after accepting
  • decline → banner hidden, no ad request
  • accept, then relaunch in airplane mode → banner still loads from cached consent
  • outside the EEA → no form, banner loads as before
  • Full/pro app → unaffected, no banner, no form

Why it matters commercially

iOS is now about a third of ad revenue, and it is the half of the estate with no CMP — so the exposure sits on the growing side. Expect the Android pattern to follow once the form is live: fill rate drops as some users decline or accept only non-personalised ads. That is the cost of doing this correctly, and it is already priced into the Android numbers.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PafKp4GDZk7AYPbSNLTiti

The Lite app serves ads to EEA, UK and Swiss users with no consent
management platform in front of them. ATT is not a substitute: it is
Apple's question about the IDFA, and it neither covers the ePrivacy rule
on storing and reading identifiers on the device nor satisfies Google's
EU user consent policy, which has required a certified CMP integrated
with IAB TCF since January 2024. The Android app has had one since
f6b942b6; this side never got it.

Add Google's UMP SDK, already resolving transitively as a dependency of
GoogleMobileAds and now linked explicitly, and gather consent before the
banner loads. Whether an ad may be loaded is canRequestAds() and nothing
else - never whether the consent calls came back clean - because the SDK
caches the decision, so a form that fails to present or an update that
times out offline still leaves an earlier consent standing, and outside
the regions where a form is required at all there is no decision to fail.

Who is asked is decided by UMP, not here: the messages are geo-targeted
in AdMob under Privacy & messaging and the SDK resolves the region
server-side, so users outside a configured region see no form and get
canRequestAds() == true, as today.

Move the ad setup to viewDidAppear. The form is presented modally and
viewWillAppear is too early for that, since the controller is not in the
window hierarchy yet. ATT now follows the consent form rather than
replacing it, and is only asked of users who will actually see an ad.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PafKp4GDZk7AYPbSNLTiti
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant