Skip to content

Add offering_id to custom paywall impression event#6456

Merged
rickvdl merged 9 commits into
mainfrom
rickvdl/add-offering-id-to-custom-paywall-impression-event
Mar 16, 2026
Merged

Add offering_id to custom paywall impression event#6456
rickvdl merged 9 commits into
mainfrom
rickvdl/add-offering-id-to-custom-paywall-impression-event

Conversation

@rickvdl

@rickvdl rickvdl commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds offering_id field to the custom paywall impression event, populated from the current cached offering identifier
  • Field is omitted from the JSON payload when no offering is cached

Related PRs

Test plan

  • Unit tests for offering_id appearing in encoded JSON
  • Unit tests for offering_id omitted when nil
  • Unit tests for offering ID passed through from cached offerings
  • Unit tests for nil offering ID when no cached offerings

Note

Medium Risk
Adds a new optional field to the custom paywall impression event payload and changes how impressions are populated (pulling offeringId from cached offerings when not provided), which could affect backend analytics ingestion and client expectations.

Overview
Custom paywall impression tracking now carries an optional offering_id end-to-end: CustomPaywallEvent/FeatureEventsRequest.CustomPaywallEvent include and encode the field (and FeatureEvent.toMap() exposes it for hybrid SDK consumers), omitting it when nil.

Purchases.trackCustomPaywallImpression(_:) now populates offeringId from CustomPaywallImpressionParams or falls back to the current cached offering identifier. Public API surface is extended to accept offeringId (Swift + ObjC), with unit/API tests updated to validate JSON encoding and fallback/override behavior.

Written by Cursor Bugbot for commit d8a3392. This will update automatically on new commits. Configure here.

@rickvdl rickvdl marked this pull request as ready for review March 13, 2026 10:29
@rickvdl rickvdl requested a review from a team as a code owner March 13, 2026 10:29
Comment thread Sources/Paywalls/Events/CustomPaywallEvent.swift
struct Data {

var paywallId: String?
var offeringId: String?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Not related to this line]
Similar to in Android, I wonder if we want to allow overriding the offering used on the developer side... But yeah we can go with this for now and add this later if needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added it here as well, like in Android

case appSessionID = "appSessionId"
case timestamp
case paywallId
case offeringId = "offering_id"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm curious... why are we adding the offering_id here but not paywall_id?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point, this actually resulted in the same thing, since we use convertToSnakeCase as the keyEncodingStrategy, so this was redundant and I have removed it.

I've also updated the tests to use the same JSON encoder that the actual implementation uses, because we were actually asserting against different keys in the tests than the ones we expect in the backend, due to the difference in JSON encoding settings.

Comment thread Sources/Paywalls/Events/CustomPaywallEvent.swift
rickvdl added 2 commits March 16, 2026 11:21
Pass the current cached offering identifier when tracking custom paywall
impressions. The field is encoded as `offering_id` in the JSON payload
sent to the backend.
- Add offeringId to customPaywallEventMap() for hybrid SDK consumption
- Remove redundant explicit CodingKey mappings (convertToSnakeCase handles it)
- Update tests to use JSONEncoder.default to verify actual wire format
@rickvdl rickvdl force-pushed the rickvdl/add-offering-id-to-custom-paywall-impression-event branch from d99143a to f07287d Compare March 16, 2026 10:42
Add offeringId parameter to CustomPaywallImpressionParams, matching the
Android SDK API. When provided, the explicit value is used; otherwise
falls back to the current cached offering identifier.
@rickvdl rickvdl requested a review from tonidero March 16, 2026 11:04

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚢

github-merge-queue Bot pushed a commit to RevenueCat/purchases-android that referenced this pull request Mar 16, 2026
## Summary
- Adds `offering_id` field to the custom paywall impression event,
populated from the current cached offering identifier
- Exposes `cachedCurrentOfferingIdentifier` through `OfferingsManager`
and `PurchasesOrchestrator`
- Field is omitted from the JSON payload when no offering is cached

## Related PRs
- iOS: RevenueCat/purchases-ios#6456

## Test plan
- [x] Unit tests for event creation with and without offering ID
- [x] Unit tests for backend event conversion with offering ID
- [x] Unit tests for JSON serialization (key present when set, omitted
when null)
- [x] Unit tests for JSON roundtrip with offering ID

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this is an additive analytics/event-schema change that
threads an optional `offering_id` through existing custom paywall
impression tracking, with unit tests covering serialization and null
handling.
> 
> **Overview**
> Custom paywall impression tracking now includes an optional
**`offering_id`** field in the stored/serialized backend event payload.
> 
> `CustomPaywallImpressionParams` gains an `offeringId` parameter, and
`Purchases.trackCustomPaywallImpression` will default it from the cached
current offering identifier when not explicitly provided (plumbed via
new `cachedCurrentOfferingIdentifier` accessors on
`OfferingsManager`/`PurchasesOrchestrator`). API tester fixtures and
unit tests are updated to cover creation, backend conversion, and JSON
behavior (key present when set, omitted when null).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a2a1f96. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@_spi(Experimental) APIs are not visible to ObjC consumers. The ObjC
API tester will be re-added when the experimental annotation is removed.
@@ -2163,10 +2163,11 @@ extension Purchases {
@_spi(Experimental)
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@objc public func trackCustomPaywallImpression(_ params: CustomPaywallImpressionParams) {

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.

I see the ObjC API tests were removed. Do we need to remove the @objc marker here as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It was actually failing because of the new property that was added (offeringId). Updated with the right initializers for Objc now as well :)

@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 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: ObjC API tester deleted instead of updated
    • I restored the deleted ObjC API tester files and re-registered them in main.m to keep compile-time coverage for RCCustomPaywallImpressionParams, including offeringId usage.

Create PR

Or push these changes by commenting:

@cursor push 9487e792cb
Preview (9487e792cb)
diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.h b/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.h
new file mode 100644
--- /dev/null
+++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.h
@@ -1,0 +1,16 @@
+//
+//  RCCustomPaywallImpressionAPI.h
+//  ObjCAPITester
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface RCCustomPaywallImpressionAPI : NSObject
+
++ (void)checkAPI;
+
+@end
+
+NS_ASSUME_NONNULL_END

diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.m b/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.m
new file mode 100644
--- /dev/null
+++ b/Tests/APITesters/AllAPITests/ObjcAPITester/RCCustomPaywallImpressionAPI.m
@@ -1,0 +1,33 @@
+//
+//  RCCustomPaywallImpressionAPI.m
+//  ObjCAPITester
+//
+
+#import "RCCustomPaywallImpressionAPI.h"
+
+@import RevenueCat;
+
+@implementation RCCustomPaywallImpressionAPI
+
++ (void)checkAPI {
+    if (@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)) {
+        // CustomPaywallImpressionParams API
+        RCCustomPaywallImpressionParams *paramsDefault __unused = [[RCCustomPaywallImpressionParams alloc] initWithPaywallId:nil];
+        RCCustomPaywallImpressionParams *paramsWithId __unused = [[RCCustomPaywallImpressionParams alloc] initWithPaywallId:@"my-paywall"];
+        RCCustomPaywallImpressionParams *paramsWithNil __unused = [[RCCustomPaywallImpressionParams alloc] initWithPaywallId:nil];
+        RCCustomPaywallImpressionParams *paramsWithOffering __unused = [[RCCustomPaywallImpressionParams alloc] initWithPaywallId:@"my-paywall" offeringId:@"my-offering"];
+        RCCustomPaywallImpressionParams *paramsOfferingOnly __unused = [[RCCustomPaywallImpressionParams alloc] initWithPaywallId:nil offeringId:@"my-offering"];
+
+        // CustomPaywallImpressionParams properties
+        NSString *paywallId __unused = paramsWithId.paywallId;
+        NSString *offeringId __unused = paramsWithOffering.offeringId;
+
+        // trackCustomPaywallImpression API
+        RCPurchases *purchases = RCPurchases.sharedPurchases;
+        [purchases trackCustomPaywallImpression:paramsDefault];
+        [purchases trackCustomPaywallImpression:paramsWithId];
+        [purchases trackCustomPaywallImpression];
+    }
+}
+
+@end

diff --git a/Tests/APITesters/AllAPITests/ObjcAPITester/main.m b/Tests/APITesters/AllAPITests/ObjcAPITester/main.m
--- a/Tests/APITesters/AllAPITests/ObjcAPITester/main.m
+++ b/Tests/APITesters/AllAPITests/ObjcAPITester/main.m
@@ -30,6 +30,7 @@
 #import "RCSubscriptionPeriodAPI.h"
 #import "RCTransactionAPI.h"
 #import "RCVerificationResultAPI.h"
+#import "RCCustomPaywallImpressionAPI.h"
 #import "RCPaywallViewControllerAPI.h"
 
 
@@ -91,6 +92,7 @@
         [RCVerificationResultAPI checkAPI];
 
         if (@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)) {
+            [RCCustomPaywallImpressionAPI checkAPI];
             [RCPaywallViewControllerAPI checkAPI];
         }
     }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread Tests/APITesters/AllAPITests/ObjcAPITester/main.m Outdated
rickvdl added 4 commits March 16, 2026 12:29
…m paywall impression API"""

This reverts commit c2a164f.
Adding offeringId to the init removed the single-arg initWithPaywallId:
ObjC selector. This convenience init restores backwards compatibility.

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

I think it makes sense! I only have one more suggestion, to avoid two inits clashing in Swift

/// - paywallId: An optional identifier for the custom paywall being shown.
/// - offeringId: An optional identifier for the offering associated with the custom paywall.
/// If not provided, the SDK will use the current offering identifier from the cache.
@objc public init(paywallId: String? = nil, offeringId: String? = nil) {

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.

Suggested change
@objc public init(paywallId: String? = nil, offeringId: String? = nil) {
@objc public init(paywallId: String? = nil, offeringId: String?) {

Probably not a big deal, but in Swift this could clash with the new convenience init below. So I'd say it's better to remove the default value to nil.

Then, to be strict we would need to update the text

If not provided, the SDK will use the current offering identifier from the cache

to say

If nil, the SDK will use the current offering identifier from the cache

(I'd say to update the doc for the parameter only in the init's documentation, not on the offeringId property itself)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a good point, addressed that and added some more API tests to handle some more variations

@rickvdl rickvdl merged commit 0336213 into main Mar 16, 2026
38 checks passed
@rickvdl rickvdl deleted the rickvdl/add-offering-id-to-custom-paywall-impression-event branch March 16, 2026 13:58
github-merge-queue Bot pushed a commit to RevenueCat/purchases-android that referenced this pull request Mar 16, 2026
## Summary
- Removes `@ExperimentalPreviewRevenueCatPurchasesAPI` from
`CustomPaywallImpressionParams` and `trackCustomPaywallImpression`

Related iOS PR: RevenueCat/purchases-ios#6456

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this only changes public API annotations/signatures for
`trackCustomPaywallImpression`/`CustomPaywallImpressionParams` without
altering runtime behavior, but it does affect compile-time opt-in
requirements for consumers.
> 
> **Overview**
> Promotes custom paywall impression tracking to a stable API by
removing `@ExperimentalPreviewRevenueCatPurchasesAPI` from
`Purchases.trackCustomPaywallImpression(...)` and
`CustomPaywallImpressionParams`.
> 
> Updates the generated API signature files so the methods/classes no
longer appear as experimental in the published surface.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
ba6bf0e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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