Skip to content

AdMob SSV: add @_spi(Internal) poll endpoint on Purchases#6641

Merged
polmiro merged 29 commits into
mainfrom
admob-ssv-poll-endpoint
Apr 22, 2026
Merged

AdMob SSV: add @_spi(Internal) poll endpoint on Purchases#6641
polmiro merged 29 commits into
mainfrom
admob-ssv-poll-endpoint

Conversation

@polmiro

@polmiro polmiro commented Apr 20, 2026

Copy link
Copy Markdown
Member

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-android and hybrids

Motivation

Adapter modules that import the SDK with @_spi(Internal) import RevenueCat (starting with RevenueCatAdMob) need a way to ask the backend whether AdMob's SSV postback for a given rewarded ad has been validated, is still pending, or was rejected.

This PR adds the SDK-side polling primitive used by adapters after AdMob's userDidEarnRewardHandler fires.

Description

Internal SPI surface (@_spi(Internal)):

  • Purchases.pollAdMobSSVStatus(clientTransactionID:) async throws -> AdMobSSVPollStatus
  • AdMobSSVPollStatus enum cases: .validated, .pending, .failed, .unknown
  • Cancelling the calling Task does not cancel an in-flight HTTP request

Backend/networking additions:

  • New AdsAPI entry point for ad-related backend calls
  • New HTTPRequest.Path.adMobSSVStatus(appUserID:clientTransactionID:)
  • GetAdMobSSVStatusOperation implemented as CacheableNetworkOperation to dedupe concurrent identical polls
  • Cache key uses appUserID + "\n" + clientTransactionID to avoid collisions
  • shouldSendEtag = false for this polling endpoint so status transitions are not hidden by 304 responses

Response behavior:

  • Unknown future backend status values decode to .unknown
  • SPI surfaces .unknown to adapters so adapter-level polling policy can decide how to handle it

Testing

  • Added BackendGetAdMobSSVStatusTests coverage for status decoding, HTTP wiring, errors, empty user ID handling, concurrent dedupe, and sequential reissue behavior
  • Added PurchasesAdMobSSVTests coverage for Purchases.pollAdMobSSVStatus status mapping and error forwarding
  • Snapshot baselines are now committed for this test suite across CI-generated platforms (iOS 16/17/18/26, tvOS, macOS, watchOS)

Note

Medium Risk
Touches core networking (Backend, HTTPRequest.Path, shared error types) and introduces a new signed/verified backend endpoint, so regressions could affect request routing/caching behavior despite good test coverage.

Overview
Adds an internal SPI polling primitive for AdMob SSV verification: Purchases.pollAdMobSSVStatus(clientTransactionID:) returning AdMobSSVPollStatus (validated/pending/failed/unknown).

Implements a new ads backend surface (AdsAPI) and GET /v1/subscribers/{app_user_id}/ads/admob/ssv/{client_transaction_id} wiring via HTTPRequest.Path.adMobSSVStatus and GetAdMobSSVStatusOperation, including deduping concurrent identical polls and graceful decoding/logging of unknown future status values.

Extends error handling with BackendError.missingClientTransactionID and adds unit tests + snapshot baselines covering request formation, decoding, error cases, and dedupe behavior (plus MockAdsAPI and PurchasesAdMobSSVTests).

Reviewed by Cursor Bugbot for commit d0216f6. Bugbot is set up for automated code reviews on this repo. Configure here.

@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch 2 times, most recently from c865a7e to bbef629 Compare April 20, 2026 17:15
@polmiro polmiro force-pushed the followup-systeminfo-apikey-plumbing branch from da15696 to be5f20b Compare April 20, 2026 17:29
Base automatically changed from followup-systeminfo-apikey-plumbing to expose-api-key-spi April 20, 2026 17:37
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch from bbef629 to ec05f64 Compare April 20, 2026 17:48
Base automatically changed from expose-api-key-spi to main April 21, 2026 06:14
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch 4 times, most recently from 8799eb5 to 167727f Compare April 21, 2026 08:51
@polmiro polmiro marked this pull request as ready for review April 21, 2026 09:00
@polmiro polmiro requested a review from a team as a code owner April 21, 2026 09:00
@RevenueCat-Danger-Bot

RevenueCat-Danger-Bot commented Apr 21, 2026

Copy link
Copy Markdown
2 Warnings
⚠️ This PR increases the size of the repo by more than 100.00 KB (increased by 135.81 KB).
⚠️ RevenueCat.xcodeproj is out of sync.

The following Swift files were added but are missing from RevenueCat.xcodeproj:
Tests/UnitTests/Purchasing/Purchases/PurchasesAdMobSSVTests.swift

To fix: open RevenueCat.xcodeproj in Xcode, add/remove the files above in the appropriate target. Check where similar files in the same directory are assigned if you're unsure which target to use.

Generated by 🚫 Danger

Comment thread Sources/Ads/AdMobSSV/Networking/GetAdMobSSVStatusOperation.swift
@polmiro

polmiro commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@emerge-tools

emerge-tools Bot commented Apr 21, 2026

Copy link
Copy Markdown

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 17.7 MB ⬆️ 25.6 kB (0.15%) 63.5 MB ⬆️ 109.3 kB (0.17%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.0 MB ⬆️ 9.7 kB (0.24%) 12.1 MB ⬆️ 24.5 kB (0.21%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.1 MB ⬆️ 14.2 kB (0.24%) 26.7 MB ⬆️ 58.0 kB (0.22%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.1 MB ⬆️ 9.6 kB (0.23%) 10.5 MB ⬆️ 19.8 kB (0.19%) N/A

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 109.3 kB (0.17%)
Total download size change: ⬆️ 25.6 kB (0.15%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 37.6 kB
DYLD.Exports ⬆️ 2.7 kB
Code Signature ⬆️ 2.4 kB
📝 RevenueCat.AdsAPI.AdsAPI ⬆️ 1.3 kB
RevenueCat.PostSubscriberAttributesOperation.PostSubscriberAttrib... ⬇️ -628 B
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 24.5 kB (0.21%)
Total download size change: ⬆️ 9.7 kB (0.24%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 3.4 kB
📝 RevenueCat.GetAdMobSSVStatusOperation.getAdMobSSVStatus(completio... ⬆️ 2.1 kB
RevenueCat.Result.parseResponse ⬆️ 896 B
Code Signature ⬆️ 624 B
📝 RevenueCat.AdsAPI.AdsAPI ⬆️ 608 B
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 58.0 kB (0.22%)
Total download size change: ⬆️ 14.2 kB (0.24%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 25.6 kB
📝 RevenueCat.GetAdMobSSVStatusOperation.getAdMobSSVStatus(completio... ⬆️ 2.1 kB
Code Signature ⬆️ 1.4 kB
RevenueCat.Result.parseResponse ⬆️ 896 B
📝 RevenueCat.AdsAPI.AdsAPI ⬆️ 608 B
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 19.8 kB (0.19%)
Total download size change: ⬆️ 9.6 kB (0.23%)

Largest size changes

Item Install Size Change
📝 RevenueCat.GetAdMobSSVStatusOperation.getAdMobSSVStatus(completio... ⬆️ 2.1 kB
RevenueCat.Result.parseResponse ⬆️ 896 B
📝 RevenueCat.AdsAPI.AdsAPI ⬆️ 608 B
Swift._NativeDictionary.mutatingFind(isUnique) ⬇️ -588 B
Other ⬆️ 16.8 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Comment trigger: Size diff threshold of 100.00kB exceeded

@polmiro

polmiro commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@polmiro polmiro requested review from a team and ajpallares April 21, 2026 10:40
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch from c0c80de to 4ad4633 Compare April 21, 2026 10:42
@emerge-tools

emerge-tools Bot commented Apr 21, 2026

Copy link
Copy Markdown

📸 Snapshot Test

327 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
RevenueCat
com.revenuecat.PaywallsTester
0 0 0 0 264 0 N/A
PaywallsTester V1 swift-snapshot-testing
com.revenuecat.PaywallsTester.v1-snapshots
0 0 0 0 63 0 N/A

🛸 Powered by Emerge Tools

Comment thread Sources/Networking/HTTPClient/HTTPRequestPath.swift Outdated
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch from 2e2c1f7 to b45dfb4 Compare April 21, 2026 10:57
Comment thread rewarded-ads-ssv-api-specs/README.md Outdated
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch from b45dfb4 to 4ad4633 Compare April 21, 2026 11:02
@polmiro

polmiro commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@ajpallares ajpallares left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! I left some comments for now

Comment thread Sources/Purchasing/Purchases/Purchases.swift
Comment thread Tests/UnitTests/Mocks/MockAdsAPI.swift Outdated
Comment thread Sources/Purchasing/Purchases/Purchases.swift Outdated
Comment thread Sources/Ads/AdMobSSV/AdMobSSVPollStatus.swift Outdated
Comment thread Sources/Ads/AdMobSSV/Networking/AdMobSSVStatusResponse.swift
Comment thread Sources/Ads/AdMobSSV/Networking/AdMobSSVStatusResponse.swift Outdated
Comment thread Sources/Purchasing/Purchases/Purchases.swift
Comment thread Sources/Error Handling/BackendError.swift Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f9a6cc2. Configure here.

Comment thread Sources/Purchasing/Purchases/Purchases.swift Outdated
Comment thread Sources/Error Handling/BackendError.swift Outdated
@polmiro

polmiro commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@polmiro polmiro merged commit abad547 into main Apr 22, 2026
41 checks passed
@polmiro polmiro deleted the admob-ssv-poll-endpoint branch April 22, 2026 13:55
polmiro added a commit that referenced this pull request Apr 22, 2026
Renames the internal AdMobSSV* types and SPI introduced in #6641 to
RewardVerification* so the wording stays adapter-agnostic. Also renames
the backend URL path and the client-side metric to match, so the SPI is
provider-agnostic end to end.

Renames:
- Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/
- AdMobSSVPollStatus -> RewardVerificationPollStatus
- RewardVerificationPollStatus.validated -> .verified
- AdMobSSVStatusResponse -> RewardVerificationStatusResponse
- RewardVerificationStatusResponse.Status.validated -> .verified
  (the decoder still accepts the legacy "validated" wire value during
  the rollout window; covered by a new decoder unit test)
- AdMobSSVStatusCallback -> RewardVerificationStatusCallback
- GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation
- AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus
- Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus
- HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus
- URL path: /ads/admob/ssv/{tx} -> /ads/reward_verification/{tx}
- Metric name: get_admob_ssv_status -> get_reward_verification_status
- BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status
- Test files, test methods and snapshot artifacts renamed to match.
- Xcode project file references updated in lockstep.
polmiro added a commit that referenced this pull request Apr 22, 2026
Renames the internal AdMobSSV* types and SPI introduced in #6641 to
RewardVerification* so the wording stays adapter-agnostic. Also renames
the backend URL path and the client-side metric to match, so the SPI is
provider-agnostic end to end.

Renames:
- Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/
- AdMobSSVPollStatus -> RewardVerificationPollStatus
- RewardVerificationPollStatus.validated -> .verified
- AdMobSSVStatusResponse -> RewardVerificationStatusResponse
- RewardVerificationStatusResponse.Status.validated -> .verified
  (the decoder still accepts the legacy "validated" wire value during
  the rollout window; covered by a new decoder unit test)
- AdMobSSVStatusCallback -> RewardVerificationStatusCallback
- GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation
- AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus
- Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus
- HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus
- URL path: /ads/admob/ssv/{tx} -> /ads/reward_verifications/{tx}
- Metric name: get_admob_ssv_status -> get_reward_verification_status
- BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status
- Test files, test methods and snapshot artifacts renamed to match.
- Xcode project file references updated in lockstep.
polmiro added a commit that referenced this pull request Apr 23, 2026
Renames the internal AdMobSSV* types and SPI introduced in #6641 to
RewardVerification* so the wording stays adapter-agnostic. Also renames
the backend URL path and the client-side metric to match, so the SPI is
provider-agnostic end to end.

Renames:
- Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/
- AdMobSSVPollStatus -> RewardVerificationPollStatus
- RewardVerificationPollStatus.validated -> .verified
- AdMobSSVStatusResponse -> RewardVerificationStatusResponse
- RewardVerificationStatusResponse.Status.validated -> .verified
  (the decoder still accepts the legacy "validated" wire value during
  the rollout window; covered by a new decoder unit test)
- AdMobSSVStatusCallback -> RewardVerificationStatusCallback
- GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation
- AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus
- Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus
- HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus
- URL path: /ads/admob/ssv/{tx} -> /ads/reward_verifications/{tx}
- Metric name: get_admob_ssv_status -> get_reward_verification_status
- BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status
- Test files, test methods and snapshot artifacts renamed to match.
- Xcode project file references updated in lockstep.
This was referenced Apr 23, 2026
polmiro added a commit that referenced this pull request Apr 23, 2026
)

* Rename internal AdMobSSV symbols, URL and metric to RewardVerification

Renames the internal AdMobSSV* types and SPI introduced in #6641 to
RewardVerification* so the wording stays adapter-agnostic. Also renames
the backend URL path and the client-side metric to match, so the SPI is
provider-agnostic end to end.

Renames:
- Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/
- AdMobSSVPollStatus -> RewardVerificationPollStatus
- RewardVerificationPollStatus.validated -> .verified
- AdMobSSVStatusResponse -> RewardVerificationStatusResponse
- RewardVerificationStatusResponse.Status.validated -> .verified
  (the decoder still accepts the legacy "validated" wire value during
  the rollout window; covered by a new decoder unit test)
- AdMobSSVStatusCallback -> RewardVerificationStatusCallback
- GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation
- AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus
- Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus
- HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus
- URL path: /ads/admob/ssv/{tx} -> /ads/reward_verifications/{tx}
- Metric name: get_admob_ssv_status -> get_reward_verification_status
- BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status
- Test files, test methods and snapshot artifacts renamed to match.
- Xcode project file references updated in lockstep.

* Drop legacy `validated` wire-value compat in RewardVerification decoder

The `RewardVerificationStatusResponse` decoder was accepting both `"verified"`
and the legacy `"validated"` status from the backend as a backwards-compat
hedge during rollout. The endpoint has not been shipped and the SDK has no
public entry point that polls it yet, so there is no compatibility window to
preserve — the decoder can require `"verified"` outright.

While here, simplify the decoder to look up known cases via
`Status(rawValue:)` instead of a string-literal switch, so adding a future
case can't silently drift from the enum.

Removes the corresponding `testDecodesLegacyValidatedWireValueAsVerified`
test. Existing coverage for `verified`, `pending`, `failed`, and the
`unknown` fallback (with the `Logger.warn` assertion) is unchanged.

* Update Sources/Purchasing/Purchases/Purchases.swift

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>

* test: assert warning is logged when decoding literal "unknown" status

Address review feedback: cover the case where the backend sends the
literal "unknown" wire value, which still falls through the
unmapped-status branch and should emit a warning.

---------

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants