Skip to content

Add internal reward-verification adapter pipeline#6663

Merged
peterporfy merged 20 commits into
mainfrom
admob-ssv-adapter-pipeline
May 6, 2026
Merged

Add internal reward-verification adapter pipeline#6663
peterporfy merged 20 commits into
mainfrom
admob-ssv-adapter-pipeline

Conversation

@polmiro

@polmiro polmiro commented Apr 21, 2026

Copy link
Copy Markdown
Member

Checklist

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

Motivation

Builds the adapter-internal half of the AdMob reward-verification pipeline on top of the @_spi(Internal) Purchases.pollRewardVerificationStatus(...) primitive added in #6678/#6667. All changes are scoped to AdapterSDKs/RevenueCatAdMob/ and are internal to the RevenueCatAdMob target.

Description

A set of leaf utilities nested under a RewardVerification namespace (mirroring the Tracking namespace from #6681), each independently testable. The next PR will expose a public opt-in entry point on top of these.

  • RewardVerification.Setup — generates a per-ad client_transaction_id, builds the customRewardText JSON, and sets ServerSideVerificationOptions on the loaded ad. Gated by a CapableAd protocol so only RewardedAd / RewardedInterstitialAd are accepted.
  • RewardVerification.Poller — bounded async retry loop. Returns .verified(VerifiedReward) / .failed (non-throwing). Retries on pending/unknown and transient network errors; task-cancellation check short-circuits each iteration.
  • RewardVerification.State — per-ad correlation object holding the client_transaction_id and a one-shot fire token (ensures the outcome is delivered at most once per ad).
  • RewardVerification.Dispatcher — drives the poller, hops to the main actor, and delivers the Outcome via the one-shot token. Cancellation preserves the token.
  • AssociatedObjectStore lifted to module top level, shared between Tracking and RewardVerification.

Out of scope (deferred to follow-up)

  • Public API surface (@objc / @_spi(Public) entry points)
  • Reward-time orchestrator wrapping userDidEarnRewardHandler
  • present(...) overloads and GMA→VerifiedReward bridge

Testing

149 adapter unit tests covering poll-status/outcome mapping, transient vs terminal error partitioning, cancellation semantics, one-shot dispatch guard, associated-object lifetime, and load-time setup payload. Full suite passes on iOS Simulator. SwiftLint clean. No public API changes.


Note

Medium Risk
Adds new async polling and dispatch logic around reward verification status (including retries, jitter, and cancellation semantics) and wires SSV options onto ads; while internal-only, concurrency and backend polling behavior changes are moderately risk-prone.

Overview
Adds a new internal RewardVerification subsystem for the AdMob adapter, including load-time SSV setup (Setup.install) that generates a client_transaction_id, encodes it into deterministic JSON customRewardText, and attaches ServerSideVerificationOptions to rewarded ad types while stashing per-ad state via associated objects.

Introduces an async Poller + Dispatcher pipeline that repeatedly calls Purchases.shared.pollRewardVerificationStatus(...) with bounded retries (pending/unknown + transient network ErrorCodes), then delivers a single terminal Outcome on the main actor using a one-shot guard in State (with cancellation preserving the guard).

Refactors AssociatedObjectStore to a module-level utility shared by both Tracking and RewardVerification, and adds comprehensive unit tests covering store lifetime semantics, setup payload wiring, polling behavior, and dispatch one-shot/cancellation guarantees.

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

@emerge-tools

emerge-tools Bot commented Apr 21, 2026

Copy link
Copy Markdown

📸 Snapshot Test

332 unchanged

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

🛸 Powered by Emerge Tools

@polmiro polmiro changed the title AdMob SSV: add internal adapter pipeline (load-time setup + reward-time runner) AdMob SSV: add internal adapter pipeline Apr 22, 2026
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch 2 times, most recently from eaae45f to 8a34a97 Compare April 22, 2026 09:05
@polmiro polmiro force-pushed the admob-ssv-poll-endpoint branch from 1256d78 to d0216f6 Compare April 22, 2026 10:49
Base automatically changed from admob-ssv-poll-endpoint to main April 22, 2026 13:55
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from 8a34a97 to 4a42ff7 Compare April 22, 2026 15:07
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from 4a42ff7 to 11cc073 Compare April 22, 2026 21:50
@polmiro polmiro changed the title AdMob SSV: add internal adapter pipeline AdMob: add internal reward-verification adapter pipeline Apr 22, 2026
@polmiro polmiro changed the base branch from main to rewardverification-rename-merged April 22, 2026 21:52
@polmiro polmiro force-pushed the rewardverification-rename-merged branch from 3112c62 to a51bf20 Compare April 23, 2026 07:07
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from 24d6c90 to 2d99253 Compare April 23, 2026 07:08
@polmiro polmiro changed the title AdMob: add internal reward-verification adapter pipeline Add internal reward-verification adapter pipeline Apr 23, 2026
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from 6de97f7 to b26e613 Compare April 23, 2026 10:43
@polmiro polmiro changed the base branch from rewardverification-rename-merged to rewardverification-poll-reward-payload April 23, 2026 10:43
polmiro added a commit that referenced this pull request Apr 23, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from bce4ba0 to 2a4d47b Compare April 23, 2026 10:56
polmiro added a commit that referenced this pull request Apr 23, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
@polmiro polmiro force-pushed the rewardverification-poll-reward-payload branch from c0121f8 to 5b051bb Compare April 23, 2026 11:22
polmiro added a commit that referenced this pull request Apr 23, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
@polmiro polmiro force-pushed the rewardverification-poll-reward-payload branch from 5b051bb to 4055f9b Compare April 23, 2026 12:08
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from 2a4d47b to 845296b Compare April 23, 2026 13:33
@polmiro polmiro changed the base branch from rewardverification-poll-reward-payload to admob-tracking-namespace-enum April 23, 2026 13:33
polmiro added a commit that referenced this pull request Apr 24, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
@polmiro polmiro force-pushed the admob-tracking-namespace-enum branch from 910f4cc to 6cfa59a Compare April 24, 2026 10:16
@polmiro polmiro force-pushed the admob-ssv-adapter-pipeline branch from ddb2d4f to 1e1be08 Compare April 24, 2026 10:47
@polmiro

polmiro commented Apr 24, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

polmiro added 4 commits May 5, 2026 15:25
…State

`AssociatedObjectStore<Value>` is a generic primitive over Obj-C
associated objects with no knowledge of either tracking or
reward-verification. Having `RewardVerification` reach into the
`Tracking` namespace just to use it inverted the dependency between
two peer subsystems.

- Move the class out of `extension Tracking` into a top-level file
  `AssociatedObjectStore.swift`.
- The four tracking-specific delegate-store typealiases stay inside
  `Tracking`; renamed the file to `Tracking/DelegateStores.swift` to
  reflect what it now contains.
- `RewardVerification.StateStore.swift` (a 2-line typealias + singleton)
  is folded into `RewardVerification.State.swift` next to the State
  class it stashes; both files become one.

No behavior changes; 143/143 adapter tests still pass.
…Dispatcher

Poller.run now throws Outcome and only retries on a small allowlist of
transient ErrorCode cases (networkError, offlineConnectionError,
unknownBackendError). Terminal ErrorCode cases surface as .failed
without retrying. CancellationError and unrecognized error types
propagate to Dispatcher, which catches CancellationError (preserving
the one-shot fire token) and uses an assertionFailure + .failed safety
net for unexpected throws so the consumer's UI is never left hanging.

Removes PollerResult; Outcome no longer needs Equatable.
Collapse Poller.run to `async -> Outcome` (was `async throws -> Outcome`):
sleeper failures are swallowed via `try?`, terminal/unknown errors fall into
a generic `catch` that returns `.failed`, and a `Task.isCancelled` check at
the top of every iteration exits the loop without further polling.

Dispatcher loses its do/catch safety net and gains a single
`if Task.isCancelled { return }` guard before the MainActor hop, so a
cancelled task stops polling fast, delivers nothing, and preserves
`state.consumeFireToken()` for a later attempt.

Also trim verbose docs across the RewardVerification subsystem to match
the rest of the codebase.

Tests: PollerTests/DispatcherTests updated for the non-throwing API,
synthetic-CancellationError tests replaced with `.failed` assertions, and a
new test pins the cancellation short-circuit (0 polls when cancelled
before the first attempt). 38/38 green.
@peterporfy peterporfy force-pushed the admob-ssv-adapter-pipeline branch from 6e4e9da to a756934 Compare May 5, 2026 13:26
peterporfy pushed a commit that referenced this pull request May 5, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
peterporfy and others added 2 commits May 5, 2026 15:44
…fication/Setup.swift

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
…fication/Dispatcher.swift

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
peterporfy pushed a commit that referenced this pull request May 6, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
@peterporfy

Copy link
Copy Markdown
Contributor

@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.

Great job on this! I think it looks good!

@peterporfy

Copy link
Copy Markdown
Contributor

@RCGitBot please test

@peterporfy peterporfy merged commit c908f21 into main May 6, 2026
42 checks passed
@peterporfy peterporfy deleted the admob-ssv-adapter-pipeline branch May 6, 2026 17:58
polmiro added a commit that referenced this pull request May 7, 2026
Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)
polmiro added a commit that referenced this pull request May 8, 2026
* Add unit tests for VerifiedReward and VirtualCurrencyReward

Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)

* fix: warn when reward verification reward value is not a JSON object

Previously, `decodeVerifiedReward` silently returned `.noReward` for any
of: missing reward key, null reward, or non-object reward value (e.g. a
string/number/array). The first two are expected; the last indicates a
backend mismatch and should be surfaced.

Now distinguish:
- absent or null reward → `.noReward` (silent, expected)
- present but not a JSON object → `.unsupportedReward` + `Logger.warn`

This matches the existing convention in this file of logging warnings
when decoding unrecognized/malformed values into fallback cases.

* feat(AdMob): add enableRewardVerification for rewarded ads

Expose enableRewardVerification() on RewardedAd and RewardedInterstitialAd
as a thin public entry point over existing reward-verification setup.
Add APISurfaceTests coverage for the new symbols.

* WIP(AdMob): reward verification present + public outcome structs

- Add struct-based ValidatedReward and RewardVerificationOutcome (no public enums)
- Wire Present helper, map internal Outcome to public API, precondition outcome requires enable
- Expose present(from:placement:rewardVerificationStarted:rewardVerificationOutcome:) on rewarded types
- Add Setup.verificationState, mapping/present tests, APISurface coverage

* refactor(AdMob): use verified naming for public reward verification API

- Rename ValidatedReward to VerifiedReward (qualify RevenueCat.VerifiedReward internally)
- RewardVerificationOutcome: validated -> verified, verifiedReward, isVerified
- Rename mapValidatedReward to mapVerifiedReward

* refactor(AdMob): remove redundant amount check in verified reward mapping

Amount validity is enforced when decoding the poll response; map virtual
currency rewards directly without re-checking > 0.

* refactor(AdMob): clarify reward verification types and file layout

- Replace Public* filenames with VerifiedReward.swift and RewardVerificationResult.swift
- Expose presentation callback as rewardVerificationResult delivering RewardVerificationResult
- Fold internal→adapter mapping into Present.swift; remove OutcomeMapping.swift
- Rename PublicOutcomeMappingTests to PresentMappingTests; update API surface tests

* fix: remove duplicate reward verification status decoding test

Drop the legacy copy that used .verified without a payload and
verifiedReward; the remaining test matches Status.verified(VerifiedReward).

* fix(AdMob): repair RevenueCatAdMobTests for Experimental SPI and throws

- Import @_spi(Experimental) with @testable RevenueCatAdMob in mapping/present tests
- Mark present verification tests throws for try XCTUnwrap (Swift 6 / Xcode 26)

* fix: address review — @mainactor on rewardVerificationResult callbacks

Match Dispatcher main-actor delivery for the verification result closure
in Present and public RewardedAd present overloads.

* fix: preserve reward verification load-time placement

Split reward-verification present overloads so placement override is explicit and add regression tests proving load-time placement is preserved unless explicitly overridden.

* chore: rename retain to set

* fix: address review — avoid production crash on verification misuse

Use an assert instead of a precondition when rewardVerificationResult is provided without verification setup so debug builds still catch misuse while release builds follow the existing graceful fallback path.

* fix: address review — simplify verification result guards

Combine verification state and result callback checks into one guard in the present helper while preserving the same fallback behavior.

* fix: address review — move present helper near rewarded APIs

Remove the RewardVerification.Present namespace and expose a private-style helper on CapableAd that builds the userDidEarnRewardHandler closure, keeping behavior and test coverage intact while simplifying the flow.

* fix: address review — remove internal detail from public docs

Trim RewardVerificationResult documentation to public-facing behavior only by removing internal pipeline implementation details.

* fix: address review — expose nested virtual currency payload

Refactor VerifiedReward to expose a nested VirtualCurrencyReward projection and update mapping/presentation tests to use the grouped payload accessors.

* fix: address review — rename no-reward sentinel

Rename VerifiedReward.none to noReward to avoid Optional.none ambiguity and keep mapping consistent.

* fix: address review — remove redundant reward state helpers

Drop isUnknown and isNone from VerifiedReward and rely on Equatable comparisons in tests and call sites.

* fix: address review — align docs with assert behavior

Update reward verification API docs to describe debug-assert validation instead of runtime precondition enforcement.

* fix: address review — tighten public API documentation wording

Simplify reward verification public API comments to match existing SDK tone and reduce implementation-specific phrasing.

* test: strengthen reward verification callback/result coverage

Add coverage for started-callback ordering and direct RewardVerificationResult projection/equality behavior.

* fix: address review — simplify placement override handling

Apply show-time placement overrides only in overloads that accept placement and remove the extra resolver type while preserving keep/override/clear behavior via API shape.

* fix: log reward verification callback misuse

Warn when rewardVerificationResult is provided without installed verification state so release builds surface this integration misuse.

* fix: qualify reward verification string references

Use RewardVerification.Strings explicitly in rewarded presentation flow and refine the misuse warning text for clearer diagnostics.

* fix: use LogMessage type for verification misuse warning

Wrap reward verification misuse warning text in a LogMessage value so Logger.warn compiles in iOS test builds.

* test: add assertion coverage for reward verification safeguards

Adopt Nimble assertion tests in adapter tests and cover both invalid mapped reward amounts and callback misuse assertions in reward verification flow.

* fix: harden unsupported reward fallback behavior

Move invalid virtual-currency handling into VerifiedReward with assertion + fallback, and centralize the assertion string while simplifying mapping logic.

* fix: rename unknown reward to unsupportedReward

Make unsupportedReward the canonical public representation and remove the transient unknown naming from the adapter API surface.

* fix: return failed result when verification state is missing

Simplify handler control flow by gating state checks on a non-nil result callback and invoke `.failed` when state is unexpectedly missing so production callers receive a terminal outcome.

* refactor: simplify reward verification handler closure flow

Unify the reward-earned handler into a single closure path that invokes the start callback once and handles missing verification state with a failed result while preserving debug assertions and logging.

* refactor: inline reward mapping helpers into tracking extension

Move RewardVerification mapping helpers next to their only call site and remove the now-redundant Present.swift file.

* refactor: inline reward placement override assignment

Remove the dedicated placement-override helper and assign the tracked placement directly at the show-time call sites to keep the flow simpler with unchanged behavior.

* test: remove non-behavioral placement round-trip case

Drop the test that only re-validates delegate store round-tripping so the suite stays focused on show-time placement override behavior.

* refactor: move reward verification ad extensions to feature module

Relocate rewarded ad reward-verification extensions from Tracking into RewardVerification and keep placement wiring in Tracking through a small show-time placement bridge.

* fix: log invalid virtual currency reward amounts

Emit a production error log before asserting when reward verification receives a non-positive virtual currency amount, while still falling back to unsupportedReward.

* fix: log reward callback misuse before assert

Emit the warning before the debug assert so callback misuse diagnostics are visible in all build configurations.

* fix: address review — remove non-behavioral placement test

Drop the test that only validates delegate-store round-tripping and keep coverage focused on explicit nil placement override behavior.

* fix: address review — trim no-op API surface assertions

Remove not-nil assertions that do not add value while keeping the API symbol references in place for signature coverage.

* fix: address review — annotate started callback with MainActor

Mark rewardVerificationStarted callbacks as MainActor in public present overloads and helper signatures to make callback threading guarantees explicit and consistent.

* fix: address review — document reward callback timing

Clarify when rewardVerificationStarted and rewardVerificationResult callbacks execute in all reward verification present overloads.

* fix: refine reward verification callback timing docs

Clarify that verification starts when AdMob invokes the reward callback while keeping result timing wording implementation-agnostic.

* fix: strengthen MainActor API surface coverage

Update API surface method reference annotations so tests fail if MainActor is removed from rewardVerificationStarted callback parameters.

---------

Co-authored-by: Peter Porfy <peter.porfy@revenuecat.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.

3 participants