Skip to content

Reconcile package selection when a window size rule hides the selected package - #4198

Draft
joshdholtz wants to merge 5 commits into
mainfrom
feat/window-size-selection-reconcile
Draft

joshdholtz wants to merge 5 commits into
mainfrom
feat/window-size-selection-reconcile

Conversation

@joshdholtz

@joshdholtz joshdholtz commented Sep 8, 2026

Copy link
Copy Markdown
Member

Stacked on #4193. Closes the selection gap flagged in review: a package hidden by a window size rule could stay selected and purchasable.

What

  • reconcileSelectionForWindowSize on PaywallState.Loaded.Components: when the measured paywall bounds change and the selected package no longer resolves visible, selection moves to the authored default if it renders, else the first visible package. A visible selection is never moved; if nothing is visible, selection stays put.
  • Runs from MeasurePaywallBounds, keyed on the measured bounds — also covers the initial selection, which is resolved before the bounds are known.
  • Tab switches and sheet-dismiss resets restore selections that were resolved without the bounds, so those paths reconcile too.
  • resolvesVisible and the default-selection helpers take an optional measured size; the initial-selection path is unchanged (window unknown).

Design note

When nothing resolves visible at the current size, the hidden selection is kept — matching iOS. Rules that hide every package at some size are an authoring error; the plan is to guard against that in the dashboard (validation when window rules can leave zero visible packages) rather than guess at runtime.

Scope note for reviewers

Size-class (screen condition) rules have the same gap and keep their existing pinned-to-COMPACT behavior — reconciling those changes behavior of already-shipped paywalls, so it's left as a deliberate follow-up decision. Mirrors iOS reconcileSelection (window-size scope).

Tests

  • WindowSizeSelectionReconcileTests: moves off a hidden package, keeps a visible one, no-op on unknown size, keeps selection when nothing is visible, prefers a visible authored default, no flapping on resize-back, height rules.
  • PaywallStateLoadedComponentsPackageSelectionTests: tab switch onto a hidden remembered default, sheet-dismiss reset restoring a hidden default, user-selected package hidden by a rule.

🤖 Generated with Claude Code


Note

Medium Risk
Changes which package is selected for purchase across resize, tabs, and sheet dismiss, but behavior is guarded by extensive tests and explicit iOS parity rules.

Overview
Fixes a gap where a package hidden by window width/height or size-class visibility rules could remain selected and purchasable after bounds change, tab switch, or sheet reset—initial selection still runs before measure.

reconcileSelectionForWindowSize on PaywallState.Loaded.Components re-checks whether the current selection would render visible (using measured paywall bounds for dimension rules and windowScreenCondition from the app window for compact/medium/expanded). If not, selection moves to the tab’s authored default, then other visible fallbacks; if nothing is visible, selection stays put (iOS parity). Replacement candidates are evaluated unselected; the keep-check uses selected view state so selected-condition rules don’t oscillate.

MeasurePaywallBounds now writes windowScreenCondition and runs reconciliation in a LaunchedEffect keyed on bounds, screen condition, and state list (including prewarmed workflow states). Tab update, resetToDefaultPackage, and peekDefaultPackageUniqueIdAfterSheetDismiss use the same visibility-aware helpers so analytics and dismiss match reset behavior.

resolvesVisible / default-selection helpers accept optional measured size and screen condition; FakePaywallState stops synthesizing extra always-visible package components that broke visibility tests. New unit and Compose window-resize tests cover resize, tabs, medium/selected rules, and late-added states.

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

@emerge-tools

emerge-tools Bot commented Sep 8, 2026

Copy link
Copy Markdown

📸 Snapshot Test

632 unchanged

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

🛸 Powered by Emerge Tools

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.70%. Comparing base (3172730) to head (1763d7b).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4198   +/-   ##
=======================================
  Coverage   83.70%   83.70%           
=======================================
  Files         456      456           
  Lines       18636    18636           
  Branches     2813     2813           
=======================================
  Hits        15599    15599           
  Misses       2134     2134           
  Partials      903      903           

☔ View full report in Codecov by Harness.
📢 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.

@joshdholtz
joshdholtz force-pushed the feat/window-size-selection-reconcile branch from 307fa8a to 728a0ff Compare September 8, 2026 20:43
Base automatically changed from feat/window-size-conditions to main September 9, 2026 12:40
joshdholtz and others added 3 commits September 11, 2026 11:52
…d package

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both paths restore selections that were resolved before the bounds were
known, so a window-hidden package could become selected without a bounds
change to trigger the reconcile effect. The effect now also keys on the
locally computed bounds inside the measurer, avoiding stale captures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joshdholtz
joshdholtz force-pushed the feat/window-size-selection-reconcile branch from 2fc1831 to 712faad Compare September 11, 2026 16:52
- Keep-check now evaluates the current package as selected, in the
  active tab's context, under the screen condition the measured width
  implies — so packages visible via Medium/Expanded/Selected overrides
  are no longer deselected on resize, and a visible copy on an inactive
  tab no longer masks a hidden copy on the active one.
- Replacement resolution shares the sheet-dismiss priority (tab's
  authored default first) and skips candidates hidden at the current
  size class.
- peekDefaultPackageUniqueIdAfterSheetDismiss evaluates at the measured
  bounds, including remembered ids, so sheet analytics and the reset
  land on the same package.
- MeasurePaywallBounds' reconcile effect is keyed on the states list,
  so prewarmed workflow steps joining after first measure reconcile too.
- Reconcile also runs when a tab switch keeps an outside-tabs selection.
- The nothing-visible test now uses the direct-constructor state, which
  has no synthesized always-visible duplicates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joshdholtz
joshdholtz marked this pull request as ready for review September 11, 2026 18:04
@joshdholtz
joshdholtz requested a review from a team as a code owner September 11, 2026 18:04
Copilot AI lite review requested due to automatic review settings September 11, 2026 18:04

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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 3 potential issues.

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 301466a. Configure here.

- Reconcile and peek now use the app-window screen condition (set by
  MeasurePaywallBounds next to the bounds) instead of deriving one from
  the paywall bounds — the two can fall in different size classes in a
  sheet or pane.
- A selection is kept while ANY on-screen copy of its uniqueId renders
  visible, so a hidden outside-tabs copy no longer masks a visible copy
  on the active tab (and vice versa).
- The sheet-dismiss peek prefers a bounds-visible outside-tabs package
  before falling back to the unchecked init-time id.
- FakePaywallState no longer synthesizes a duplicate always-visible
  component for packages the test already declares — the duplicates
  defeated visibility-rule assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joshdholtz
joshdholtz marked this pull request as draft September 15, 2026 17:53
aelaguiz pushed a commit to funcountry/purchases-android that referenced this pull request Sep 19, 2026
…t#4193)

### Checklist
- [x] If applicable, unit tests
- [x] If applicable, create follow-up issues for `purchases-ios` and
hybrids (iOS: RevenueCat/purchases-ios#7645; hybrids need no changes)

### Motivation
Let one paywall adapt its layout to the window it renders in (tablets,
foldables, multi-window) — the Android side of upcoming responsive
paywall rules in the dashboard.

### Description
- New override conditions: `WindowWidthRule`, `WindowHeightRule`,
`WindowAspectRatioRule` (`window_width_condition` etc.) — operators `>=`
`>` `<=` `<` `=`, values in dp, ratio is width / height (>1 = landscape)
- Conditions AND together (`width >= 700` + `height >= 480` = large
windows, not landscape phones)
- Evaluated against the paywall's own measured bounds (same as iOS): a
paywall in a sheet, dialog, or pane matches its own size. An unbounded
axis (fit-content sheet height) falls back to the window dimension.
Re-evaluates live on rotation/resize/multi-window
- Older SDKs decode them as `Unsupported` → default layout; unknown
window size never matches
- Snapshot coverage twice over: Compose previews (CI snapshot pipeline)
+ Paparazzi goldens — phone portrait stacked, phone landscape stacked
(height floor), tablets side by side

⚠️ Reviewer notes:
- Selection gap (a rule hiding the currently selected package leaves it
selected and purchasable) is fixed in the stacked follow-up RevenueCat#4198,
mirroring iOS's reconcile-on-resize.
- Earlier revisions evaluated the app window; after review feedback this
now measures the paywall's own bounds, matching iOS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Wide changes to override resolution and layout roots across paywall
rendering, though behavior is gated on new dashboard rules and covered
by tests; package selection still ignores window-size rules (follow-up
noted for hidden-selection edge cases).
> 
> **Overview**
> Adds **responsive paywall overrides** driven by measured paywall size
(not the full app window), aligned with iOS: width, height, and
aspect-ratio rules with `>=` / `>` / `<=` / `<` / `=` on dp values,
AND-combined within an override.
> 
> The **purchases** layer gains `ComparisonOperator` and three new
serialized condition types (`window_width_condition`, etc.), marked as
rules so bad payloads still fall back safely. **RevenueCat UI** measures
bounds via `MeasurePaywallBounds` into `paywallBoundsDp`, threads that
through `ConditionContext` into every component state, and evaluates
comparisons (including epsilon for `=`) while size is unknown → no match
until the first measure.
> 
> Standard and **workflow** paywalls wrap their scaffolds in the
measurer so headers see bounds on frame one. Previews, Paparazzi
snapshots, and broad unit tests cover deserialization, operator edge
cases, and a split-layout fixture (stacked vs side-by-side at
700×480dp).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a25cc1a. 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: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants