Add internal reward-verification adapter pipeline#6663
Merged
Conversation
📸 Snapshot Test332 unchanged
🛸 Powered by Emerge Tools |
eaae45f to
8a34a97
Compare
1256d78 to
d0216f6
Compare
8a34a97 to
4a42ff7
Compare
2 tasks
4a42ff7 to
11cc073
Compare
3112c62 to
a51bf20
Compare
24d6c90 to
2d99253
Compare
2 tasks
6de97f7 to
b26e613
Compare
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.)
bce4ba0 to
2a4d47b
Compare
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.)
c0121f8 to
5b051bb
Compare
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.)
5b051bb to
4055f9b
Compare
2a4d47b to
845296b
Compare
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.)
910f4cc to
6cfa59a
Compare
ddb2d4f to
1e1be08
Compare
Member
Author
|
@RCGitBot please test |
…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.
6e4e9da to
a756934
Compare
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.)
…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.)
Contributor
|
@RCGitBot please test |
ajpallares
approved these changes
May 6, 2026
ajpallares
left a comment
Member
There was a problem hiding this comment.
Great job on this! I think it looks good!
Contributor
|
@RCGitBot please test |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist
purchases-androidand hybridsMotivation
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 toAdapterSDKs/RevenueCatAdMob/and areinternalto theRevenueCatAdMobtarget.Description
A set of leaf utilities nested under a
RewardVerificationnamespace (mirroring theTrackingnamespace 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-adclient_transaction_id, builds thecustomRewardTextJSON, and setsServerSideVerificationOptionson the loaded ad. Gated by aCapableAdprotocol so onlyRewardedAd/RewardedInterstitialAdare accepted.RewardVerification.Poller— bounded async retry loop. Returns.verified(VerifiedReward)/.failed(non-throwing). Retries onpending/unknownand transient network errors; task-cancellation check short-circuits each iteration.RewardVerification.State— per-ad correlation object holding theclient_transaction_idand 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 theOutcomevia the one-shot token. Cancellation preserves the token.AssociatedObjectStorelifted to module top level, shared betweenTrackingandRewardVerification.Out of scope (deferred to follow-up)
@objc/@_spi(Public)entry points)userDidEarnRewardHandlerpresent(...)overloads and GMA→VerifiedRewardbridgeTesting
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
RewardVerificationsubsystem for the AdMob adapter, including load-time SSV setup (Setup.install) that generates aclient_transaction_id, encodes it into deterministic JSONcustomRewardText, and attachesServerSideVerificationOptionsto rewarded ad types while stashing per-ad state via associated objects.Introduces an async
Poller+Dispatcherpipeline that repeatedly callsPurchases.shared.pollRewardVerificationStatus(...)with bounded retries (pending/unknown + transient networkErrorCodes), then delivers a single terminalOutcomeon the main actor using a one-shot guard inState(with cancellation preserving the guard).Refactors
AssociatedObjectStoreto a module-level utility shared by bothTrackingandRewardVerification, 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.