UI events for paywall component interactions#3287
Conversation
| LaunchedEffect(pagerState, pageCount, style.componentName, controlInteractionTracker) { | ||
| var previousPage = pagerState.currentPage | ||
| snapshotFlow { pagerState.currentPage }.collect { page -> | ||
| if (page != previousPage) { | ||
| if (skipProgrammaticPageTracking.getAndSet(false)) { | ||
| // Auto-advance scroll; do not emit control interaction (parity with iOS). | ||
| } else { | ||
| val logicalPage = page % pageCount | ||
| controlInteractionTracker.track( | ||
| componentType = PaywallControlType.CAROUSEL, | ||
| componentName = style.componentName, | ||
| componentValue = logicalPage.toString(), | ||
| componentUrl = null, | ||
| ) | ||
| } | ||
| previousPage = page |
There was a problem hiding this comment.
We had a bug in non-looping carousels with auto-advance turned on.
The code always computed the next page as (current + 1) mod page count, which is correct when the carousel loops but wrong when it does not. On the last page it would wrap back to the first page and keep animating forever instead of staying put.
Single-page carousels had the same kind of problem: modulo math still produced a “next” page when there was nowhere real to go. The fix is to only schedule the next auto-advance page when there actually is one, for non-loop mode that means stop once you are on the last page (and bail out for empty or single-page carousels).
Looping behavior is unchanged; we still just increment the page index there.
tonidero
left a comment
There was a problem hiding this comment.
Looking really great! Left a few initial comments but I think it's close!
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… paywalls (#3336) <!-- Thank you for contributing to Purchases! Before pressing the "Create Pull Request" button, please provide the following: --> ### Checklist - [x] If applicable, unit tests - [x] If applicable, create follow-up issues for `purchases-ios` and hybrids ### Motivation Paywall component interaction analytics did not consistently record taps on purchase CTAs for Paywalls V2 (component JSON) or v1 template paywalls. Product and backend need a purchase_button interaction type with stable component_value, package/product identifiers, and URL semantics aligned with how checkout is opened (in-app vs web / browser mode). Resolves: PWENG-32 ### Description - **V2 / component paywalls:** On purchase-related **`ButtonComponentStyle`** actions, emit **`PaywallComponentType.PURCHASE_BUTTON`** via **`paywallPurchaseButtonAction`**, using **`packageForPurchaseButtonInteraction`** for package/product IDs and **`purchaseButtonInteractionComponentUrl`** (built on **`resolveWebCheckoutUrlForInteraction`**) for **`component_url`** (external browser = fully resolved checkout URL; in-app / deep link = package/offering WPL + custom URL fallback as implemented). - **Style factory:** Wire **`PurchaseButtonComponent.name`** into **`ButtonComponentStyle.componentName`** where applicable. - **Legacy templates:** Track the same interaction shape from **`PurchaseButton`** and the validation-warning **`DefaultPaywallView`** purchase path, using **`PaywallLegacyComponentInteraction`** constants for stable **`component_name`** / **`component_value`**. - **Serialization / backend:** Extend **`PaywallComponentType`** with **`purchase_button`** and keep backend flattening in sync; fix **`PaywallEventSerializationTests`** to cast **`BackendStoredEvent.Paywalls`** before reading **`event`**. - **Detekt:** Collapse **`paywallCarouselPageChange`** parameters into **`CarouselPageChangeInteraction`** (with **`@Suppress("LongParameterList")`** on the data class constructor if needed). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes paywall analytics emission and URL resolution for purchase-related buttons across both component (V2) and legacy paywalls, which could affect event payloads and downstream reporting. No purchase/entitlement logic is modified, but incorrect URL/package resolution could misattribute interactions. > > **Overview** > Adds a new paywall component interaction type, `PaywallComponentType.PURCHASE_BUTTON`, and wires backend flattening/serialization to emit the `purchase_button` wire value. > > Updates RevenueCatUI to **track purchase CTA taps** for both component paywalls (purchase-related `ButtonComponentStyle` actions) and legacy templates (including the validation-warning/default paywall), emitting a standardized `purchase_button` interaction with stable `component_value`, optional `component_name`, `currentPackageIdentifier`/`currentProductIdentifier`, and consistent `component_url` semantics. > > Refactors web-checkout URL construction into a shared resolver (`resolveWebCheckoutUrlForInteraction`) and adjusts carousel page-change tracking to use a `CarouselPageChangeInteraction` container to reduce parameter noise; tests are updated/added to validate the new purchase-button interaction shape and stored-event casting. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 02014ea. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Alexander Repty <alex.repty@revenuecat.com>
… tracking event (PWENG-16)
tonidero
left a comment
There was a problem hiding this comment.
Still reviewing but looking good so far! Just some thoughts. Will finish tomorrow 🙇
…n case (PWENG-16)
tonidero
left a comment
There was a problem hiding this comment.
Looks like some tests are failing. But I think the changes here makes sense! Amazing job!! Complex feature touching many things 🙌
| currentPackage = currentPackage, | ||
| state = state, | ||
| ) | ||
| componentInteractionTracker.track( |
There was a problem hiding this comment.
[Not for this PR]
Could be very interesting to have a project with a paywall with all these components and then add some E2E integration tests, either using instrumentation tests, or maestro tests, that verifies the events are sent with the expected properties. We could potentially use the debug event internal API.
This could prove very valuable as a fully E2E tests for these analytics. But as I said, doesn't need to be part of this PR.
There was a problem hiding this comment.
Agreed, I've added this backlog ticket so we can introduce the e2e tests
| fun clear() { | ||
| shouldSkipNextPageChange = false | ||
| } | ||
| } |
There was a problem hiding this comment.
Auto-advance flag may suppress user carousel tracking
Low Severity
ProgrammaticPageTrackingFlag is a plain mutable holder shared between two coroutines (auto-advance LaunchedEffect and snapshotFlow page-change collector). If a user swipe interrupts an auto-advance animation, the currentPage snapshot change from the user gesture can fire before the CancellationException catch block calls clear(), causing the one-shot flag to be consumed by the user-initiated page change — silently dropping that user interaction event.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 848b970. Configure here.
📸 Snapshot Test21 modified, 567 unchanged
🛸 Powered by Emerge Tools |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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 69c91fc. Configure here.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3287 +/- ##
==========================================
+ Coverage 79.21% 79.39% +0.18%
==========================================
Files 354 354
Lines 14121 14252 +131
Branches 1948 1950 +2
==========================================
+ Hits 11186 11316 +130
Misses 2133 2133
- Partials 802 803 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
**This is an automatic release.** ## RevenueCat SDK ### 🐞 Bugfixes * fix: move Google BillingClient connection off the main thread (RevenueCat#3369) via Toni Rico (@tonidero) * [EXTERNAL] fix(google): guard showInAppMessages against BillingClient runtime crashes (RevenueCat#3367) by @matteinn (RevenueCat#3368) via Monika Mateska (@MonikaMateska) ## RevenueCatUI SDK ### Paywallv2 #### 🐞 Bugfixes * Add Workflows network layer (RevenueCat#3300) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * Fix `revenuecat.useWorkflowsEndpoint` compiler flag (RevenueCat#3374) via Cesar de la Vega (@vegaro) * Create paywall from workflow response. Add `USE_WORKFLOWS_ENDPOINT` BuildConfig (RevenueCat#3350) via Cesar de la Vega (@vegaro) * Refactor: Remove unnecessary lint suppressions (RevenueCat#3373) via cursor[bot] (@cursor[bot]) * Bump fastlane-plugin-revenuecat_internal from `a1eed48` to `b822f01` (RevenueCat#3371) via dependabot[bot] (@dependabot[bot]) * Bump fastlane from 2.232.2 to 2.233.0 (RevenueCat#3370) via dependabot[bot] (@dependabot[bot]) * Attempt to fix `AssertionError` "ms is denormalized" in `QueryPurchasesUseCaseTest` (RevenueCat#3361) via Cesar de la Vega (@vegaro) * Update baseline profiles (RevenueCat#3296) via Jaewoong Eum (@skydoves) * fix: reduce precision for flaky HeaderDirectHeroImage snapshot (RevenueCat#3362) via Cesar de la Vega (@vegaro) * Fix test failures reported twice (RevenueCat#3360) via Cesar de la Vega (@vegaro) * refactor: extract `updateStateFromOffering` in `PaywallViewModel` (RevenueCat#3359) via Cesar de la Vega (@vegaro) * [Fix] Include parent tabs component_name in tab-control switch interaction events (RevenueCat#3358) via Monika Mateska (@MonikaMateska) * Refactor: Remove unnecessary lint suppressions (RevenueCat#3348) via cursor[bot] (@cursor[bot]) * fix: always upload CI test results even when tests fail (RevenueCat#3357) via Cesar de la Vega (@vegaro) * refactor: extract `RevenueCatDialogScaffold` (RevenueCat#3355) via Cesar de la Vega (@vegaro) * Fix Slack notifications for nightly integration tests (RevenueCat#3354) via Toni Rico (@tonidero) * UI events for paywall component interactions (RevenueCat#3287) via Monika Mateska (@MonikaMateska) * Bump fastlane-plugin-revenuecat_internal from `20911d1` to `a1eed48` (RevenueCat#3351) via dependabot[bot] (@dependabot[bot]) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Primarily a version bump and release automation updates (docs deploy/redirect and changelog); no functional library code changes beyond updating embedded version constants. > > **Overview** > Cuts the `10.2.1` release by updating version references across the repo (Gradle `VERSION_NAME`, internal `frameworkVersion`, sample/test app dependency pins, and `.version`). > > Updates the docs release pipeline and website redirect to publish and point at `10.2.1`, and refreshes `CHANGELOG.md`/`CHANGELOG.latest.md` with the 10.2.1 release notes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a0a325b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Checklist
purchases-iosand hybridsMotivation
This PR is part of the “Posting UI Events to Integrations” initiative and focuses on enabling
paywall_component_interactionon Android so UI behavior events can flow through the existing paywall event pipeline and be forwarded to integrations without app-side callback wiring.It addresses the current gap where UI events exist on other platforms but are not consistently represented in Android’s paywall event payloads—including package selection, package-selection sheet lifecycle, and extended interaction metadata aligned with iOS. This is especially important for the upcoming Campaigns/Workflows/Checkpoints direction and for high-integration customers (e.g. Leadtech).
Resolves: PWENG-15
Description
This PR adds Android support for
paywall_component_interactionand wires control interaction metadata through purchases + RevenueCatUI, aligned with the iOS wire format and semantics (same backend field names as iOS’s paywall component interaction map, e.g.origin_package_id,current_package_id, etc.).What was added
paywall_component_interaction(unchanged type string on Android; payload extended to match iOS).component_type,component_name,component_value,component_url(optional)ComponentInteractionData/ backend map):origin_index,destination_index,default_indexorigin_context_name,destination_context_nameorigin_package_id,destination_package_id,default_package_idorigin_product_id,destination_product_id,default_product_idcurrent_package_id,resulting_package_id,current_product_id,resulting_product_idtab,switch,carousel,button,textpackage(selectable package row)package_selection_sheet(bottom sheet open/close lifecycle)PaywallControlInteractionDataexpanded;PaywallEvent/PaywallStoredEventround-tripBackendEvent.Paywalls+BackendStoredEvent/PaywallStoredEventmapping viaPaywallControlInteractionData.toBackendControlFields()→BackendPaywallControlFieldsPaywallViewModel.trackControlInteraction(PaywallControlInteractionData)as the primary entry point; 4-parameter overload builds a minimalPaywallControlInteractionDatafor legacy call sitesPurchases.track(PaywallEvent)unchanged as the sinkLocalPaywallControlInteractionTrackernow takes a singlePaywallControlInteractionDataargumentpaywallPackageSelectionSheetOpen/Close,paywallPackageRowSelection,paywallTierSelection)nameonPackageComponent→PackageComponentStyle.componentNameforcomponent_nameon package rows when the dashboard provides itUI coverage included
V2 (components paywall)
on/off)navigate_to_url+ URL)package):component_value= destination package id; origin/destination/default package + product ids when applicablepackage_selection_sheet):component_value=open;current_*= root selection at open timecomponent_value=close;current_*= selection while sheet was open;resulting_*= root afterresetToDefaultPackage()V1 (template / legacy paywall)
Footeris not shown)Semantics used
component_namenamewhen available (including package components when JSON includesname)all_plans_button,restore_button,terms_link,privacy_link,tier_selector)component_valuerestore_purchases,navigate_to_terms,navigate_to_privacy_policy,toggle_all_plans,navigate_to_url,open/closefor package sheet, etc.)on/offcurrent/resultingpair on dismiss)Note
Medium Risk
Adds a new paywall event type and threads many new optional analytics fields through serialization, backend event mapping, and UI interaction points; mistakes could change event payloads or increase event volume but core purchase logic is largely untouched.
Overview
Adds support for a new paywall event type,
PaywallEventType.COMPONENT_INTERACTION, including a typed payload (PaywallComponentInteractionData) and backend wire fields (component metadata, indices/context names, and package/product lifecycle identifiers). These fields are plumbed throughBackendEvent.Paywalls, stored-event conversion (PaywallStoredEvent/BackendStoredEvent), and request serialization, with new/updated tests verifying round-trip encoding and backend mapping.Extends the paywall components data model to carry optional dashboard
namevalues (e.g.,ButtonComponent,CarouselComponent,PackageComponent,StackComponent,TabsComponent,TextComponent) and threads those into UI styles. RevenueCatUI now emits component-interaction tracking for V2 components (buttons, tabs/toggles, carousel page changes with auto-advance suppression, package selection rows, sheet open/close, and text link taps) and adds comparable tracking calls for key legacy/V1 paywall actions; paywall impression tracking is also gated behindLaunchedEffectkeys to avoid redundant calls.Reviewed by Cursor Bugbot for commit b2e6c69. Bugbot is set up for automated code reviews on this repo. Configure here.