monetization-sdk-integration
Use when adding, upgrading, or auditing a third-party monetization SDK (AdMob / Google Mobile Ads incl. UMP consent; the same contract applies to any other ad or subscription SDK), or when a PR adds `import GoogleMobileAds` outside the single live-bridge file, breaks `canImport`
Install
npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/monetization-sdk-integration
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
git clone https://github.com/wei18/apple-dev-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wei18/apple-dev-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Monetization SDK Integration
When to invoke
- Adding a new monetization SDK (AdMob, ATT, UMP, AdMob mediation networks, RevenueCat, etc.)
- Upgrading existing AdMob / UMP versions (e.g. v11 → v13)
- Auditing PR diff that touches the monetization target's ad-bridge sources (e.g.
Sources/<AdsBridge>/) - Cross-platform SDK questions (iOS only? macOS catalyst? watchOS?)
- Anyone proposing "let's just
import GoogleMobileAdsover here too" — IMMEDIATE invoke
Skip when: changing pure values / protocols inside the monetization core target (no third-party touch).
The contract
Default rule: no third-party SDKs in the app. Apple-platform native APIs preferred (OSLog over Sentry, MetricKit over Firebase Crashlytics, GameKit over Steam-style backend, etc.).
Break-glass exception is granted when ALL of:
- The capability genuinely requires the SDK (no Apple-native alternative); e.g. AdMob banner serving has no Apple-platform equivalent
- The SDK is shippable under the project's privacy regime (PrivacyInfo.xcprivacy supports its tracking domains)
- The SDK's import is isolated to a SINGLE source file behind a protocol seam (see §isolation contract below)
- The dep arrow is one-way (consumer → SDK; the SDK does not call back into the host app's code beyond delegate/callback bridges)
- iOS-only conditional compile gating (
canImport) — macOS / Catalyst paths must build without the SDK
If any of (1)-(5) fails: deny. Reject the SDK proposal; suggest Apple-native fallback or sit it out.
The isolation contract
For every accepted SDK:
File-layout invariant
- Protocol file
<SdkName>Bridge.swiftdefines the seam. Plain Swift; no SDK import. - Live impl file
Live<SdkName>Bridge.swiftis the ONLY file allowed toimport <SDKModule>. - All other code uses
any <SdkName>Bridgefor DI. Test seam:Fake<SdkName>Bridgein the matching test target (e.g.Tests/<AdsBridge>Tests/FakeAdMobBridge.swift).
Example for AdMob (currently shipped):
Sources/<AdsBridge>/AdMobBridge.swift— protocol (the monetization target's bridge protocol file)Sources/<AdsBridge>/LiveAdMobBridge.swift— soleimport GoogleMobileAdssiteTests/<AdsBridge>Tests/FakeAdMobBridge.swift— test seam
Cross-boundary type invariant
When the live bridge's output crosses into UI or other host code, it must cross as a platform-neutral type (e.g. SwiftUI's AnyView), never a raw SDK type — that's what keeps UI targets at zero SDK imports even though they display SDK-provided content. Example: BannerViewProviding.bannerView(for:) -> AnyView? returns AnyView, not a GoogleMobileAds type.
Build-time audit (run before every monetization PR merge)
Canonical regex (matches Swift 6 access-level imports too):
# replace <SDKModule> / <project-sources-root> with the project's actual values
rg '^(internal |private |public |@_implementationOnly |@preconcurrency )*import <SDKModule>' <project-sources-root>
Expected count: 1 (live bridge file).
If > 1: the contract is broken. Either consolidate behind the existing bridge OR file an exception in the project's architecture-decisions doc (docs/foundations.md for consumers of collaboration-skills:spec-phase-orchestration) documenting WHY a second import site is necessary (with prior reviewer sign-off) — see Documentation pointers below.
Conditional compile invariant
iOS-only SDKs (AdMob/UMP/most monetization stack):
#if canImport(GoogleMobileAds)
import GoogleMobileAds
// ... live impl code
#else
// macOS / catalyst fallback — usually NoOp returning sensible empty values
#endif
The Package.swift dep arrow itself must also gate the SDK to iOS:
.product(
name: "GoogleMobileAds",
package: "swift-package-manager-google-mobile-ads",
condition: .when(platforms: [.iOS])
),
Without condition:, macOS build fails on swift build because Google ships iOS-only xcframeworks.
Test seam invariant
A test target for the bridge ships Fake<SdkName>Bridge (actor or class). All unit tests inject the fake; real SDK only loaded at runtime via DI in the app's composition root. A shared testing-scaffolding target may ship shared fakes used across both ad and IAP test targets (e.g. FakeAdProvider, FakeIAPClient); the per-SDK bridge fakes live in their own test target.
Fake must:
- Be
Sendable(Swift 6 actor or @unchecked Sendable + lock-guarded) - Have a
script(...)or per-call setter API for deterministic test outcomes - Not import the real SDK (zero hidden dependency)
Real-world incidents this skill encodes
Production ID swap safety
- A Release build's
bannerAdUnitIDconstant first usedfatalError("REPLACE_BEFORE_RELEASE: …")as a transitional guard; it has since been replaced by xcconfig injection (→build-time-secret-injection), which is the standing solution — thefatalErrorform is not a long-term answer on its own - A paired-flip checklist ensures Info.plist
GADApplicationIdentifier+ bridge constant are always updated together
Real banner landed + SDK-view-crossing seam
- The
BannerViewSwiftUI host shipped, crossing into the UI layer per the cross-boundary type invariant above. One sharedBannerSlotViewreplaced per-app placeholder slots. - ID split:
#if DEBUGforces Google's universal test unit; Release reads the per-app prod id fromBundle.mainvia xcconfig.
macOS conditional gating
- Initial AdMob integration left
import GoogleMobileAdsungated → macOS build broke - Fix:
canImport(GoogleMobileAds)+ Package.swiftcondition: .when(platforms: [.iOS])+ macOS fallback usesNoopAdProvider
lefthook parallel invocations — secondary effect
- Multiple SDK installs can trigger concurrent
mise execinvocations - Keep
lefthook.yml pre-commit.parallel: true(matchesapple-public-repo-security's baseline); if a specific pair of commands genuinely races, serialize those two, not the whole file
Anti-patterns
- "Just import it where you need it" — NO. Single-file isolation is the contract; multiple imports = no audit signal, no clean removal path.
- "Skip the Fake for now, we'll add it later" — NO. Unit tests must work from day one; integrating SDK without a test seam means every test becomes integration-test territory.
- "Macros + canImport are too verbose; let's drop conditional gating for v2" — NO. macOS build will break the moment a maintainer runs
swift buildon a Mac, blocking PRs. - "Production IDs in source for ease of swap" — NO. Use build-config injection (
build-time-secret-injection); afatalErrorguard is acceptable only as a transitional step before that lands. - "PrivacyInfo.xcprivacy can wait until submission" — NO. Upload-time checks only catch (a) undeclared required-reason API use, (b) a listed third-party SDK missing its manifest/signature — GoogleMobileAds and UserMessagingPlatform are not on that list — and (c) an invalid manifest (ITMS-91056), including
NSPrivacyTracking/NSPrivacyTrackingDomainsinconsistency (truewith an empty domain list,falsewith domains listed); nothing checks the declaration against the app's actual network behavior. The real cost lands later: while the user hasn't granted ATT permission, requests to the domains listed inNSPrivacyTrackingDomainsfail, so the list must be exact — don't pad it for safety; an omitted domain costs a privacy-label mismatch and a 5.1.x rejection, not a runtime failure. Update PrivacyInfo BEFORE adding the SDK. - Assuming a Swift symbol name survives a major SDK upgrade unchanged — NO. AdMob dropped the
GADprefix from its Swift API at v12.0.0 (GADBannerView→BannerView,GADRequest→Request, etc.; theGAD-prefixed spellings remain only in the Objective-C API). Check the SDK's migration guide before a major-version bump instead of assuming old names still resolve, and re-run the isolation audit (Build-time audit above) after — a migration can silently widen the import-site count past 1.
Pre-integration checklist
When proposing a new SDK, fill this in:
SDK: <name + version + GitHub URL>
Capability: <what it does that Apple-native can't>
Privacy domains: <list — must match PrivacyInfo.xcprivacy>
iOS-only / cross-platform: <iOS-only | iOS+macOS | etc.>
Bundle size impact: <KB / MB>
Tracking ATT required: <yes/no>
UMP consent required: <yes/no>
Test seam plan: <how Fake<SdkName>Bridge will look>
Isolation audit grep target count: <expected 1>
Fallback platform behaviour: <Noop / throw / etc.>
If any field is "TBD" or "?", do NOT proceed — research first.
Documentation pointers
- The project's architecture-decisions doc (
docs/foundations.mdfor consumers ofcollaboration-skills:spec-phase-orchestration) — the no-3rd-party rule + break-glass exception + isolation contract text - The project's design doc — monetization design intent
- The project's plan / readiness doc — AdMob impl phase, isolation acceptance criteria, pre-submission audit step
Sources/<AdsBridge>/<SdkName>Bridge.swift— protocol seam exampleSources/<AdsBridge>/Live<SdkName>Bridge.swift— single-import-site example<App>/Resources/PrivacyInfo.xcprivacy— tracking domains declaration
Related skills
build-time-secret-injection— SIBLING; invoke together when wiring AdMob — that skill is the secret-handling layer (xcconfig injection), this skill is the SDK isolation and testing contract.storekit2-iap-defaults— same bridge-isolation pattern for the IAP side;app-store-review-rejections— ATT / PrivacyInfo consequences of shipping AdMob (5.1.2).- Official sources: when verifying or updating a factual or version-sensitive claim, read
references/official-docs.md.
Files (apple-dev-skills)
-
references
-
official-docs.md 1.8 KB
Official pages backing this skill's claims; read when verifying or updating a factual or version-sensitive claim. | Page | URL | Backs | |---|---|---| | Set up Google Mobile Ads SDK (iOS) | https://developers.google.com/admob/ios/quick-start | SPM repo `swift-package-manager-google-mobile-ads`; `GADApplicationIdentifier` | | Migrate SDK versions (iOS) | https://developers.google.com/admob/ios/migration | Anti-pattern: v12.0.0's Swift API dropped the `GAD` prefix | | Set up UMP SDK (iOS) | https://developers.google.com/admob/ios/privacy | UMP consent flow | | Third-party SDK requirements | https://developer.apple.com/support/third-party-SDK-requirements/ | The listed Google entries are GoogleDataTransport / GoogleSignIn / GoogleToolboxForMac / GoogleUtilities only -- not GoogleMobileAds / UMP | | Privacy manifest files | https://developer.apple.com/documentation/bundleresources/privacy-manifest-files | An SDK's own manifest obligations (including tracking domains) | | NSPrivacyTrackingDomains | https://developer.apple.com/documentation/bundleresources/app-privacy-configuration/nsprivacytrackingdomains | "If the user has not granted tracking permission ... network requests to these domains fail" | | TN3181: Debugging an invalid privacy manifest | https://developer.apple.com/documentation/technotes/tn3181-debugging-invalid-privacy-manifest | Upload-time validation since 2024-11-12 (ITMS-91056); `NSPrivacyTracking=true` with empty domains (or false with domains present) is invalid | | App Tracking Transparency | https://developer.apple.com/documentation/apptrackingtransparency | Pre-integration checklist's "Tracking ATT required" | | when(platforms:) (SwiftPM 5.7+) | https://developer.apple.com/documentation/packagedescription/targetdependencycondition/when(platforms:)-5bxhc | Conditional-compile invariant: `condition: .when(platforms: [.iOS])` |
-
-
SKILL.md 10.3 KB
--- name: monetization-sdk-integration description: 'Use when adding, upgrading, or auditing a third-party monetization SDK (AdMob / Google Mobile Ads incl. UMP consent; the same contract applies to any other ad or subscription SDK), or when a PR adds `import GoogleMobileAds` outside the single live-bridge file, breaks `canImport` / `.when(platforms: [.iOS])` gating, or lands without a Fake bridge. Owns the break-glass admission test, single-import-site isolation contract, and test seam. Does not cover native StoreKit 2 IAP (storekit2-iap-defaults), production ID storage (build-time-secret-injection), or App Review consequences (app-store-review-rejections).' --- # Monetization SDK Integration ## When to invoke - Adding a new monetization SDK (AdMob, ATT, UMP, AdMob mediation networks, RevenueCat, etc.) - Upgrading existing AdMob / UMP versions (e.g. v11 → v13) - Auditing PR diff that touches the monetization target's ad-bridge sources (e.g. `Sources/<AdsBridge>/`) - Cross-platform SDK questions (iOS only? macOS catalyst? watchOS?) - Anyone proposing "let's just `import GoogleMobileAds` over here too" — IMMEDIATE invoke Skip when: changing pure values / protocols inside the monetization core target (no third-party touch). ## The contract Default rule: **no third-party SDKs** in the app. Apple-platform native APIs preferred (OSLog over Sentry, MetricKit over Firebase Crashlytics, GameKit over Steam-style backend, etc.). **Break-glass exception** is granted when ALL of: 1. The capability genuinely requires the SDK (no Apple-native alternative); e.g. AdMob banner serving has no Apple-platform equivalent 2. The SDK is shippable under the project's privacy regime (PrivacyInfo.xcprivacy supports its tracking domains) 3. The SDK's import is isolated to a SINGLE source file behind a protocol seam (see §isolation contract below) 4. The dep arrow is one-way (consumer → SDK; the SDK does not call back into the host app's code beyond delegate/callback bridges) 5. iOS-only conditional compile gating (`canImport`) — macOS / Catalyst paths must build without the SDK If any of (1)-(5) fails: deny. Reject the SDK proposal; suggest Apple-native fallback or sit it out. ## The isolation contract For every accepted SDK: ### File-layout invariant - Protocol file `<SdkName>Bridge.swift` defines the seam. Plain Swift; no SDK import. - Live impl file `Live<SdkName>Bridge.swift` is the ONLY file allowed to `import <SDKModule>`. - All other code uses `any <SdkName>Bridge` for DI. Test seam: `Fake<SdkName>Bridge` in the matching test target (e.g. `Tests/<AdsBridge>Tests/FakeAdMobBridge.swift`). Example for AdMob (currently shipped): - `Sources/<AdsBridge>/AdMobBridge.swift` — protocol (the monetization target's bridge protocol file) - `Sources/<AdsBridge>/LiveAdMobBridge.swift` — sole `import GoogleMobileAds` site - `Tests/<AdsBridge>Tests/FakeAdMobBridge.swift` — test seam ### Cross-boundary type invariant When the live bridge's output crosses into UI or other host code, it must cross as a platform-neutral type (e.g. SwiftUI's `AnyView`), never a raw SDK type — that's what keeps UI targets at zero SDK imports even though they display SDK-provided content. Example: `BannerViewProviding.bannerView(for:) -> AnyView?` returns `AnyView`, not a `GoogleMobileAds` type. ### Build-time audit (run before every monetization PR merge) Canonical regex (matches Swift 6 access-level imports too): ```bash # replace <SDKModule> / <project-sources-root> with the project's actual values rg '^(internal |private |public |@_implementationOnly |@preconcurrency )*import <SDKModule>' <project-sources-root> ``` Expected count: **1** (live bridge file). If > 1: the contract is broken. Either consolidate behind the existing bridge OR file an exception in the project's architecture-decisions doc (`docs/foundations.md` for consumers of `collaboration-skills:spec-phase-orchestration`) documenting WHY a second import site is necessary (with prior reviewer sign-off) — see Documentation pointers below. ### Conditional compile invariant iOS-only SDKs (AdMob/UMP/most monetization stack): ```swift #if canImport(GoogleMobileAds) import GoogleMobileAds // ... live impl code #else // macOS / catalyst fallback — usually NoOp returning sensible empty values #endif ``` The Package.swift dep arrow itself must also gate the SDK to iOS: ```swift .product( name: "GoogleMobileAds", package: "swift-package-manager-google-mobile-ads", condition: .when(platforms: [.iOS]) ), ``` Without `condition:`, macOS build fails on `swift build` because Google ships iOS-only xcframeworks. ### Test seam invariant A test target for the bridge ships `Fake<SdkName>Bridge` (actor or class). All unit tests inject the fake; real SDK only loaded at runtime via DI in the app's composition root. A shared testing-scaffolding target may ship shared fakes used across both ad and IAP test targets (e.g. `FakeAdProvider`, `FakeIAPClient`); the per-SDK bridge fakes live in their own test target. `Fake` must: - Be `Sendable` (Swift 6 actor or @unchecked Sendable + lock-guarded) - Have a `script(...)` or per-call setter API for deterministic test outcomes - Not import the real SDK (zero hidden dependency) ## Real-world incidents this skill encodes ### Production ID swap safety - A Release build's `bannerAdUnitID` constant first used `fatalError("REPLACE_BEFORE_RELEASE: …")` as a transitional guard; it has since been replaced by xcconfig injection (→ `build-time-secret-injection`), which is the standing solution — the `fatalError` form is not a long-term answer on its own - A paired-flip checklist ensures Info.plist `GADApplicationIdentifier` + bridge constant are always updated together ### Real banner landed + SDK-view-crossing seam - The `BannerView` SwiftUI host shipped, crossing into the UI layer per the cross-boundary type invariant above. One shared `BannerSlotView` replaced per-app placeholder slots. - ID split: `#if DEBUG` forces Google's universal test unit; Release reads the per-app prod id from `Bundle.main` via xcconfig. ### macOS conditional gating - Initial AdMob integration left `import GoogleMobileAds` ungated → macOS build broke - Fix: `canImport(GoogleMobileAds)` + Package.swift `condition: .when(platforms: [.iOS])` + macOS fallback uses `NoopAdProvider` ### lefthook parallel invocations — secondary effect - Multiple SDK installs can trigger concurrent `mise exec` invocations - Keep `lefthook.yml pre-commit.parallel: true` (matches `apple-public-repo-security`'s baseline); if a specific pair of commands genuinely races, serialize those two, not the whole file ## Anti-patterns - **"Just import it where you need it"** — NO. Single-file isolation is the contract; multiple imports = no audit signal, no clean removal path. - **"Skip the Fake for now, we'll add it later"** — NO. Unit tests must work from day one; integrating SDK without a test seam means every test becomes integration-test territory. - **"Macros + canImport are too verbose; let's drop conditional gating for v2"** — NO. macOS build will break the moment a maintainer runs `swift build` on a Mac, blocking PRs. - **"Production IDs in source for ease of swap"** — NO. Use build-config injection (`build-time-secret-injection`); a `fatalError` guard is acceptable only as a transitional step before that lands. - **"PrivacyInfo.xcprivacy can wait until submission"** — NO. Upload-time checks only catch (a) undeclared required-reason API use, (b) a listed third-party SDK missing its manifest/signature — GoogleMobileAds and UserMessagingPlatform are not on that list — and (c) an invalid manifest (ITMS-91056), including `NSPrivacyTracking` / `NSPrivacyTrackingDomains` inconsistency (`true` with an empty domain list, `false` with domains listed); nothing checks the declaration against the app's actual network behavior. The real cost lands later: while the user hasn't granted ATT permission, requests to the domains *listed* in `NSPrivacyTrackingDomains` fail, so the list must be exact — don't pad it for safety; an omitted domain costs a privacy-label mismatch and a 5.1.x rejection, not a runtime failure. Update PrivacyInfo BEFORE adding the SDK. - **Assuming a Swift symbol name survives a major SDK upgrade unchanged** — NO. AdMob dropped the `GAD` prefix from its Swift API at v12.0.0 (`GADBannerView` → `BannerView`, `GADRequest` → `Request`, etc.; the `GAD`-prefixed spellings remain only in the Objective-C API). Check the SDK's migration guide before a major-version bump instead of assuming old names still resolve, and re-run the isolation audit (Build-time audit above) after — a migration can silently widen the import-site count past 1. ## Pre-integration checklist When proposing a new SDK, fill this in: ``` SDK: <name + version + GitHub URL> Capability: <what it does that Apple-native can't> Privacy domains: <list — must match PrivacyInfo.xcprivacy> iOS-only / cross-platform: <iOS-only | iOS+macOS | etc.> Bundle size impact: <KB / MB> Tracking ATT required: <yes/no> UMP consent required: <yes/no> Test seam plan: <how Fake<SdkName>Bridge will look> Isolation audit grep target count: <expected 1> Fallback platform behaviour: <Noop / throw / etc.> ``` If any field is "TBD" or "?", do NOT proceed — research first. ## Documentation pointers - The project's architecture-decisions doc (`docs/foundations.md` for consumers of `collaboration-skills:spec-phase-orchestration`) — the no-3rd-party rule + break-glass exception + isolation contract text - The project's design doc — monetization design intent - The project's plan / readiness doc — AdMob impl phase, isolation acceptance criteria, pre-submission audit step - `Sources/<AdsBridge>/<SdkName>Bridge.swift` — protocol seam example - `Sources/<AdsBridge>/Live<SdkName>Bridge.swift` — single-import-site example - `<App>/Resources/PrivacyInfo.xcprivacy` — tracking domains declaration ## Related skills - `build-time-secret-injection` — SIBLING; invoke together when wiring AdMob — that skill is the secret-handling layer (xcconfig injection), this skill is the SDK isolation and testing contract. - `storekit2-iap-defaults` — same bridge-isolation pattern for the IAP side; `app-store-review-rejections` — ATT / PrivacyInfo consequences of shipping AdMob (5.1.2). - Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.