Decode reward payload in RewardVerification poll response#6678
Merged
Conversation
Member
Author
|
@RCGitBot please test |
polmiro
added a commit
that referenced
this pull request
Apr 23, 2026
Post-rebase fixup. The base PR (#6678) changed `RewardVerificationPollStatus.verified` from a payload-less case to `.verified(VerifiedReward)`. The adapter doesn't yet propagate the poll-time reward through the pipeline (today the dispatcher attaches the present-time reward to the outcome), so this commit just keeps the adapter compiling against the new shape: - `RewardVerificationPoller` switches on `status` and ignores the associated `VerifiedReward` payload, with a comment marking the follow-up to wire the poll-time payload through. - Test stubs that constructed `[.verified]` literals now construct `[.verified(.noReward)]` since the polled payload is currently unused downstream. No behavior change. Plumbing the poll-time reward through to `RewardVerificationOutcome` (and dropping the now-redundant `verifiedReward:` parameter from the dispatcher) is deliberately left for a follow-up commit / PR.
polmiro
added a commit
that referenced
this pull request
Apr 23, 2026
`Sources/Ads/RewardVerification/VerifiedReward.swift` and `VirtualCurrencyReward.swift` are introduced (with the more complete doc comments) by the base PR #6678 (`rewardverification-poll-reward-payload`). During the rebase conflict resolution for the "Decouple reward-verification outcome from GoogleMobileAds.AdReward" commit, the wrong side of the add/add conflict was taken, reverting the file contents to PR B's earlier sparser docs. Restore both files to base's content so PR B's diff against base for these two source files is now net-zero — base owns them, PR B doesn't touch them. API and behavior identical (the only difference between the two versions was the doc comments and the `Created by` date). The PR B-only test files `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift` and `VirtualCurrencyRewardTests.swift` stay; they remain valid against base's source files (same API).
2 tasks
polmiro
added a commit
that referenced
this pull request
Apr 23, 2026
Post-rebase fixup. The base PR (#6678) changed `RewardVerificationPollStatus.verified` from a payload-less case to `.verified(VerifiedReward)`. The adapter doesn't yet propagate the poll-time reward through the pipeline (today the dispatcher attaches the present-time reward to the outcome), so this commit just keeps the adapter compiling against the new shape: - `RewardVerificationPoller` switches on `status` and ignores the associated `VerifiedReward` payload, with a comment marking the follow-up to wire the poll-time payload through. - Test stubs that constructed `[.verified]` literals now construct `[.verified(.noReward)]` since the polled payload is currently unused downstream. No behavior change. Plumbing the poll-time reward through to `RewardVerificationOutcome` (and dropping the now-redundant `verifiedReward:` parameter from the dispatcher) is deliberately left for a follow-up commit / PR.
polmiro
added a commit
that referenced
this pull request
Apr 23, 2026
`Sources/Ads/RewardVerification/VerifiedReward.swift` and `VirtualCurrencyReward.swift` are introduced (with the more complete doc comments) by the base PR #6678 (`rewardverification-poll-reward-payload`). During the rebase conflict resolution for the "Decouple reward-verification outcome from GoogleMobileAds.AdReward" commit, the wrong side of the add/add conflict was taken, reverting the file contents to PR B's earlier sparser docs. Restore both files to base's content so PR B's diff against base for these two source files is now net-zero — base owns them, PR B doesn't touch them. API and behavior identical (the only difference between the two versions was the doc comments and the `Created by` date). The PR B-only test files `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift` and `VirtualCurrencyRewardTests.swift` stay; they remain valid against base's source files (same API).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 6f2d6df. Configure here.
c0121f8 to
5b051bb
Compare
5b051bb to
4055f9b
Compare
polmiro
commented
Apr 23, 2026
Member
Author
|
@RCGitBot please test |
polmiro
added a commit
that referenced
this pull request
Apr 23, 2026
PR #6678 made `RewardVerificationPollStatus.verified` carry the granted `VerifiedReward`. The adapter pipeline was discarding that payload and accepting a parallel `verifiedReward:` argument on the dispatcher as a placeholder for a future present-time `GoogleMobileAds.AdReward` translation. Forward the polled reward end-to-end: - `PollResult.verified` now carries `VerifiedReward`; `Poller.run` extracts and forwards it. - `Outcome.init(pollResult:verifiedReward:)` collapses to `init(pollResult:)` sourcing the reward from the verified case. - `Dispatcher.run`/`dispatch` drop the `verifiedReward:` parameter. Tests updated to encode the verified reward via the stub poll status, and `PollerTests` verified-result matches tightened to assert the propagated payload.
ajpallares
reviewed
Apr 23, 2026
ajpallares
left a comment
Member
There was a problem hiding this comment.
Looks mostly good. I have some comments, mostly about the Status and VerifiedReward, which could be converted into an enum for better representability and validation
Plumbs the verified-reward payload from the backend response through the
internal `pollRewardVerificationStatus` SPI so RC ad adapters can dispatch
meaningful "verified" events with the granted reward.
* Adds `VirtualCurrencyReward` and `VerifiedReward` (`@_spi(Internal) public`)
under `Sources/Ads/RewardVerification/`.
* Extends `RewardVerificationStatusResponse` to optionally decode a typed nested
reward payload (`{ type, code, amount }`) on `verified` responses.
- `reward` absent or null → `.noReward`
- `type == "virtual_currency"` with valid `code`+`amount` → `.virtualCurrency`
- any other `type` (or malformed `virtual_currency`) → `.unsupportedReward` + warn
* `RewardVerificationPollStatus.verified` now carries the `VerifiedReward`.
* `Purchases.pollRewardVerificationStatus` returns `.verified(reward)`.
* Test mocks injected via a new `BasePurchasesTests.MockBackend` convenience
init so `backend.adsAPI` resolves to `MockAdsAPI`.
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.)
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.
Align reward payload modeling with backend semantics by decoding `amount` as Int and updating tests to reject fractional values as malformed rewards.
Make the reward payload part of the verified status case so invalid state combinations are unrepresentable and remove the verified fallback mapping.
Treat virtual-currency rewards with non-positive amounts as malformed payloads and return unsupported reward with warning logs.
Validate virtual-currency payloads require a non-empty code and treat empty code values as malformed unsupported rewards.
Drop the exhaustive switch smoke test in VerifiedRewardTests since it did not validate behavior and duplicated compiler guarantees.
38c43d0 to
86bb476
Compare
Member
Author
|
@RCGitBot please test |
ajpallares
approved these changes
Apr 24, 2026
ajpallares
left a comment
Member
There was a problem hiding this comment.
Looks good to me! Just a nit!
Also I think the PR description is a bit stale. Would be worth updating it
Drop VerifiedReward tests that only mirrored compiler-synthesized Equatable behavior and keep behavior-focused coverage.
Member
Author
|
@RCGitBot please test |
JZDesign
pushed a commit
that referenced
this pull request
Apr 24, 2026
* Replace print with Logger.error in ISODurationFormatter (#6691) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Decode reward payload in RewardVerification poll response (#6678) * Decode reward payload in RewardVerification poll response Plumbs the verified-reward payload from the backend response through the internal `pollRewardVerificationStatus` SPI so RC ad adapters can dispatch meaningful "verified" events with the granted reward. * Adds `VirtualCurrencyReward` and `VerifiedReward` (`@_spi(Internal) public`) under `Sources/Ads/RewardVerification/`. * Extends `RewardVerificationStatusResponse` to optionally decode a typed nested reward payload (`{ type, code, amount }`) on `verified` responses. - `reward` absent or null → `.noReward` - `type == "virtual_currency"` with valid `code`+`amount` → `.virtualCurrency` - any other `type` (or malformed `virtual_currency`) → `.unsupportedReward` + warn * `RewardVerificationPollStatus.verified` now carries the `VerifiedReward`. * `Purchases.pollRewardVerificationStatus` returns `.verified(reward)`. * Test mocks injected via a new `BasePurchasesTests.MockBackend` convenience init so `backend.adsAPI` resolves to `MockAdsAPI`. * 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. * Fix virtual currency reward amount type to integer. Align reward payload modeling with backend semantics by decoding `amount` as Int and updating tests to reject fractional values as malformed rewards. * fix: address review — couple verified status to reward Make the reward payload part of the verified status case so invalid state combinations are unrepresentable and remove the verified fallback mapping. * fix: address review — reject non-positive reward amounts Treat virtual-currency rewards with non-positive amounts as malformed payloads and return unsupported reward with warning logs. * fix: address review — reject empty reward currency code Validate virtual-currency payloads require a non-empty code and treat empty code values as malformed unsupported rewards. * fix: address review — remove non-actionable enum switch test Drop the exhaustive switch smoke test in VerifiedRewardTests since it did not validate behavior and duplicated compiler guarantees. * fix: address review — remove synthesized equatable assertions Drop VerifiedReward tests that only mirrored compiler-synthesized Equatable behavior and keep behavior-focused coverage. * Delete claude.yml workflow (#6688) * Add workflowTrigger to ButtonComponent.Action (#6693) * Add workflowTrigger case to ButtonComponent.Action Mirrors Android's rename of Action.Workflow → Action.WorkflowTrigger (purchases-android#3380). Decodes backend "type": "workflow" into .workflowTrigger so workflow buttons are rendered (previously decoded as .unknown and hidden) and track as "workflow_trigger" in analytics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add tests for workflowTrigger action decoding and analytics value Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Cache decoded images by file URL in `FileImageLoader` `URL.asImageAndSize` (used by `FileImageLoader.loadFromCache`) previously called `UIImage(contentsOfFile:)` / `NSImage(contentsOfFile:)` on every access. A SwiftUI paywall with many `RemoteImage` instances (e.g. a looping carousel that materialises multiple copies of each page) ends up hitting this path hundreds of times because `StateObject(wrappedValue:)` evaluates its closure on every host view re-init. UIKit/AppKit's internal caches still cost ~0.5–1 ms per call, which adds up to ~100+ ms of main-thread blocking for only a handful of unique URLs. Introduce a process-wide `DecodedImageCache` backed by `NSCache<NSURL, _>` so repeated lookups for the same URL become a dictionary hit. `NSCache` evicts on memory pressure, so we don't pin images forever. Reads/writes are serialised through a concurrent dispatch queue with a barrier on writes to keep the cache thread-safe. Made-with: Cursor --------- Co-authored-by: Facundo Menzella <facumenzella@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Pol Miro <polmiro@gmail.com> Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
This was referenced Apr 30, 2026
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
The internal
@_spi(Internal) Purchases.pollRewardVerificationStatus(clientTransactionID:)SPI today only exposes whether verification passed (.verified/.pending/.failed/.unknown).This PR adds reward-payload support to the response shape and surfaces it through the SPI.
Description
Wire shape (typed nested)
Discussed with @peter-revenuecat to follow this typed nested approach.
{ "status": "verified", "reward": { "type": "virtual_currency", "code": "coins", "amount": 10 } }The explicit
typediscriminator makes it possible to distinguish future reward kinds (e.g. physical items) without having to disambiguate by-shape.New types (internal SPI)
Both under
Sources/Ads/RewardVerification/, both@_spi(Internal) public,Sendable,Equatable:VirtualCurrencyReward { code: String, amount: Decimal }VerifiedReward { case virtualCurrency(VirtualCurrencyReward), .noReward, .unsupportedReward }Per the codebase rule against new public
@frozenenums, these stay on the SPI surface only — they don't widen the stable public API.Decoder (
RewardVerificationStatusResponse)Decode is lenient on the reward: never fails the overall decode just because the reward subtree is missing or malformed.
verifiedRewardstatus != "verified"nilstatus == "verified",rewardabsent ornull.noRewardstatus == "verified",rewardpresent but not a JSON object.unsupportedReward+Logger.warnreward.type == "virtual_currency"with validcode+amount.virtualCurrency(...)reward.type == "virtual_currency"with malformed fields.unsupportedReward+Logger.warnreward.typeis anything else.unsupportedReward+Logger.warnUnknown wire status values continue to map to
.unknownwith the existingLogger.warn(unchanged).SPI mapping
RewardVerificationPollStatus.verifiednow carries aVerifiedReward.Purchases.pollRewardVerificationStatus(clientTransactionID:)returns.verified(response.verifiedReward ?? .noReward)(defensive fallback so a future decoder regression can't crash the SPI).Tests
RewardVerificationStatusResponseDecodingTests— added cases for verified+virtual-currency, decimal precision, missing/null reward, non-object reward, unknown reward type, malformedvirtual_currency, and non-verified statuses ignoring stray reward payloads.BackendGetRewardVerificationStatusTests— extendedtestGetRewardVerificationStatusVerifiedto assert the reward payload reaches the callback.PurchasesRewardVerificationTests— end-to-end SPI mapping for allVerifiedRewardcases plus the defensive fallback. (Also fixed: this test file was previously not registered inRevenueCat.xcodeproj, so it was silently never running. Registered it and added aBasePurchasesTests.MockBackendconvenience init that injects aMockAdsAPIsobackend.adsAPI as? MockAdsAPIresolves.)VirtualCurrencyRewardTestsandVerifiedRewardTests— direct unit tests for the new types underTests/UnitTests/Ads/RewardVerification/(field storage, equality, decimal precision; case construction, equality, exhaustive switch).Verification
swift build✅xcodebuild test -only-testing:UnitTests/{RewardVerificationStatusResponseDecodingTests,BackendGetRewardVerificationStatusTests,PurchasesRewardVerificationTests,VirtualCurrencyRewardTests,VerifiedRewardTests}✅swiftlinton changed files ✅bundle exec fastlane run_api_tests— pending (couldn't run locally; relying on CI)Note
Medium Risk
Changes the reward-verification response model and decoding logic to carry reward payloads through an internal SPI, which could affect ad-reward integrations if decoding/mapping is wrong. Impact is limited to
@_spi(Internal)APIs and is covered by expanded unit tests for malformed/unknown payload handling.Overview
Reward verification polling now returns reward details on success. The internal SPI
RewardVerificationPollStatus.verifiednow carries aVerifiedReward, andPurchases.pollRewardVerificationStatusmaps backend responses through with that associated payload.Backend response decoding was expanded to parse the
rewardsubtree leniently.RewardVerificationStatusResponsenow decodesrewardonly forstatus == "verified", supportsvirtual_currencyvia newVirtualCurrencyReward, and downgrades missing/malformed/unsupported reward payloads to.noReward/.unsupportedRewardwhile logging new backend warning strings.Tests and project wiring were updated. Adds unit tests for the new reward types and decoding edge cases, updates backend and purchases reward-verification tests to assert reward propagation, and registers new test/source files in the Xcode project (including fixing
PurchasesRewardVerificationTestsbeing included in the test target).Reviewed by Cursor Bugbot for commit 7bbdea2. Bugbot is set up for automated code reviews on this repo. Configure here.