Skip to content

Add presented offering context to custom paywall events#3424

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

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

Conversation

@rickvdl

@rickvdl rickvdl commented Apr 30, 2026

Copy link
Copy Markdown
Member

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


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.

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

@rickvdl rickvdl requested review from a team as code owners April 30, 2026 14:28
@rickvdl rickvdl added the pr:feat A new feature label Apr 30, 2026

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

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 c1701e85a67ca633a8ccaba1716ee33d5d79a007. Configure here.

Comment thread api-tester/src/defaults/java/com/revenuecat/apitester/java/PurchasesAPI.java Outdated

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

Just some suggestions in the public API but in general makes sense. Thank you for adding this!

* identifier from the cache.
* @property offering An optional [Offering] associated with the custom paywall. When provided, the
* SDK will derive both the offering identifier and the presented offering context (placement and
* targeting information) from this offering.
*/
@Poko
public class CustomPaywallImpressionParams @JvmOverloads constructor(
public val paywallId: String? = null,
public val offeringId: String? = null,

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 we should deprecate the constructor with the offeringId and just leave one with the optional offering as the preferred way to do this, so there are less likely chances of folks passing us the offering id directly

Also, doesn't make sense to have this constructor with both the offeringId + offering. Instead, I would say to make the main constructor private or internal, and make it store just the paywallId + offeringId + presentedOfferingContext.

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 would also add a @InternalRevenueCatAPI constructor that received both an optional offeringId + presentedOfferingContext. This will be meant to be used by hybrids

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.

Thanks @tonidero, great callouts. Refactored it a bit and implemented both suggestions :)

@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch from d588c0d to 92a1214 Compare May 28, 2026 13:27

@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 makes a lot of sense! Thank you!!

resolvedPresentedOfferingContext = resolvedOffering
?.availablePackages
?.firstOrNull()
?.presentedOfferingContext

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 PR but I'm seeing we have this code (getting the presented offering context from the first package of an offering) in a ton of places. This could/should be an internal function on the Offering object and uses that instead... But I think that should be on a separate PR.

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.

Agree! Was thinking about that as well, will do so in a follow-up

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 here

@@ -139,10 +142,21 @@ public void onError(@NonNull PurchasesError error) {}
VirtualCurrencies cachedVirtualCurrencies = purchases.getCachedVirtualCurrencies();

// trackCustomPaywallImpression API
Offering offering = new 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.

Hmm we could potentially just have this as a parameter to avoid having to create a new instance here, same for the kotlin API tests... NABD though.

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 idea, just updated it :)

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.36364% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.03%. Comparing base (c3e9a45) to head (ea3369c).

Files with missing lines Patch % Lines
...s/paywalls/events/CustomPaywallImpressionParams.kt 82.35% 2 Missing and 1 partial ⚠️
...revenuecat/purchases/common/events/BackendEvent.kt 81.81% 0 Missing and 2 partials ⚠️
...cat/purchases/common/offerings/OfferingsManager.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3424      +/-   ##
==========================================
+ Coverage   79.96%   80.03%   +0.07%     
==========================================
  Files         370      370              
  Lines       15014    15048      +34     
  Branches     2071     2074       +3     
==========================================
+ Hits        12006    12044      +38     
+ Misses       2167     2161       -6     
- Partials      841      843       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rickvdl rickvdl force-pushed the rickvdl/add-presented-offering-context-to-custom-paywall-events branch from a4ed790 to 7564901 Compare June 1, 2026 10:26
@rickvdl rickvdl added this pull request to the merge queue Jun 2, 2026
Merged via the queue into main with commit 9bd3edd Jun 2, 2026
38 checks passed
@rickvdl rickvdl deleted the rickvdl/add-presented-offering-context-to-custom-paywall-events branch June 2, 2026 06: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
…y across SDK (RevenueCat#3513)

Added a small helper function for getting the `PresentedOfferingContext`
from an Offering object as a follow up on
RevenueCat#3424 (comment)

Replacing all occurrences of 
```
offering.availablePackages.firstOrNull()?.presentedOfferingContext
```

with the use of the new helper
```
offering.presentedOfferingContext
```


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Mechanical refactor with equivalent semantics for offerings with
packages; empty offerings still yield null context where callers pass it
through directly.
> 
> **Overview**
> Adds an **`@InternalRevenueCatAPI`**
`Offering.presentedOfferingContext` property (placement/targeting from
the first available package, or **null** when there are no packages) and
updates the public API stub files accordingly.
> 
> Call sites across **purchases** and **RevenueCat UI** that previously
inlined `availablePackages.firstOrNull()?.presentedOfferingContext` now
use `offering.presentedOfferingContext`, including custom paywall
impressions, paywall activity launch options, and embedded paywall
views. **`PaywallViewModel`** keeps a local
`presentedOfferingContextOrDefault` extension that falls back to
`PresentedOfferingContext(identifier)` for paywall analytics when the
offering has no packages.
> 
> Unit tests cover the new property and the empty-offering case for
`CustomPaywallImpressionParams`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
184c137. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
**This is an automatic release.**

## RevenueCat SDK
### ✨ New Features
* Add presented offering context to custom paywall events (RevenueCat#3424) via
Rick (@rickvdl)
* Add Workflows list endpoint (RevenueCat#3509) via Cesar de la Vega (@vegaro)

## RevenueCatUI SDK
### Paywalls_v2
#### 🐞 Bugfixes
* Fix 1px seam between sliding multipage paywall pages (RevenueCat#3526) via Cesar
de la Vega (@vegaro)

### 🔄 Other Changes
* refactor: extract Offering.presentedOfferingContext() helper and apply
across SDK (RevenueCat#3513) via Rick (@rickvdl)
* Add JSON Logic string + array operators (RevenueCat#3485) via Antonio Pallares
(@ajpallares)
* Add ForbiddenPublicSealedClass detekt rule (RevenueCat#3503) via Toni Rico
(@tonidero)
* Update baseline profiles (RevenueCat#3519) via RevenueCat Git Bot (@RCGitBot)
* build(deps): bump fastlane-plugin-revenuecat_internal from `af7bb5c`
to `ce6a7ef` (RevenueCat#3515) via dependabot[bot] (@dependabot[bot])
* Add JSON Logic comparison operators (<, <=, >, >=) (RevenueCat#3484) via Antonio
Pallares (@ajpallares)
* Add JSON Logic arithmetic operators (+, -, *, /, %) (RevenueCat#3483) via
Antonio Pallares (@ajpallares)
* Add WorkflowEvent model and backend serialization (RevenueCat#3486) via Cesar de
la Vega (@vegaro)
* RulesEngine: add JSON Logic predicate evaluator (RevenueCat#3482) via Antonio
Pallares (@ajpallares)
* Add :rules-engine-internal skeleton module (RevenueCat#3478) via Antonio
Pallares (@ajpallares)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Version bump and changelog/docs/CI path updates only; no application
logic changes in the diff.
> 
> **Overview**
> This **automatic release** finalizes **Android SDK 10.8.0** by
replacing **`10.8.0-SNAPSHOT`** with **`10.8.0`** across versioning
(`gradle.properties`, `.version`, `Config.frameworkVersion`), sample
apps, and changelog files.
> 
> Release notes for **10.8.0** are recorded in **`CHANGELOG.md`** /
**`CHANGELOG.latest.md`** (workflows list API, paywall offering context
on custom events, multipage paywall seam fix, rules-engine/JSON Logic
work, etc.). **Docs publishing** now targets **`10.8.0`** on S3, and
**`docs/index.html`** redirects to the new doc URL.
> 
> There are **no functional code changes** in this diff beyond version
strings and release metadata.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c3048b8. 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.

2 participants