feat(ads): add rewarded-ad reward event types and AdTracker methods#6843
Conversation
4 builds increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 72.4 kB |
| RevenueCat.InternalAPI.InternalAPI | ⬆️ 11.7 kB |
| Code Signature | ⬆️ 6.0 kB |
| DYLD.Exports | ⬆️ 5.1 kB |
| Strings.Unmapped | ⬆️ 728 B |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 67.9 kB (0.55%)
Total download size change: ⬆️ 22.7 kB (0.55%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 5.7 kB |
| RevenueCat.AdEvent.value witness | ⬆️ 3.5 kB |
| DYLD.Exports | ⬆️ 2.9 kB |
| 📝 RevenueCat.AdRewardVerified.init(from) | ⬆️ 2.5 kB |
| 📝 RevenueCat.AdRewardEarnedUnverified.init(from) | ⬆️ 2.2 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 146.6 kB (0.54%)
Total download size change: ⬆️ 32.7 kB (0.52%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 58.0 kB |
| Code Signature | ⬆️ 4.1 kB |
| RevenueCat.AdEvent.value witness | ⬆️ 3.5 kB |
| DYLD.Exports | ⬆️ 3.0 kB |
| 📝 RevenueCat.AdRewardVerified.init(from) | ⬆️ 2.5 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 58.8 kB (0.54%)
Total download size change: ⬆️ 22.4 kB (0.53%)
Largest size changes
| Item | Install Size Change |
|---|---|
| RevenueCat.AdEvent.value witness | ⬆️ 3.5 kB |
| DYLD.Exports | ⬆️ 2.9 kB |
| 📝 RevenueCat.AdRewardVerified.init(from) | ⬆️ 2.5 kB |
| 📝 RevenueCat.AdRewardEarnedUnverified.init(from) | ⬆️ 2.2 kB |
| 📝 RevenueCat.AdRewardFailedToVerify.init(from) | ⬆️ 1.8 kB |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
Adds two rawValue-based public classes that will be used as discriminators and failure-reason values on the upcoming reward-SDK event types. Both follow the existing MediatorName/AdFormat/Precision pattern: @objc NSObject subclass with rawValue String, public static constants, Codable, equality/hash overrides, @_spi(Experimental). Using rawValue classes (not Swift enums) keeps the public API extensible without source-breaking exhaustive-switch consumers, per CLAUDE.md. No existing code references these types yet; they will be wired up in the following commits.
Adds three public event-data classes for the rewarded-ad reward lifecycle: - AdRewardEarnedUnverified: fires when the ad SDK reports a user-earned reward before server-side verification completes. Carries rewardVerificationEnabled, rewardItem (AdMob reward.type), and rewardAmount. - AdRewardVerified: fires when server-side verification confirms the reward. Carries rewardType (discriminator) plus optional virtual-currency code/amount fields populated only for .virtualCurrency. - AdRewardFailedToVerify: fires on terminal verification failure. Carries a structured failureReason. All three conform to AdImpressionEventData (mediatorName/adFormat/placement/ adUnitId/impressionId + optional networkName) and follow the existing AdLoaded/AdRevenue shape: NSObject subclass, @_spi(Experimental), @objc(RC...) name, Codable, @unchecked Sendable, private(set) properties, NSObject equality/hash overrides. Optional NSNumber fields use the existing private rawValue Int? + computed NSNumber? pattern from AdFailedToLoad. The internal AdEvent enum gains three matching cases and the creationData/eventData/impressionIdentifier/networkName/mediatorErrorCode helpers are extended exhaustively. Three new accessors are added for reward-specific data (rewardEarnedUnverifiedData, rewardVerifiedData, rewardFailedToVerifyData) for use by the upcoming wire-schema mapping. EventType cases are added to AdEventsRequest to keep the eventType switch exhaustive; wire raw values keep the rc_ads_ad_reward_sdk_* prefix to preserve the cross-platform backend ingestion contract. The full wire-field mapping lands in a follow-up commit.
Adds three @_spi(Experimental) @objc methods on AdTracker mirroring the existing trackAdRevenue pattern: - trackAdRewardEarnedUnverified(_:) — fires when the ad SDK reports a user-earned reward before verification. - trackAdRewardVerified(_:) — fires when verification confirms the reward. - trackAdRewardFailedToVerify(_:) — fires when verification terminally fails. Each constructs the matching AdEvent case and dispatches to the events manager via a fire-and-forget Task, exactly like the other track* methods. This exposes reward-event tracking to apps that don't use the AdMob adapter; the adapter will route through these in a follow-up commit.
Adds the reward-event wire fields to AdEventsRequest.AdEventRequest: - rewardVerificationEnabled, rewardItem, rewardAmount (earned/unverified) - rewardType, rewardCurrencyCode, rewardCurrencyAmount (verified) - failureReason (failed-to-verify) The init?(storedEvent:) mapping reads the three new accessors on AdEvent (rewardEarnedUnverifiedData, rewardVerifiedData, rewardFailedToVerifyData) introduced in the prior commit, so the fields are populated only for the matching event type and null for all others. CodingKeys gains entries for each new field; snake_case conversion turns them into reward_verification_ enabled / reward_item / reward_amount / reward_type / reward_currency_code / reward_currency_amount / failure_reason on the wire. EventType cases for the three reward events were added in the prior commit to keep that commit's switch exhaustive; this commit completes the wire schema by adding the corresponding field mapping.
Extends the existing AdEventTests, AdEventsRequestTests, and PurchasesAdEventsTests files (matching the codebase convention of one file per category covering all event variants): - AdEventTests: equality assertions for each new reward type (same / different properties), nil-field acceptance for optional reward fields, rawValue stability for AdRewardType / AdRewardFailureReason static constants, and Codable round-trips for all three reward data classes. - AdEventsRequestTests: snapshot tests covering the wire JSON payloads for the three new reward event types, with the corresponding committed snapshot fixtures. - PurchasesAdEventsTests: integration tests verifying that calling Purchases.shared.adTracker.trackAdReward* writes the expected AdEvent case into mockEventsManager.trackedAdEvents with all fields preserved. Snapshot fixtures verify the snake_case wire format produced by the JSONEncoder.KeyEncodingStrategy.convertToSnakeCase strategy applied to AdEventsRequest.AdEventRequest. The wire event names retain the _sdk_ infix (rc_ads_ad_reward_sdk_unverified, etc.) per the cross-platform backend ingestion contract.
Extends RCAdTrackerAPI to exercise the new reward public surface so ObjC compatibility breakage is caught by the API tester target in CI. Covers: - RCAdRewardType (static constants, raw-value initializer, rawValue property). - RCAdRewardFailureReason (static constants, raw-value initializer, rawValue property). - RCAdRewardEarnedUnverified (full initializer with NSNumber rewardAmount, every public property accessor). - RCAdRewardVerified (full initializer with NSNumber rewardCurrencyAmount, every public property accessor). - RCAdRewardFailedToVerify (full initializer, every public property accessor). - AdTracker.trackAdRewardEarnedUnverified / trackAdRewardVerified / trackAdRewardFailedToVerify. Note: the api/*.swiftinterface baselines do not include @_spi(Experimental) declarations (verified by the existing AdLoaded / AdRevenue / MediatorName also being absent from the baselines), so the new reward types do not appear in those files and regeneration produces no diff. The ObjC API tester is the active compatibility check for this surface.
Fixes CI build failures: pass `nil as Int?` for amount parameters to pick the Int? overload over NSNumber?, and qualify `AdRewardType.noReward` to disambiguate from `VerifiedReward.noReward`.
4927a72 to
d5de0c9
Compare
Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
Replaces the AdRewardType discriminator + flat rewardCurrencyCode/Amount fields on AdRewardVerified with a single reward: AdReward property that bundles kind and payload together. AdReward also subsumes the adapter's VerifiedReward and the internal RevenueCat.VerifiedReward enum, so all three modules share one type. - New AdReward struct at @_spi(Experimental) in the main SDK with virtualCurrency / noReward / unsupportedReward factories and a kindRawValue accessor for wire encoding and ObjC interop. - Promotes VirtualCurrencyReward from @_spi(Internal) to @_spi(Experimental). - AdRewardVerified gains custom Codable that preserves the existing rewardType / rewardCurrencyCode / rewardCurrencyAmount wire keys, plus @objc derived accessors (rewardKindRawValue, virtualCurrencyCode, virtualCurrencyAmount) and an ObjC init taking the flat fields. - Adapter's VerifiedReward struct removed; RewardVerificationResult now carries AdReward and exposes it via a renamed `reward` property.
Re-aligns these types with the rest of the ads + reward verification surface, which is uniformly @_spi(Experimental). Keeps them out of RevenueCat's stable public namespace until the feature stabilizes.
The ObjC convenience init shared its reward-construction helper with the Codable decoder, so invalid developer inputs (kindRawValue "virtual_currency" with nil code or amount) silently produced an unsupportedReward with no diagnostic. Split the helpers: a developer-input path that logs and asserts via AdsStrings.missing_virtual_currency_reward_fields, and a decoder path that stays intentionally silent (malformed wire data isn't a programming bug).
…FailedToVerify to struct AdRewardFailureReason becomes a Swift enum with String raw values (timeout / network_error / backend_error / unknown). The enum forces exhaustive switches at compile time when new failure reasons are added — a stronger guarantee than the previous class-with-rawValue pattern. AdRewardFailedToVerify becomes a plain struct (parallel to the AdRewardEarnedUnverified and AdRewardVerified conversions): drops @objc/NSObject/equality overrides, since the enum-typed `failureReason` can no longer be represented in Objective-C anyway.
…to public Consistency with AdReward's .noReward / .unsupportedReward static lets: external devs now write `if result == .failed` rather than `if result.isFailed`, using the same equality idiom they already use for AdReward cases. `.failed` is a parameter-less singleton with no provenance to forge, so promoting it to `public` introduces no construction-safety concern.
… reward The previous example called AdReward.virtualCurrency(code:amount:), which is internal — SPI consumers cannot construct it. Show the realistic flow where the AdReward comes from the reward-verification pipeline.
AdRewardEarnedUnverified, AdRewardVerified, and AdRewardFailedToVerify are value types whose stored properties are all Sendable lets. Plain Sendable turns on the compiler check we were opting out of for no reason — the @unchecked was a copy-paste from the @objc NSObject-backed event classes.
Both are final NSObject subclasses with only let properties, so they're trivially Sendable-safe. Declaring @unchecked Sendable on them lets the reward event structs that hold them use checked Sendable instead of @unchecked, closing the diagnostic warning surfaced by the previous commit.
When AdRewardVerified's decoder hits an unknown reward kind or a malformed virtual_currency payload, it correctly falls back to .unsupportedReward without crashing — but until now it did so silently, hiding backend/SDK schema drift. Add two AdsStrings warnings and emit them on the fallback paths, plus three tests covering unknown kind, missing amount, and non-positive amount.
…a protocol Both accessors previously enumerated every case; all non-failedToLoad data types already conform to AdImpressionEventData, so a single cast on the existing eventData reads both fields. Future ad-event cases that conform to AdImpressionEventData get covered automatically.
The previous one-liners ('Data for ad reward earned (unverified) events.')
repeated the type name. Replace with descriptions that explain what moment
the event represents, mirroring the case-level docs on AdEvent.
Swiftlint flags the previous AdEventTests body as over the type_body_length limit even before this PR. Move the three new decoder-fallback tests into an extension on AdEventTests, and consolidate the JSON construction into a shared helper so each test reads as a one-line behavioural assertion.
Adds parameterized `encode<K:CodingKey>(into:typeKey:codeKey:amountKey:)` and `decode<K:CodingKey>(from:typeKey:codeKey:amountKey:)` helpers on AdReward, and has AdRewardVerified delegate to them. The wire layout (rewardType / rewardCurrencyCode / rewardCurrencyAmount) is unchanged. Adding a new AdReward.Kind now only requires editing AdReward (factory, Kind raw value, decode branch) — AdRewardVerified stays untouched unless the new kind introduces new wire fields.
…gle validation point Routes AdReward.virtualCurrency(code:amount:), AdReward.decode, and RewardVerificationStatusResponse through VirtualCurrencyReward.init?(code:amount:), which rejects empty code or non-positive amount. Drops the now-redundant AdsStrings.invalid_virtual_currency_amount.
Moves AdRewardFailureReason and the three AdReward* event structs out of AdEvent.swift into a new file, and registers it in the Xcode project. The file_length disable on AdEvent.swift stays since the remainder is still above the 400-line limit.
ajpallares
left a comment
There was a problem hiding this comment.
Looking good! Just a coupe of suggestions
- PR description. Could you update it (e.g. still references outdated RCAdTrackerAPI.m file) and please 🙏 make it smaller so that it's easier to read?
- Small code change below
…ount:) factory to tests
|
Updated the PR description: trimmed it down, removed the stale ObjC |
ajpallares
left a comment
There was a problem hiding this comment.
Thanks for iterating on this!
I would only change the PR description to make it completely up-to-date and to make it easier (and quicker) to read, only keeping the essential. But that's not a blocker for merging as it can be updated after the fact




Motivation
The ad-events pipeline (loaded / displayed / opened / revenue / failed-to-load) had no
way to represent a user earning a rewarded-ad reward or a reward being verified.
This blocks reward-funnel metrics: how many rewarded ads produce a reward, server-side
verification success rate, and time-to-verification.
What this PR does
@_spi(Internal)reward event types —AdRewardEarnedUnverified,AdRewardVerified,AdRewardFailedToVerify— plus anAdRewardFailureReasonenum,all conforming to
AdImpressionEventData.AdTracker.trackAdReward*methods, the internalAdEventcases, and thewire mapping.
@_spi(Experimental)AdReward/VirtualCurrencyRewardpayload typesfrom the AdMob adapter into the core SDK, removing the adapter's duplicate
VerifiedReward. The AdMob adapter, sample app, and docs are updated to consumeAdReward.AdTrackermethodrc_ads_ad_reward_sdk_unverifiedAdRewardEarnedUnverifiedtrackAdRewardEarnedUnverified(_:)rc_ads_ad_reward_sdk_verifiedAdRewardVerifiedtrackAdRewardVerified(_:)rc_ads_ad_reward_sdk_failed_to_verifyAdRewardFailedToVerifytrackAdRewardFailedToVerify(_:)Wire event names keep the
_sdk_infix to preserve the backend ingestion contract thatdistinguishes SDK-observed events from backend SSV-webhook events.
Testing
.unsupportedReward(with warning log) on unknown kind / malformed virtual-currencypayload; wire-JSON snapshots for all reward events;
Purchases.shared.adTrackerintegration coverage.
Note
Medium Risk
Touches reward verification public/experimental types and ad event ingestion; wire schema is extended but backward-compatible, while adapter apps must follow the AdReward shape instead of the removed VerifiedReward type.
Overview
Introduces reward lifecycle analytics in the core SDK: three
@_spi(Internal)event payloads (AdRewardEarnedUnverified,AdRewardVerified,AdRewardFailedToVerify) plus matchingAdTracker.trackAdReward*methods and wire types (rc_ads_ad_reward_sdk_*) with new optional fields onAdEventsRequest.Consolidates verified-reward modeling by adding
@_spi(Experimental)AdRewardin the core SDK (replacing the internalVerifiedRewardenum and the adapter’s duplicateVerifiedRewardstruct).RewardVerificationResult.verifiedRewardnow exposesAdReward?; the adapter passes rewards through withoutmapVerifiedReward.VirtualCurrencyRewarduses a failable initializer (empty code / non-positive amount → invalid); invalid payloads decode to.unsupportedRewardwith warnings instead of adapter-side assertions.The AdMob adapter, sample app, and docs are updated to use
reward.virtualCurrencyonAdReward.RewardVerificationResult.isFailedis removed in favor of equality with.failed.Reviewed by Cursor Bugbot for commit 5ec035c. Bugbot is set up for automated code reviews on this repo. Configure here.