Skip to content

Add placement and targeting context to paywall events#3253

Merged
rickvdl merged 7 commits into
mainfrom
feat/paywall-events-placement-targeting
Apr 28, 2026
Merged

Add placement and targeting context to paywall events#3253
rickvdl merged 7 commits into
mainfrom
feat/paywall-events-placement-targeting

Conversation

@dpannasch

@dpannasch dpannasch commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Include presentedOfferingContext (placementIdentifier, targetingRevision, targetingRuleId) in paywall event serialization so that events & charts can utilize those dimensions.

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-ios and hybrids

Motivation

Description


Note

Medium Risk
Changes the schema of emitted paywall events (new optional field), which could impact backend analytics/parsing if assumptions are strict, though the field is nullable and covered by compatibility tests.

Overview
Paywall backend event payloads now optionally include a nested presented_offering_context object (placement + targeting metadata) so downstream processing can segment paywall events by placement/targeting.

Conversion paths (PaywallEvent.toBackendStoredEvent and PaywallStoredEvent.toBackendEvent) populate this new field via PresentedOfferingContextData.fromContext, which omits the object entirely when no placement/targeting is present, and tests were added to validate request JSON, round-trip serialization, and backward-compatible deserialization of older stored events without the new field.

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

@rickvdl rickvdl added the pr:feat A new feature label Mar 20, 2026
@rickvdl rickvdl force-pushed the feat/paywall-events-placement-targeting branch from 2f8a6fe to a7a725f Compare March 20, 2026 15:23
@codecov

codecov Bot commented Mar 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.39%. Comparing base (b53859c) to head (9e64fda).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...revenuecat/purchases/common/events/BackendEvent.kt 90.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3253      +/-   ##
==========================================
+ Coverage   79.36%   79.39%   +0.02%     
==========================================
  Files         361      361              
  Lines       14458    14473      +15     
  Branches     1964     1968       +4     
==========================================
+ Hits        11475    11491      +16     
+ Misses       2190     2188       -2     
- Partials      793      794       +1     

☔ 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 feat/paywall-events-placement-targeting branch from 12fc978 to d872299 Compare March 23, 2026 12:43
@rickvdl rickvdl marked this pull request as ready for review March 23, 2026 12:47
@rickvdl rickvdl requested review from a team as code owners March 23, 2026 12:47
@rickvdl rickvdl changed the title DRAFT: Add placement and targeting rule to paywall events Add placement and targeting context to paywall events Mar 23, 2026

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

Looking great! If we've tested this and the backend is already accepting the new propert, let's 🚢

Comment thread purchases/src/main/kotlin/com/revenuecat/purchases/common/events/BackendEvent.kt Outdated
@rickvdl rickvdl force-pushed the feat/paywall-events-placement-targeting branch from 9b96692 to d7bb203 Compare April 23, 2026 09:46
@rickvdl rickvdl requested a review from a team as a code owner April 23, 2026 09:46

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

🚢

dpannasch and others added 7 commits April 28, 2026 12:21
Include presentedOfferingContext (placementIdentifier, targetingRevision,
targetingRuleId) in BackendEvent.Paywalls serialization so the backend
can calculate conversion rates per placement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add fromContext factory on PresentedOfferingContextBackend that
encapsulates the nil-check logic, removing duplication between
BackendStoredEvent and PaywallStoredEvent conversion paths.
Verify that placement and targeting context survives the full
PaywallStoredEvent serialize → deserialize → toBackendEvent flow,
and that events without placement produce null backend context.
- Add placement-only round-trip test (no targeting)
- Add targeting-only round-trip test (no placement)
- Add test through PaywallEvent.toBackendStoredEvent() path
  verifying placement and targeting are preserved
Align naming with iOS for cross-platform consistency.
- Use import instead of fully-qualified name for PresentedOfferingContext
- Assert full PresentedOfferingContextData objects instead of individual fields
- Add backward compat test for old stored events without presented_offering_context
@rickvdl rickvdl force-pushed the feat/paywall-events-placement-targeting branch from d7bb203 to 9e64fda Compare April 28, 2026 10:21
@rickvdl rickvdl added this pull request to the merge queue Apr 28, 2026
Merged via the queue into main with commit 44b0a1f Apr 28, 2026
36 checks passed
@rickvdl rickvdl deleted the feat/paywall-events-placement-targeting branch April 28, 2026 12:12
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request May 5, 2026
**This is an automatic release.**

## RevenueCat SDK
### ✨ New Features
* Unified StoreReplacementMode API (RevenueCat#3234) via Will Taylor
(@fire-at-will)
* Add placement and targeting context to paywall events (RevenueCat#3253) via Dan
Pannasch (@dpannasch)
### 🐞 Bugfixes
* Fix null Placements when offering_ids_by_placement is absent (RevenueCat#3254)
via Dan Pannasch (@dpannasch)

## RevenueCatUI SDK
### Paywallv2
#### ✨ New Features
* Wire multipage workflow navigation into PaywallViewModel (RevenueCat#3381) via
Cesar de la Vega (@vegaro)

### 🔄 Other Changes
* Add `triggerType` to `WorkflowTrigger` (RevenueCat#3393) via Cesar de la Vega
(@vegaro)
* Extract private function `NavigateTo.toPaywallAction` (RevenueCat#3392) via
Cesar de la Vega (@vegaro)
* Bump revenucatui-tests gradle cache key (RevenueCat#3391) via Toni Rico
(@tonidero)
* Create `WorkflowTriggerType` and `WorkflowTriggerActionType` (RevenueCat#3386)
via Cesar de la Vega (@vegaro)
* Update baseline profiles (RevenueCat#3390) via RevenueCat Git Bot (@RCGitBot)
* Plumb `componentId` through buttons on workflow interactions (RevenueCat#3380)
via Cesar de la Vega (@vegaro)
* Add `ButtonComponent.Action.Workflow` (RevenueCat#3385) via Cesar de la Vega
(@vegaro)
* Add `componentId` to `ButtonCoomponentStyle` (RevenueCat#3384) via Cesar de la
Vega (@vegaro)
* Migrate all suspendCoroutine usages to suspendCancellableCoroutine
(RevenueCat#3365) via Jaewoong Eum (@skydoves)
* Add `WorkflowNavigator` for multipage workflow step navigation (RevenueCat#3379)
via Cesar de la Vega (@vegaro)
* build(deps): bump fastlane-plugin-revenuecat_internal from `b822f01`
to `d24ab26` (RevenueCat#3383) via dependabot[bot] (@dependabot[bot])
* Add `id` field to `ButtonComponent` (RevenueCat#3377) via Cesar de la Vega
(@vegaro)
* Add CI workflows for generating Baseline Profiles (RevenueCat#3372) via Jaewoong
Eum (@skydoves)
* add min sdk level for paywalls and customer center (RevenueCat#2465) via
Muhammad-Sharif Moustafa (@mshmoustafa)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk release bookkeeping: updates version strings, changelogs, and
documentation deployment targets without changing runtime logic beyond
the reported version identifier.
> 
> **Overview**
> Cuts the `10.3.0` release by updating all version references from
`10.3.0-SNAPSHOT` to `10.3.0` across build config (`gradle.properties`,
`.version`, `Config.frameworkVersion`) and sample/test app dependency
pins.
> 
> Updates release documentation publishing to sync Dokka output to the
`10.3.0` S3 path and changes `docs/index.html` to redirect to `10.3.0`.
Also promotes release notes by moving `CHANGELOG.latest.md` entries into
a new `CHANGELOG.md` section for `10.3.0`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
056ce62. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
rickvdl added a commit to RevenueCat/purchases-js that referenced this pull request Jun 2, 2026
## Summary
- Include `placement_identifier`, `targeting_revision`, and
`targeting_rule_id` in paywall events under a
`presented_offering_context` nested object
- Field is omitted when no placement or targeting data exists
- Consistent with iOS and Android implementations

## Related PRs
- iOS: RevenueCat/purchases-ios#6476
- Android: RevenueCat/purchases-android#3253

## Test plan
- [x] Unit tests for all context permutations (full, placement-only,
targeting-only, none, empty string)
- [x] JSON round-trip serialization tests
- [x] Integration test verifying context flows from offering to event
payload
- [x] Manual testing with targeting rules and placements in demo app
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