* ci: test AdMob adapter deploy with hardcoded version 5.66.0 Temporarily hardcode version and simplify deploy-tag workflow to test the deploy-to-purchases-ios-admob job from the release-admob-adapter branch. * Revert "ci: test AdMob adapter deploy with hardcoded version 5.66.0" This reverts commit 104542f0ab0a0dbb590b19187f8e598ca8ece96c. * feat: add AdMobIntegrationSample example app xcodeproj and config Add the Xcode project, Info.plist, gitignore, and Package.resolved for the AdMob example app. Clean up duplicate RevenueCatAdMob package dependencies and fix project-level deployment target. * fix: allow example app Package.resolved through gitignore The root Package.resolved pattern matched at any depth, silently excluding the example app's Package.resolved from the deployed repo. Add a negation so the example app keeps reproducible builds. * fix: gitignore Package.resolved in example app instead of committing it * feat: add VanillaAdTrackingSample example app for manual ad tracking Adds a companion example to AdMobIntegrationSample that demonstrates manual ad event tracking via Purchases.shared.adTracker without the RevenueCatAdMob adapter library. Covers all ad formats (banner, interstitial, app open, rewarded, rewarded interstitial, native) with manual delegate callbacks and paidEventHandler wiring. * feat: add VanillaAdTrackingSample to Tuist workspace Adds Tuist project definition and workspace entry for the vanilla ad tracking example. Introduces vanillaAdTrackingPackages helper that provides RevenueCat (local) + GoogleMobileAds (remote) without the adapter dependency. * Fixup * docs: remove adapter comparison from VanillaAdTrackingSample README * fix: use dynamic placement in banner impression and click callbacks * fix: update Google Mobile Ads version constraint to allow v13 * fix: use configuration-based styling for CTA button in native ad view * fix: remove unused placement parameter from loadNativeAd * fix: inline Google Mobile Ads package in Project.swift Move the Google Mobile Ads package declaration directly into the VanillaAdTrackingSample Project.swift instead of using a shared helper, since it's only used by this one project. * fix: remove unused VanillaAdTrackingSample-Info.plist The info.plist values are already declared in Project.swift via infoPlist: .extendingDefault, making this file redundant. * fix: add didFailToPresentFullScreenContent delegate method Handle the case where a loaded full-screen ad fails to present by cleaning up metadata, resetting the ad reference and status. This prevents leaked metadata entries and stuck "Ready" states. * fix: mark AdMobManager as @MainActor for thread safety AdMob callbacks may fire off the main thread, but @Published properties are observed by SwiftUI and must be mutated on main. * docs: update build instructions to use tuist generate The project uses Tuist for workspace generation, not a standalone .xcodeproj file. * fix: move native ad delegate assignment before availability guard The delegate assignment is not iOS 15-dependent and should be set before the guard so impression/click callbacks work regardless of the consumer's deployment target. * refactor: extract shared full-screen ad cleanup into helper method Deduplicate the identical cleanup logic between adDidDismissFullScreenContent and ad(_:didFailToPresentFullScreenContentWithError:) into a single cleanUpFullScreenAd helper. * fix(ads): save adLoader before setting delegate * chore(ads): cleanup vanilla example and project file * fix(ads): remove hardcoded development team * fix(ads): move from local to remote package dep * fix(ads): revert tuist and fix project file * fix(ads): revert tuist and fix project file --------- Co-authored-by: Peter Porfy <peter.porfy@revenuecat.com>
Vanilla Ad Tracking Sample
This sample app demonstrates how to track Google AdMob ad events with RevenueCat by calling Purchases.shared.adTracker.trackXxx(...) directly in each AdMob callback.
Overview
Architecture
┌─────────────┐
│ AdMob SDK │
│ (Load Ads) │
└──────┬──────┘
│
│ Ad callbacks (delegate, completion, paidEventHandler)
▼
┌─────────────────────────┐
│ Your App Code │
│ (Manual tracking) │
│ │
│ Purchases.shared │
│ .adTracker │
│ .trackAdLoaded(...) │
│ .trackAdDisplayed(...)│
│ .trackAdOpened(...) │
│ .trackAdRevenue(...) │
│ .trackAdFailedToLoad(...)│
└──────┬──────────────────┘
│
│ RevenueCat events
▼
┌────────────────────────┐
│ RevenueCat Dashboard │
│ (Analytics) │
└────────────────────────┘
5 tracked events
| Event | When to call |
|---|---|
| Ad Loaded | Ad successfully loads (completion handler / delegate callback) |
| Ad Displayed | Ad impression is recorded (adDidRecordImpression / bannerViewDidRecordImpression) |
| Ad Opened | User clicks/interacts with ad (adDidRecordClick / bannerViewDidRecordClick) |
| Ad Revenue | Ad generates revenue (paidEventHandler) |
| Ad Failed to Load | Ad fails to load (error callback) |
Code example — manual interstitial tracking
import GoogleMobileAds
import RevenueCat
@_spi(Experimental) import RevenueCat
// Load
InterstitialAd.load(withAdUnitID: adUnitID, request: Request()) { [weak self] ad, error in
guard let self else { return }
if let error {
Purchases.shared.adTracker.trackAdFailedToLoad(AdFailedToLoad(
mediatorName: .adMob,
adFormat: .interstitial,
placement: "my_placement",
adUnitId: adUnitID,
mediatorErrorCode: (error as NSError).code
))
return
}
guard let ad else { return }
let responseInfo = ad.responseInfo
// Track loaded
Purchases.shared.adTracker.trackAdLoaded(AdLoaded(
networkName: responseInfo?.loadedAdNetworkResponseInfo?.adNetworkClassName ?? "",
mediatorName: .adMob,
adFormat: .interstitial,
placement: "my_placement",
adUnitId: adUnitID,
impressionId: responseInfo?.responseIdentifier ?? ""
))
// Track revenue
ad.paidEventHandler = { adValue in
Purchases.shared.adTracker.trackAdRevenue(AdRevenue(
networkName: responseInfo?.loadedAdNetworkResponseInfo?.adNetworkClassName ?? "",
mediatorName: .adMob,
adFormat: .interstitial,
placement: "my_placement",
adUnitId: adUnitID,
impressionId: responseInfo?.responseIdentifier ?? "",
revenueMicros: Int(adValue.value.multiplying(by: 1_000_000).int64Value),
currency: adValue.currencyCode,
precision: .estimated // map from adValue.precision
))
}
// Set delegate for impression/click tracking
ad.fullScreenContentDelegate = self
}
Ad formats demonstrated
This sample uses Google Mobile Ads SDK v13 Swift API:
- Banner —
BannerViewwithBannerViewDelegate - Interstitial —
InterstitialAd.load(...)withFullScreenContentDelegate - App Open —
AppOpenAd.load(...)withFullScreenContentDelegate - Rewarded —
RewardedAd.load(...)withFullScreenContentDelegate - Rewarded Interstitial —
RewardedInterstitialAd.load(...)withFullScreenContentDelegate - Native —
AdLoaderwithNativeAdLoaderDelegate+NativeAdDelegate - Native Video — native loader with a video-oriented ad unit ID
- Error Handling — intentionally invalid ad unit ID to validate failed-to-load tracking
Key files
Sources/App.swift— initializesGoogleMobileAds.MobileAdsandPurchasesSources/HomeView.swift— UI for loading/showing each ad format and statusSources/AdMobManager.swift— all manual ad tracking codeSources/Constants.swift— RevenueCat API key and AdMob ad unit IDs
Experimental API notice
This sample uses RevenueCat's ad tracking APIs exposed as experimental Swift SPI (@_spi(Experimental)), and behavior may evolve before full stabilization.
- Ad tracking APIs are available on iOS 15.0+.
- Swift imports must use
@_spi(Experimental) import RevenueCat. - Review release notes when upgrading RevenueCat.
Mediator name
This sample uses .adMob as the MediatorName in all tracking calls since it uses the AdMob SDK directly. If you use a different ad network or mediation platform, substitute the appropriate value:
.appLovinfor AppLovin MAXMediatorName(rawValue: "YourNetwork")for custom networks
Requirements
- Xcode 16+ recommended
- iOS Simulator or device (iOS 15+)
- A valid RevenueCat API key
- AdMob test ad unit IDs (included in this sample)
- Google Mobile Ads SDK v13 (Swift API; this sample uses v13 naming)
RevenueCatfrom thepurchases-iospackageGoogleMobileAdsfrom the Google Mobile Ads SPM package
Setup & Run
1. Prerequisites
-
Get a RevenueCat API key
- Sign up at revenuecat.com
- Get your API key from the RevenueCat Dashboard
-
AdMob setup (optional for testing)
- This sample uses Google's official test ad unit IDs (see below)
- No AdMob account is required to run the sample as-is
- For production validation, create an AdMob account and replace IDs
2. Configure the app
- Open
Sources/Constants.swift. - Replace
YOUR_REVENUECAT_API_KEYwith your real RevenueCat API key. - Keep the default AdMob test ad unit IDs for local testing.
3. Build and run
- From the repository root, run
tuist generateto generate the Xcode workspace. - Open the generated workspace and select the
VanillaAdTrackingSamplescheme. - Run on an iPhone simulator or device.
4. Verify ad events
In the app:
- Tap an ad format from the list.
- Tap Load and wait for status to become
Ready. - Tap Show (where applicable).
- Interact with the ad and dismiss it.
The sample prints diagnostics in the Xcode console and emits RevenueCat ad events for each format. For dashboard verification, background the app after testing to trigger SDK flush.
Test ad unit IDs used by this sample
| Ad Format | Ad Unit ID | Status |
|---|---|---|
| Banner | ca-app-pub-3940256099942544/2435281174 |
Working |
| Interstitial | ca-app-pub-3940256099942544/4411468910 |
Working |
| Rewarded | ca-app-pub-3940256099942544/1712485313 |
Working |
| Rewarded Interstitial | ca-app-pub-3940256099942544/6978759866 |
Working |
| App Open | ca-app-pub-3940256099942544/5575463023 |
Working |
| Native | ca-app-pub-3940256099942544/2247696110 |
Unreliable |
| Native Video | ca-app-pub-3940256099942544/1044960115 |
Unreliable |
| Error Testing | invalid-ad-unit-id |
Working |
These are official Google test IDs and are safe for development.
Native ads and test IDs
Native and native video test IDs can be less reliable than other formats depending on environment. For more reliable native testing:
- Create ad units in your AdMob account
- Replace test IDs in
Sources/Constants.swift - Configure your device as a test device in AdMob
- Keep test mode enabled during validation
Error testing
AdMob does not provide an official "error trigger" test ad unit ID. This sample uses invalid-ad-unit-id to intentionally fail ad load requests and validate failed-to-load tracking.
Use the Error Testing section in the sample UI:
- Tap Trigger Ad Load Error.
- This uses
Constants.AdMob.invalidAdUnitIDto intentionally fail load. - Confirm failure is logged and failure tracking is emitted.
Troubleshooting
"Missing RevenueCat API key" or build fails at startup
Make sure revenueCatAPIKey in Sources/Constants.swift is set to your real RevenueCat API key (replace the YOUR_REVENUECAT_API_KEY placeholder).
Ads not loading
- No internet connection — ensure simulator/device has internet access
- AdMob SDK still initializing — wait a few seconds after launch
- Test device not configured — simulator is typically treated as test; real devices may require explicit setup in AdMob
- Ad unit mismatch — start with Google's test IDs before custom IDs
"Invalid request" on real device
Your device may not yet be recognized as a test device. Configure it in AdMob and retry after propagation.
Revenue events not visible
- Verify
Purchases.configure(withAPIKey:)is called (Sources/App.swift) - Keep
Purchases.logLevel = .debugduring troubleshooting - Confirm you are calling
adTracker.trackAdRevenue(...)in each ad'spaidEventHandler - Background the app after interactions so events flush sooner
Revenue callbacks missing in test mode
AdMob test ads may not always trigger paidEventHandler. Revenue tracking is more reliable with production ad units on configured test devices.
Tracking calls not appearing in console
- Ensure you have
@_spi(Experimental) import RevenueCat(not justimport RevenueCat) - Confirm you're running on iOS 15+ (ad tracking APIs require
@available(iOS 15.0, *)) - Verify
Purchases.isConfiguredistruebefore tracking calls execute
AdMob SDK version note
This sample uses Google Mobile Ads v13; no compiler flag is required.