Skip to content

Conversation

@JZDesign
Copy link
Contributor

@JZDesign JZDesign commented Nov 14, 2025

Checklist

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

Motivation

Users want a way to control when to show or hide certain elements of their paywall. Commonly this is based on screen size, but there are other reasons this may occur as well.

Description

Josh wrote the majority of the iOS PR months ago. I picked it up and added the selected package bit, then I had codex look at those changes and replicate them here in the android project.

@emerge-tools
Copy link

emerge-tools bot commented Nov 20, 2025

📸 Snapshot Test

570 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
0 0 0 0 312 0 N/A
TestPurchasesUIAndroidCompatibility Paparazzi
com.revenuecat.testpurchasesuiandroidcompatibility.paparazzi
0 0 0 0 258 0 N/A

🛸 Powered by Emerge Tools

@JZDesign JZDesign added the pr:feat A new feature label Nov 20, 2025
@codecov
Copy link

codecov bot commented Nov 21, 2025

Codecov Report

❌ Patch coverage is 71.05263% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.22%. Comparing base (c37276b) to head (95c4589).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...c/main/kotlin/com/revenuecat/purchases/UiConfig.kt 11.11% 8 Missing ⚠️
...es/paywalls/components/common/ComponentOverride.kt 88.88% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2843      +/-   ##
==========================================
- Coverage   78.28%   78.22%   -0.07%     
==========================================
  Files         330      330              
  Lines       12753    12785      +32     
  Branches     1739     1747       +8     
==========================================
+ Hits         9984    10001      +17     
- Misses       2038     2046       +8     
- Partials      731      738       +7     

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

#2843 renamed a key from `intro_offer` to `introductory_offer` which
would break current paywalls that use `intro_offer` for "Text field for
an introductory offer" section in the builder.
We noticed that phones get categorized as tablets in landscape, and
tablets as desktops. I changed the calculation to use the width in
portrait (the shortest dimension) instead. But we still need to discuss
if this is what we want.
@JZDesign JZDesign requested a review from JayShortway December 1, 2025 19:56
@JZDesign JZDesign requested a review from vegaro December 1, 2025 19:56
@JZDesign JZDesign marked this pull request as ready for review December 1, 2025 19:56
@JZDesign JZDesign requested a review from a team as a code owner December 1, 2025 19:56
Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

Still reviewing but posting what I have so far


@Serializable
enum class EqualityOperatorType {
@SerialName("=") // WIP… Should we make this human language from the server like in and not_in?
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm could be eq and not_eq to have the same style as the other operators indeed... but not a strong preference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out, Khepri requires this. It's used a lot in other areas.

"selected" to { Condition.Selected.serializer() },
"orientation" to { Condition.Orientation.serializer() },
"screen_size" to { Condition.ScreenSize.serializer() },
"selected_package" to { Condition.SelectedPackage.serializer() },
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between selected and selected_package? And also between intro_offer and introductory_offer_available + multiple_intro_offers and multiple_intro_offers_available? Feels like a rename to support the new condition system, but basically this also means it would default to unsupported in older versions of the SDK... Not sure if that's worth it?

If I'm wrong and they are supposed to be different conditions, we might want to document the difference

Copy link
Contributor Author

@JZDesign JZDesign Dec 2, 2025

Choose a reason for hiding this comment

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

Good questions. Long story short, our foresight was only kind of right when we started this work many months ago. As development continued, other scenarios came up, and the use case for the keys that were already in the code could have been clearer but these are the meanings. I have a doc on this that I can throw into notion, or perhaps this is a safe enough one that could live in the repo… though it would be duplicated.

Key Purpose
selected The component itself is selected, like a tab, a package, a switch, etc.,
selected_package The selected package matches the given array conditions
intro_offer The selected package includes an intro offer you are eligible for
introductory_offer_available any package in the paywall includes an introductory offer you are eligible for, selected or not
multiple_intro_offers The selected package has multiple intro offers that you are eligible for
multiple_intro_offers_available any package in the paywall includes multiple intro offers you are eligible for, selected or not
app_version The consuming application version matches the given comparison

Note

Edited, adding application version to allow for fine tuned control for various reasons like being able to add new conditions that are not supported by older SDKs and giving the user the ability to ensure the paywall renders appropriately for their use case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohh thanks for the explanation. That makes sense! Then, I would indeed add that as docs to the repo, I think it should be fine, and could help understand the difference between them.

var snapshot by mutableStateOf(ScreenConditionSnapshot())
private set

fun updateLayoutSize(widthDp: Float, heightDp: Float) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I'm not in love with making all these methods available to callers, since they could potentially be called somewhere else, when they are only intended to be called by the rememberScreenConditionState and trackScreenCondition below... But I'm not sure what would be the best approach...

We could make rememberScreenConditionState a companion object function, and would allow us to make some of these private, but wouldn't work for trackScreenCondition since that's an extension function of Modifier... So I think it's ok to leave as is for now...

Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

Only that last VideoComponentState question, but it's looking great!

selectedPackageProvider: () -> Package?,
selectedTabIndexProvider: () -> Int,
screenConditionProvider: () -> ScreenCondition,
introOfferAvailability: IntroOfferAvailability = IntroOfferAvailability(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here. We probably should remove the default IntroOfferAvailability, probably a leftover? :)


@JvmSynthetic
fun update(
windowSize: WindowWidthSizeClass? = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I'm not sure this is right... in the other components states, the update method includes the screenCondition, so we can update it, but not for the VideoComponentState. Is there a reason for that difference? This also means that we will keep using the initialScreenCondition for the PresentedPartial even if the screen changes, so not sure if that's right... Maybe we're handling that in the view layer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, this here isn't actually the issue but I'm glad it flagged things for you, because there was an issue that I fixed where build partial was called

@JZDesign JZDesign marked this pull request as draft December 4, 2025 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants