Skip to content

Add presented offering context to custom paywall events#6707

Merged
rickvdl merged 15 commits into
mainfrom
rickvdl/add-presented-offering-context-to-custom-paywall-events
Jun 1, 2026
Merged

Add presented offering context to custom paywall events#6707
rickvdl merged 15 commits into
mainfrom
rickvdl/add-presented-offering-context-to-custom-paywall-events

Conversation

@rickvdl

@rickvdl rickvdl commented Apr 28, 2026

Copy link
Copy Markdown
Member

Description

Adds `placementIdentifier`, `targetingRevision`, and `targetingRuleId` to the custom paywall impression event.

Mirrors #6476, which added the same fields for the internal paywall events. Android parity: RevenueCat/purchases-android#3424.

What's new

  • `CustomPaywallImpressionParams` gains an `Offering`-based initializer. Passing an `Offering` lets the SDK derive both the offering identifier and the presented offering context (placement and targeting) from the offering's first available package.
  • The `offeringId`-only initializer is now deprecated in favour of passing an `Offering` object, matching Android.
  • `CustomPaywallImpressionParams` exposes a `@_spi(Internal) presentedOfferingContext` property (equivalent to Android's `@InternalRevenueCatAPI` constructor) so hybrid SDKs can pass pre-resolved context through purchases-hybrid-common without requiring an `Offering` object.
  • `Purchases.trackCustomPaywallImpression(_:)` resolves offering context using the following fallback chain, mirroring Android:
    Developer passes `offering_id` sent context sent
    Nothing (or only `paywallId`) from `cachedOfferings.current` from current offering
    Deprecated `offeringId` matching cache passed string from cached match
    Deprecated `offeringId` not in cache passed string nil
    `Offering` object from offering derived from offering
    Internal `presentedOfferingContext` (hybrid SDK) passed `offeringId` passed context
  • `CustomPaywallEvent.Data` carries flat `placementIdentifier` / `targetingRevision` / `targetingRuleId` fields, mirroring the existing internal `PaywallEvent.Data` shape.
  • The wire payload nests these under `presented_offering_context`. The custom paywall event has its own `FeatureEventsRequest.CustomPaywallEvent.PresentedOfferingContextData` rather than reusing the internal paywall event's struct, so each event owns its own.

Note

Medium Risk
Public API deprecation and new analytics fields affect integrators and backend payloads, but behavior is backward compatible for legacy stored events and is covered by broad unit tests.

Overview
Custom paywall impression analytics now carry placement and targeting (placement identifier, targeting revision, rule id), aligned with internal paywall events and Android.

Public API: CustomPaywallImpressionParams adds init(paywallId:offering:) so the SDK can read PresentedOfferingContext from the offering’s first package. The offeringId string initializer is deprecated in favor of passing an Offering. An @_spi(Internal) path lets hybrid SDKs supply pre-resolved context without an Offering.

Runtime: Purchases.trackCustomPaywallImpression resolves offering id and context from explicit SPI context, a cached offering for a passed id, or the current cached offering (context nil if the id isn’t in cache).

Events & wire: CustomPaywallEvent.Data stores the new fields; hybrid toMap() and backend JSON include them (nested presented_offering_context on upload, omitted when empty). Stored events without those keys still decode.

API surface snapshots and unit/API tests cover encoding, resolution, and legacy payloads.

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

@rickvdl rickvdl added the pr:feat A new feature label Apr 28, 2026
@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch 2 times, most recently from 40e2204 to e2163fe Compare April 30, 2026 14:16
@rickvdl

rickvdl commented Apr 30, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@rickvdl

rickvdl commented Apr 30, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@rickvdl rickvdl marked this pull request as ready for review April 30, 2026 14:56
@rickvdl rickvdl requested a review from a team as a code owner April 30, 2026 14:56
@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch from ca04e15 to eb46543 Compare May 28, 2026 06:24
@rickvdl

rickvdl commented May 28, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

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

I think this looks great!! Thank you!!

@objc public init(paywallId: String? = nil, offeringId: String?) {
self.paywallId = paywallId
self.offeringId = offeringId
self.offering = nil

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.

Should this init be marked as deprecated? And/or spi(Internal)

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.

I think you reviewed this just before I pushed the changes taken from your Android feedback :) Updated now!

/// When provided, the SDK will derive the presented offering context (placement and targeting
/// information) from this offering. If neither `offering` nor `offeringId` is provided, the SDK
/// will use the current offering from the cache.
@objc public let offering: Offering?

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.

I wonder if we should store the PresentedOfferingContext directly, like we do in Android... Not sure if we need the whole offering for something else?

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.

Same for this one, this is updated now :)

@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch from 9e90383 to 8ed2bf8 Compare May 28, 2026 14:19
Comment thread Sources/Paywalls/Events/CustomPaywallImpressionParams.swift
Comment thread api/revenuecat-api-ios.swiftinterface Outdated
@rickvdl

rickvdl commented May 28, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

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

Nice! Thanks for fixing those hehe

@rickvdl

rickvdl commented May 28, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

rickvdl and others added 13 commits June 1, 2026 12:25
…ing-context-to-custom-paywall-events` (#6722)

* Add presented offering context to custom paywall events

* Update API testers for offering-based custom paywall impression init

* Add tests for placement and targeting in custom paywall events

* Accept Offering for custom paywall impression and derive context

* Decouple custom paywall wire request from PaywallEvent's nested context type

* Look up offeringId in cached offerings to derive presented offering context

* Update baseline swiftinterface files

---------

Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com>
…onMetadataSyncHelper

The syncIfNeeded call in Purchases.init races with the call from
applicationWillEnterForeground: if the init task acquires isSyncing first
(even with empty metadata), it blocks the foreground notification task
from running. Removing the init call is safe because applicationWillEnterForeground
fires reliably on startup and covers the sync need.
…ransactionMetadataSyncHelper"

This reverts commit d977926.
…ad of Offering

Matches the Android PR #3424 changes:
- Replace `offering: Offering?` property with `@_spi(Internal) presentedOfferingContext`
  so the params object stores the derived context rather than the raw Offering
- Add `@_spi(Internal)` designated init for hybrid SDK (PHC) passthrough of
  pre-resolved presentedOfferingContext without requiring an Offering object
- Deprecate `init(paywallId:offeringId:)` in favour of `init(paywallId:offering:)`
- Update `trackCustomPaywallImpression` resolution order to match Android:
  params.presentedOfferingContext → cached[offeringId] → cached.current
- Update Swift and ObjC API testers accordingly
@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch from bb9525f to fe395a8 Compare June 1, 2026 10:25
@rickvdl

rickvdl commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

Comment thread Sources/Paywalls/Events/CustomPaywallEvent.swift
@rickvdl

rickvdl commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

@RCGitBot please test

@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 2 potential issues.

Fix All in Cursor

❌ 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 c89d357. Configure here.

Comment thread Sources/Paywalls/Events/CustomPaywallImpressionParams.swift
Comment thread Sources/Events/FeatureEvents/FeatureEvent.swift
@rickvdl rickvdl merged commit 27cf37c into main Jun 1, 2026
42 of 43 checks passed
@rickvdl rickvdl deleted the rickvdl/add-presented-offering-context-to-custom-paywall-events branch June 1, 2026 14:59
rickvdl added a commit to RevenueCat/purchases-hybrid-common that referenced this pull request Jun 4, 2026
…ession events (#1665)

Builds on native PRs
RevenueCat/purchases-ios#6707 and
RevenueCat/purchases-android#3424

Adds the `presentedOfferingContext` (optional) parameter support to the
custom paywall impression event API. When present the value will be read
from the passed map as `[string: any]` and parses it using existing
helpers like in other places.
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
)

### Description

Adds `placement_identifier`, `targeting_revision`, and
`targeting_rule_id` to the custom paywall impression event.

Mirrors the event payload work in iOS PR
RevenueCat/purchases-ios#6707 for Android. The
internal paywall events already received this context via RevenueCat#3253; this PR
brings the same placement/targeting attribution to custom paywall
events.

### What's new

- `CustomPaywallImpressionParams` gains an `Offering`-based constructor.
Passing an `Offering` lets the SDK derive the offering identifier and
`PresentedOfferingContext` from the offering's first available package.
- The params object stores `paywallId`, `offeringId`, and
`presentedOfferingContext`; it does not store or expose the `Offering`
object. This keeps the public params surface focused on the values
needed for event tracking.
- The string `offeringId` constructor is deprecated in favor of passing
an `Offering`, because an offering identifier alone cannot reliably
carry placement and targeting context.
- An `@InternalRevenueCatAPI` constructor accepts `paywallId`,
`offeringId`, and `presentedOfferingContext` directly, so hybrid SDKs
can pass the context through PHC to native without exposing `Offering`
on the public params API.
- `Purchases.trackCustomPaywallImpression(params)` resolves event data
with this fallback chain:

  | Developer passes | `offering_id` sent | context sent |
  | --- | --- | --- |
| Nothing (or only `paywallId`) | from `cachedOfferings.current` | from
current offering |
| Deprecated `offeringId` matching cache | passed string | from cached
match |
  | Deprecated `offeringId` not in cache | passed string | nil |
  | `Offering` object | from offering | derived from offering |
| Internal `presentedOfferingContext` | passed `offeringId` | passed
context |

- `CustomPaywallEvent.Impression.Data` carries flat
`placementIdentifier` / `targetingRevision` / `targetingRuleId` fields,
mirroring the existing internal `PaywallEvent` Android shape.
- The wire payload nests these under `presented_offering_context`. The
custom paywall event has its own
`BackendEvent.CustomPaywallPresentedOfferingContextData` rather than
reusing the internal paywall event's struct, matching the iOS approach
where each event owns its own.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Public API surface changes (new constructors, deprecation) and
analytics event schema updates; behavior is localized to custom paywall
tracking rather than purchases or auth.
> 
> **Overview**
> Custom paywall impression tracking now sends **placement and targeting
context** (placement identifier, targeting revision, rule id) on the
backend event, nested as `presented_offering_context`, aligning custom
paywalls with internal paywall analytics.
> 
> **`CustomPaywallImpressionParams`** adds constructors that take an
**`Offering`** (or offering-only) so the SDK can set `offeringId` and
`presentedOfferingContext` from the offering’s first package. The
**`offeringId` string constructor is deprecated** in favor of
`Offering`. Params expose `presentedOfferingContext` but not the
offering object itself.
> 
> **`Purchases.trackCustomPaywallImpression`** resolves what gets
tracked: explicit context from params, lookup from **cached offerings**
when only an offering id or nothing is passed, or values taken directly
from a passed **`Offering`**.
> 
> Wire and storage paths extend **`CustomPaywallEvent.Impression.Data`**
and **`BackendEvent.CustomPaywall`** with the new fields;
**`cachedOfferings`** is exposed on the orchestrator/offerings manager
to support resolution.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ea3369c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants