Skip to content

UI events for paywall component interactions#6523

Merged
MonikaMateska merged 85 commits intomainfrom
monika/UI-events/paywall-control-interaction
Apr 16, 2026
Merged

UI events for paywall component interactions#6523
MonikaMateska merged 85 commits intomainfrom
monika/UI-events/paywall-control-interaction

Conversation

@MonikaMateska
Copy link
Copy Markdown
Member

@MonikaMateska MonikaMateska commented Mar 26, 2026

Checklist

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

Motivation

This PR is part of the “Posting UI Events to Integrations” initiative and focuses on enabling paywall_component_interaction on iOS 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 but are not consistently surfaced in integration-friendly payloads. 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 iOS support for paywall_control_interacted and wires control interaction metadata end-to-end through RevenueCat + RevenueCatUI.

What was added

  • Added paywall event support for:
    • paywall_control_interaction
  • Added interaction fields:
    • component_type
    • component_name
    • component_value
    • origin_package_identifier
    • destination_package_identifier
    • default_package_identifier
    • origin_product_identifier
    • destination_product_identifier
    • default_product_identifier
    • current_package_identifier
    • resulting_package_identifier
    • current_product_identifier
    • resulting_product_identifier
  • Added component type enum coverage:
    • tab, switch (wire value), carousel, button, package, package_selection_sheet
  • Updated event propagation:
    • paywall event model
    • feature event mapping (hybrid-facing)
    • networking request encoding
  • Added tracking API surface in UI flow via PaywallEventTracker + componentInteractionLogger environment wiring.

UI coverage included

  • V2 controls
    • tab control button
    • tab control toggle (on / off)
    • button actions (action discriminator values)
    • carousel page changes (user-driven)
    • package row selection
    • package-selection sheet lifecycle (open / close)
  • V1 controls (in scope)
    • all plans toggle
    • restore purchases
    • terms link
    • privacy link
    • tier selector
    • package selection

Semantics used

  • component_name
    • V2: use builder JSON name when available (no ID exposure)
    • package-selection sheet: use actual sheet component name from JSON
    • package/tier selection helpers updated to pass real component names when available
    • V1: context-based names (all_plans_button, restore_button, terms_link, privacy_link, etc.)
  • component_value
    • aligned with action-style discriminators where applicable (restore_purchases, navigate_to_terms, navigate_to_privacy_policy, toggle_all_plans)
    • toggle values use on / off
    • tier selector uses tier display name when available
    • package-selection sheet uses open / close
  • package identifier semantics
    • plan-selection interactions use origin* / destination* / default*
    • sheet lifecycle uses:
      • current*:
        • on open: root paywall selection at sheet presentation
        • on close: sheet-context selection at dismiss
      • resulting*:
        • on close: root paywall selection after dismiss (captures reset/revert behavior)

Note

Medium Risk
Touches paywall analytics/event emission and session lifecycle across UI and core SDK, which could affect event deduping/ordering or missing/duplicate close events if session state is mishandled.

Overview
Adds a new paywall_component_interacted paywall event and maps its payload into hybrid/integration feature event fields (component type/name/value/url plus index/context and package/product identifiers).

Introduces PaywallEventTracker and a componentInteractionLogger environment value to manage per-paywall session state and emit interaction events from RevenueCatUI (V1 + V2 controls like package selection, tabs/toggles, carousel swipes, buttons/links, markdown links, and package-selection sheet open/close). Paywall session IDs are now stored per view/session and reused across impression/close/interactions, and close tracking/reset paths in PurchaseHandler/UIKit paywall dismissal are updated accordingly.

Extends paywall component models to carry optional name fields (e.g. stack/button/purchase button/package/carousel) and adjusts previews/tests/mocks to support the new tracker and event dispatching behavior.

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

@RevenueCat-Danger-Bot
Copy link
Copy Markdown

RevenueCat-Danger-Bot commented Mar 26, 2026

4 Warnings
⚠️ Size check is being bypassed due to the presence of the label "danger-bypass-size-limit"
⚠️ Size increase: 146.74 KB
⚠️ RevenueCat.xcodeproj is out of sync.

The following Swift files were added but are missing from RevenueCat.xcodeproj:
RevenueCatUI/Data/ComponentInteractionData+Factories.swift
RevenueCatUI/Templates/V2/EnvironmentObjects/PlanSelectionDefaultPackage.swift
Tests/RevenueCatUITests/Helpers/PaywallEventTrackerTestDispatcher.swift
Tests/RevenueCatUITests/Templates/TierSelectorComponentInteractionTests.swift
Tests/UnitTests/Paywalls/Components/ButtonComponentViewModelInteractionTests.swift

To fix: open RevenueCat.xcodeproj in Xcode, add/remove the files above in the appropriate target. Check where similar files in the same directory are assigned if you're unsure which target to use.

⚠️ Public enums should not be added. Consider using a struct with static properties or an @objc enum instead.

The following files contain new public enums:
• Sources/Paywalls/Components/PaywallPurchaseButtonComponent.swift
• Sources/Paywalls/Events/PaywallEvent.swift

Generated by 🚫 Danger

@emerge-tools
Copy link
Copy Markdown

emerge-tools Bot commented Mar 26, 2026

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 17.6 MB ⬆️ 199.2 kB (1.15%) 63.0 MB ⬆️ 762.9 kB (1.23%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.0 MB ⬆️ 59.8 kB (1.54%) 12.0 MB ⬆️ 177.1 kB (1.52%) ✅ Approved
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.0 MB ⬆️ 76.5 kB (1.29%) 26.5 MB ⬆️ 272.9 kB (1.05%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.1 MB ⬆️ 63.7 kB (1.59%) 10.4 MB ⬆️ 172.1 kB (1.69%) ⏳ Needs approval

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 762.9 kB (1.23%)
Total download size change: ⬆️ 199.2 kB (1.15%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 214.0 kB
RevenueCatUI.PaywallViewController.PaywallViewController ⬆️ 23.7 kB
Code Signature ⬆️ 18.6 kB
RevenueCat.InternalAPI.InternalAPI ⬆️ 18.4 kB
DYLD.Exports ⬆️ 14.6 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 177.1 kB (1.52%)
Total download size change: ⬆️ 59.8 kB (1.54%)

Largest size changes

Item Install Size Change
RevenueCat.PurchasedTransactionDataEncodedWrapper.value witness ⬆️ 11.2 kB
RevenueCat.LocalTransactionMetadata.value witness ⬆️ 11.1 kB
RevenueCat.PaywallEvent.value witness ⬆️ 8.8 kB
RevenueCat.PurchasesOrchestrator.CachedPurchaseContext.value witn... ⬆️ 7.0 kB
RevenueCat.PurchasedTransactionData.value witness ⬆️ 7.0 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 272.9 kB (1.05%)
Total download size change: ⬆️ 76.5 kB (1.29%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 39.4 kB
DYLD.String Table ⬆️ 33.8 kB
RevenueCat.PurchasedTransactionDataEncodedWrapper.value witness ⬆️ 11.2 kB
RevenueCat.LocalTransactionMetadata.value witness ⬆️ 11.1 kB
RevenueCat.PaywallEvent.value witness ⬆️ 8.8 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 172.1 kB (1.69%)
Total download size change: ⬆️ 63.7 kB (1.59%)

Largest size changes

Item Install Size Change
RevenueCat.PurchasedTransactionDataEncodedWrapper.value witness ⬆️ 11.2 kB
RevenueCat.LocalTransactionMetadata.value witness ⬆️ 11.1 kB
RevenueCat.PaywallEvent.value witness ⬆️ 8.8 kB
RevenueCat.PurchasesOrchestrator.CachedPurchaseContext.value witn... ⬆️ 7.0 kB
RevenueCat.PurchasedTransactionData.value witness ⬆️ 7.0 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Comment trigger: Size diff threshold of 100.00kB exceeded

@MonikaMateska MonikaMateska marked this pull request as ready for review March 27, 2026 10:53
@MonikaMateska MonikaMateska requested review from a team as code owners March 27, 2026 10:53
Comment thread RevenueCatUI/Templates/V2/Components/Carousel/CarouselComponentView.swift Outdated
Copy link
Copy Markdown
Contributor

@JZDesign JZDesign left a comment

Choose a reason for hiding this comment

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

Great start, Still working through it, but here are my initial thoughts

Comment thread Sources/Paywalls/Events/PaywallEvent.swift Outdated
Comment thread RevenueCatUI/Templates/V2/Components/Carousel/CarouselComponentView.swift Outdated
Comment thread Package.swift Outdated
Comment thread RevenueCatUI/Views/FooterView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/Components/Tabs/TabControlButtonComponentView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/Components/Tabs/TabControlToggleComponentView.swift Outdated
Comment thread Sources/Paywalls/Components/PaywallButtonComponent.swift Outdated
Comment thread RevenueCatUI/Purchasing/PurchaseHandler.swift
Comment thread RevenueCatUI/Templates/V2/Components/Tabs/TabControlToggleComponentView.swift Outdated
Comment thread RevenueCatUI/Purchasing/PurchaseHandler.swift
Comment on lines -72 to -79
.onChangeOf(tabControlContext.selectedTabId) { newSelectedTabId in
let newIsOn = computeIsOn(
selectedTabId: newSelectedTabId,
tabIds: tabControlContext.tabIds
)
if self.isOn != newIsOn {
self.isOn = newIsOn
}
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 is still 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.

We don’t need it anymore because the toggle no longer keeps a separate @State copy of “on vs off.” selectedTabId (via TabControlContext) is the only source of truth, and the control uses a Binding whose getter derives isOn from selectedTabId and tabIds. When something else updates selectedTabId, @published triggers a view update, SwiftUI re-reads that getter, and the toggle reflects the new state without us manually assigning into local state

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.

@facumenzella would you take a look?

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 you were the author of this stuff for various reasons that I can't recall now

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.

Okay, Facundo is on vacation These are the PRs I'm thinking of

#5982
#5929

As long as it does not regress this behavior I'm good. But I will need to consider the best testing paths

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.

Just confirming here as well that it does not regress the behavior, all the flows behave the same.

….com:RevenueCat/purchases-ios into monika/UI-events/paywall-control-interaction

# Conflicts:
#	RevenueCatUI/Templates/V2/Components/Tabs/TabControlToggleComponentView.swift
Comment thread RevenueCatUI/UIKit/PaywallViewController.swift
Copy link
Copy Markdown
Contributor

@JZDesign JZDesign left a comment

Choose a reason for hiding this comment

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

Blocking until we:

  1. test that tab selection logic
  2. Discuss the open thread we have in slack about URLs

Comment thread RevenueCatUI/Templates/Template7View.swift
@RevenueCat RevenueCat deleted a comment from emerge-tools Bot Apr 16, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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

) {
self.componentInteractionLogger(.paywallPurchaseButtonAction(
componentName: self.viewModel.componentName,
componentValue: self.viewModel.method?.description ?? "",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty component_value for web purchase button interaction

Low Severity

In logPurchaseButtonInteractionForWeb, componentValue falls back to "" when self.viewModel.method is nil, producing an empty string in the analytics event. The parallel logPurchaseButtonInteractionForInApp has a meaningful fallback of PaywallComponent.PurchaseButtonComponent.Method.inAppCheckout.description. Since component_value is a non-optional field that's always serialized into the paywall_component_interacted payload, a "" value will silently appear in integration pipelines rather than being filtered or flagged, making it harder to diagnose misconfigured components.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f94fcb7. Configure here.

@MonikaMateska MonikaMateska merged commit c23c3cd into main Apr 16, 2026
35 of 39 checks passed
@MonikaMateska MonikaMateska deleted the monika/UI-events/paywall-control-interaction branch April 16, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

analytics danger-bypass-size-limit Apply this label to bypass Dangerbot's size limit. pr:other

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants