Skip to content

feat(admob): add remaining next-gen ad formats - #3962

Closed
polmiro wants to merge 37 commits into
add-next-gen-admob-interstitialfrom
add-next-gen-admob-remaining-formats
Closed

feat(admob): add remaining next-gen ad formats#3962
polmiro wants to merge 37 commits into
add-next-gen-admob-interstitialfrom
add-next-gen-admob-remaining-formats

Conversation

@polmiro

@polmiro polmiro commented Aug 13, 2026

Copy link
Copy Markdown
Member

Motivation

Complete Google Mobile Ads Next-Gen tracking coverage for the formats that follow banner and interstitial support.

Description

  • Add load-and-track entry points for rewarded, rewarded interstitial, app open, and native ads.
  • Install RevenueCat event tracking before loaded ads reach application callbacks.
  • Support show-time placement overrides for every new full-screen format.
  • Add safe event-callback reassignment helpers that preserve RevenueCat tracking.
  • Forward mixed native-loader results and completion callbacks while tracking standard native loads and failures.
  • Publish the experimental API surface and cover success, failure, delegation, placement, and callback reassignment flows.

Stack

Validation

  • ./gradlew :feature:admob-next-gen:build
  • ./gradlew detektAll
  • ./scripts/api-check.sh

@polmiro polmiro added the pr:feat A new feature label Aug 13, 2026
@RevenueCat-Danger-Bot

RevenueCat-Danger-Bot commented Aug 13, 2026

Copy link
Copy Markdown

Danger has errored

[!] Invalid Dangerfile file: �[31m
[!] Invalid temporary_danger.rb file: syntax error, unexpected \<
<html><body><h1>503 Service Un...
^
�[0m

from /tmp/d20260817-119-fjy7/temporary_danger.rb:1

-------------------------------------------

> <html><body><h1>503 Service Unavailable</h1>

No server is available to handle this request.

-------------------------------------------

 #  from Dangerfile:1
 #  -------------------------------------------
 >  danger.import_dangerfile(github: 'RevenueCat/Dangerfile')
 #  
 #  -------------------------------------------

Generated by 🚫 Danger

@polmiro
polmiro force-pushed the add-next-gen-admob-banner-interstitial branch 2 times, most recently from 9af814b to 4eef8bb Compare August 13, 2026 11:56
@polmiro
polmiro force-pushed the add-next-gen-admob-banner-interstitial branch from 4eef8bb to 74ad4e5 Compare August 17, 2026 08:33
@polmiro
polmiro force-pushed the add-next-gen-admob-remaining-formats branch from ceb4647 to adf3473 Compare August 17, 2026 08:33
@emerge-tools

emerge-tools Bot commented Aug 17, 2026

Copy link
Copy Markdown

📸 Snapshot Test

621 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 0 0 282 0 N/A
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 339 0 N/A

🛸 Powered by Emerge Tools

polmiro added 21 commits August 17, 2026 10:59
Set unitTests.isReturnDefaultValues so android.util.Log calls do not throw
in JVM unit tests, matching the feature/admob module. Without it, any test
reaching the not-configured branch of trackIfConfigured fails with
"Method w in android.util.Log not mocked".

Add coverage for that branch: TrackIfConfiguredTest for the guard itself,
plus a test per callback wrapper asserting the delegate and configureAd
still run when Purchases has not been configured.
Replace PurchasesTestHelper, which reflected on the internal
backingFieldSharedInstance, with mockkObject(Purchases) plus stubs for the
public isConfigured and sharedInstance members.

This is what the rest of the codebase does from outside the :purchases
module (CheckpointWorkflowActivityTest, PaywallDialogTests) and inside it
(ScreenOfferingExtensionsTest). Reflection on a string field name only
fails at runtime if the field is renamed; stubbing public API is checked
at compile time. The reflection and @file:Suppress("INVISIBLE_REFERENCE")
variants in feature/admob remain the only occurrences in the repo.

The unconfigured-path tests now stub isConfigured to false rather than
clearing the singleton.
The file opted into ExperimentalPreviewRevenueCatPurchasesAPI but touches no
experimental API: trackIfConfigured is plain internal and Purchases is stable.
…perty

The constructor took a plain delegate param and re-declared a property that
shadowed it. TrackingFullScreenContentCallback in feature/admob, the existing
precedent for a mutable delegate, declares it inline instead.
Both wrappers were asserted in one test method, so a failure on the load
callback hid the banner refresh callback's result. They are now independent
test methods. The shared helper is private since it is only used here.
The failure path deliberately does not configure the ad, but nothing pinned
it. Note this documents intent rather than guarding a reachable regression:
configureAd takes an AdT, which onAdFailedToLoad has no access to, so it
could only start firing if the signature changed.
AdapterTracking and TrackIfConfigured are ports of feature/admob files that
carry this KDoc; it was dropped in the port. The two callback wrappers had
none, while their legacy counterparts document which callback maps to which
event.

Also records two things only the tests encoded: configureAd runs after
tracking and before the delegate and never for a failed load, and
responseInfoProvider is read when the callback fires because the SDK does not
pass the refreshed ResponseInfo to onAdRefreshed.
The block was separated from the first function by a blank line, making it an
orphan comment attached to nothing. It now documents the trackFromAdapter
overloads.
Add an Events tracked section covering the two events this change reports,
Ad Loaded and Ad Failed to Load, along with their payload fields and the
callback-forwarding guarantee.

Display, open and revenue events are documented in the event-tracking
change that builds on this one.
Add the one-line description of the module and the Kotlin-only note.

The placement guide, per-format usage examples, supported-formats table and
experimental-API note previously staged here document the public loading
APIs, which land in the changes above this one, so they move there.
TrackingAdEventCallback captured ResponseInfo at construction, so an
auto-refreshing banner reported the first-loaded creative's impressionId
and networkName for every later displayed/opened/revenue event, while
TrackingBannerAdRefreshCallback emitted ad_loaded with fresh response
info. Take a () -> ResponseInfo provider instead, matching the refresh
callback and the legacy admob wrappers.
The precision mapping covered every PrecisionType in the pinned Google
Mobile Ads SDK, so Kotlin compiled the when with an implicit else that
throws NoWhenBranchMatchedException. The module ships as a precompiled
artifact and declares the SDK with implementation, so an app resolving a
newer SDK could deliver a constant this binary never saw and crash inside
the paid-event callback.

Map unrecognized constants to UNKNOWN, matching the legacy admob module.
The exhaustiveness assertion in AdMobPrecisionMappingTest keeps failing at
build time when the SDK grows a constant, so the mapping cannot silently
fall behind.
…back

The six format callbacks differ only in the AdFormat and AdDisplayedTrigger
they pass to the base class, but only banner and interstitial were asserted.
A wrong format, or an IMPRESSION trigger on a full-screen format (which would
double-count displays, since the SDK fires onAdImpression alongside
onAdShowedFullScreenContent), would have shipped silently.

Replace the two single-format tests with a table covering all six, asserting
the tracked format and that the other trigger tracks nothing. A companion
test pins the table against the shared callback map so a new format cannot
be added without covering it.

Verified by mutation: flipping app open to AdFormat.REWARDED and to
AdDisplayedTrigger.IMPRESSION each fail the table test.
assertOverridesAllSdkCallbacks only compares method signatures, so an
override with an empty body passed. That left onAdShowedFullScreenContent,
onAdDismissedFullScreenContent, onAdFailedToShowFullScreenContent,
InterstitialAdEventCallback.onAppEvent, the rewarded interstitial metadata
callback and every TrackingAppOpenAdEventCallback method unverified.

Drive each SDK callback reflectively against a JDK proxy delegate that
records the invoked method, with Purchases configured so the real tracking
path runs first. This also pins that a throw on the tracking path cannot
swallow the delegate call. A second test repeats the sweep with a null
delegate to pin the no-callback path.

Verified by mutation: dropping the onAdDismissedFullScreenContent forward
fails the new test and nothing else.
FormatTrackingAdEventCallbacks was the only production file in the package
opting in at file level. Every sibling, including AdMobPrecisionMapping
added in the same PR, annotates the declaration. Narrow the scope to match.
onAdClicked, onAdPaid and trackAdDisplayed each repeated the configured
check, the event-time response info read and the adTracker hop. Pull that
into a private track helper so the callbacks carry only their payload, and
so the event-time read is stated once instead of three times.
…ructor

The base class took delegate and placement as plain parameters and then
re-declared each as a property initialized from the parameter it shadowed.
Overrides resolve to the property, so behaviour is unchanged, but inside an
init block or a property initializer the bare name would silently read the
parameter instead.

Declare both as constructor properties, removing the shadowing and matching
the legacy admob TrackingFullScreenContentCallback.
polmiro added 11 commits August 17, 2026 12:09
delegate and placement are vars so show-time code can swap them after the
ad loaded, but nothing pinned that. The forwarding contract test cannot:
a delegate snapshotted at construction still equals the one passed in, so
it passes either way.

Assert that swapping the delegate redirects both an inherited override and
a format-specific one, that clearing it stops forwarding while tracking
continues, and that display and click read placement at event time the way
the paid callback already does.

Verified by mutation: forwarding to a delegate captured at construction
fails the two delegate tests, and a placement captured at construction
fails the third.
ADS-536. FormatTrackingAdEventCallbacksTest was the last legacy-shaped
holdover: one hand-written test per format asserting the delegate was
called. The reflective sweep already drives every SDK method on all six
wrappers, so the rewarded and native tests were pure duplicates.

The banner test was not, because the sweep only asserted method names.
Assert forwarded arguments too, with a distinct value per position so a
swapped or dropped argument fails, and keep one focused test for the null
app-event payload that reflection cannot supply. Then drop the file.

Net coverage is higher, not lower: argument fidelity now applies to every
callback on every wrapper rather than to onAppEvent alone.

Verified by mutation: swapping onAppEvent's arguments fails both the sweep
and the null-payload test.
Extend the Events tracked section with the three events this change adds,
the per-format display trigger, and the event callbacks that are forwarded
but not tracked.

Scoped to what this change implements: the load events are documented in
the load-tracking change below it, and the loading APIs that expose these
callbacks are documented in the changes above it.
The flow tests asserted callback wiring and delegation but never installed a
Purchases singleton, so trackIfConfigured short-circuited on every path and the
load-time tracking arguments went unchecked. The wrapper classes are covered in
TrackingAdEventCallbackTest, but nothing pinned what the banner and interstitial
entry points hand to TrackingAdLoadCallback: swapping AdFormat.BANNER for
INTERSTITIAL, dropping the placement, or not reading adUnitId off the request all
kept the suite green.
The generated AdViewExtensionsKt-style names were leaking into api.txt and Dokka.
Follow feature/admob's @file:JvmName convention, keeping NextGen in each name so
the facades stay unambiguous next to the legacy module's.
@polmiro
polmiro force-pushed the add-next-gen-admob-banner-interstitial branch from 74ad4e5 to b64c6d1 Compare August 17, 2026 10:29
@polmiro
polmiro force-pushed the add-next-gen-admob-remaining-formats branch from adf3473 to 71ad36a Compare August 17, 2026 13:47
@polmiro
polmiro force-pushed the add-next-gen-admob-remaining-formats branch from 71ad36a to 9cc85de Compare August 17, 2026 15:59
@polmiro
polmiro changed the base branch from add-next-gen-admob-banner-interstitial to add-next-gen-admob-suspend-loading August 17, 2026 16:00
@polmiro
polmiro force-pushed the add-next-gen-admob-suspend-loading branch 2 times, most recently from 7ba09ad to 837f78c Compare August 19, 2026 07:47
@polmiro
polmiro changed the base branch from add-next-gen-admob-suspend-loading to add-next-gen-admob-interstitial August 19, 2026 09:44
@polmiro polmiro closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants