Skip to content

Feature: Update default paywall#3133

Merged
JZDesign merged 46 commits into
mainfrom
jzdesign/fallback-paywall-3-default-paywall-ui
Mar 13, 2026
Merged

Feature: Update default paywall#3133
JZDesign merged 46 commits into
mainfrom
jzdesign/fallback-paywall-3-default-paywall-ui

Conversation

@JZDesign

@JZDesign JZDesign commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds DefaultPaywallView composable: the main fallback paywall with dynamic app icon theming, package list, purchase and restore buttons
  • Adds DefaultPaywallWarning: debug-only warning panel showing what went wrong, with RevenueCat branding and dashboard links
  • Adds DefaultProductCell: selectable product row with animated color transitions
  • Adds RevenueCat branding image and string resources for the default paywall

Part 3 of the fallback paywall feature breaking up #2945. Stacked on #3132.


Note

Medium Risk
Adds a new fallback paywall UI path for legacy paywalls and threads a new validationWarning through state mapping, which can change what users see when paywall validation fails (though warning UI is debug-only). Main risk is UI/behavior regressions around package selection, purchase/restore entrypoints, and full-screen vs sheet background handling.

Overview
Adds a new default/fallback paywall Compose UI (DefaultPaywallView) that themes itself from the app icon, shows a selectable package list, and provides localized Purchase/Restore actions, plus a debug-only DefaultPaywallWarning panel (with a Dashboard link) and preview/test utilities.

Wires a new validationWarning: PaywallWarning? through PaywallValidationResult mapping into PaywallState.Loaded.Legacy, and updates InternalPaywall to render the new default paywall when the warning is present (including refactoring background handling via screenModeBackground).

Updates test/mocks to support the new legacy fallback flow (selection mutates state; purchase captures selected package id; restore uses restorePurchases() entrypoint) and adds string resources for the default paywall across locales; also ignores .cursor/ in .gitignore.

Written by Cursor Bugbot for commit 6ca1786. This will update automatically on new commits. Configure here.

JZDesign and others added 3 commits February 23, 2026 14:50
…lt.Legacy

Introduces PaywallWarning sealed class that maps PaywallValidationErrors
to user-facing warning messages with titles, body text, and help URLs.
Adds a warning computed property on PaywallValidationResult.Legacy to
surface the first error as a displayable warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds utilities for the fallback paywall dynamic theming:
- ColorComputationHelpers: WCAG 2.0 contrast ratio and luminance calculations
- AppStyleExtractor: extracts app name, icon bitmap, and prominent colors
- CircleOutlined: outlined circle icon for unselected product cells
- Tests for color extraction and contrast computation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the composable UI for the fallback paywall:
- DefaultPaywallView: main fallback paywall with app icon, color extraction,
  package list, purchase and restore buttons
- DefaultPaywallWarning: debug-only warning panel with RC branding and help links
- DefaultProductCell: selectable product row with animated color transitions
- RevenueCat branding image and string resources

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@emerge-tools

emerge-tools Bot commented Feb 23, 2026

Copy link
Copy Markdown

2 builds increased size

Name Version Download Change Install Change Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
1.0 (1) 70.8 MB ⬆️ 66.1 kB (0.09%) 113.9 MB ⬆️ 129.6 kB (0.11%) N/A
SDKSizeTesting
com.revenuecat.testapps.sdksizetesting
1.0 (1) 11.9 MB ⬆️ 22.9 kB (0.19%) 34.5 MB ⬆️ 66.1 kB (0.19%) N/A

TestPurchasesUIAndroidCompatibility 1.0 (1)
com.revenuecat.testpurchasesuiandroidcompatibility

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 129.6 kB (0.11%)
Total download size change: ⬆️ 66.1 kB (0.09%)

Largest size changes

Item Install Size Change Download Size Change
📝 visual_ob_create_paywall.webp ⬆️ 23.8 kB ⬆️ 23.9 kB
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 12.0 kB ⬆️ 4.1 kB
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 5.0 kB ⬆️ 1.7 kB
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 3.7 kB ⬆️ 1.3 kB
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPr... ⬆️ 2.9 kB ⬆️ 996 B
View Treemap

Image of diff

SDKSizeTesting 1.0 (1)
com.revenuecat.testapps.sdksizetesting

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 66.1 kB (0.19%)
Total download size change: ⬆️ 22.9 kB (0.19%)

Largest size changes

Item Install Size Change Download Size Change
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 7.3 kB ⬆️ 2.4 kB
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 3.0 kB ⬆️ 957 B
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPa... ⬆️ 2.2 kB ⬆️ 708 B
📝 com.revenuecat.purchases.ui.revenuecatui.defaultpaywall.DefaultPr... ⬆️ 2.1 kB ⬆️ 671 B
resources.arsc ⬆️ 2.4 kB ⬆️ 520 B
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Extract helper functions and centralize color constants to simplify color extraction logic and WCAG computations. AppStyleExtractor: move pixel quantization, alpha/brightness checks and key packing into quantizedColorKeyOrNull; add colorKeyToTuple, normalizedColorComponent and Triple.toColor helpers; simplify main sampling loop and sort/filter colors inline. ColorComputationHelpers: introduce channel masks/shifts, normalization divisor and WCAG-related constants; update relativeLuminance and contrastRatio to use these constants. These changes improve readability, reduce duplicated bit-manipulation, and make WCAG formulas configurable and consistent.
github-merge-queue Bot pushed a commit that referenced this pull request Mar 2, 2026
## Summary
- Adds PaywallWarning sealed class that maps PaywallValidationError to
user-facing warning messages with titles, body text, and help URLs
- Adds a warning computed property on PaywallValidationResult.Legacy to
surface the first validation error as a displayable warning

(stacked PRs) breaking up #2945.

- Add PaywallWarning type for fallback paywall #3131 (This PR)
- Add color helpers, AppStyleExtractor, and CircleOutlined icon #3132
- Add default paywall UI components #3133
- Wire validation warning through paywall state pipeline #3134
- Plug in Paywall
- Remove Big background image and clean up CI



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds an internal warning type and a derived property without
changing validation or paywall rendering logic. Main risk is minor
mismatches in error-to-message mapping affecting debug-only messaging.
> 
> **Overview**
> Adds an internal sealed `PaywallWarning` that maps
`PaywallValidationError` to user-facing `title`, `bodyText`, and
optional `helpUrl` for displaying debug warnings on the default/fallback
paywall.
> 
> Extends `PaywallValidationResult.Legacy` with a `warning` computed
property that converts the *first* validation error into a
`PaywallWarning` for easy consumption by the default paywall UI.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
800e7fe. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
JZDesign added 4 commits March 2, 2026 13:25
Calculate sampleStep using safe ceiling division with Longs to avoid overflow and large allocations; sample pixels by computing (x,y) and calling bitmap.getPixel instead of pulling the entire pixel array. Remove two unused color constant definitions. Add a test for extracting colors from a large (400x400) bitmap and make createTestBitmap accept configurable width/height.

@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 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Weight modifiers ignored inside vertically scrollable Column
    • Replaced weighted spacers with a BoxWithConstraints + heightIn(min = maxHeight) and Arrangement.SpaceBetween layout so vertical centering works while preserving scrolling behavior.

Create PR

Or push these changes by commenting:

@cursor push 8f57469255
Preview (8f57469255)
diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/defaultpaywall/DefaultPaywallView.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/defaultpaywall/DefaultPaywallView.kt
--- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/defaultpaywall/DefaultPaywallView.kt
+++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/defaultpaywall/DefaultPaywallView.kt
@@ -7,11 +7,13 @@
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.widthIn
@@ -119,58 +121,61 @@
                 ),
             ),
     ) {
-        Column(
+        BoxWithConstraints(
             modifier = Modifier
                 .fillMaxSize()
                 .widthIn(max = 630.dp)
                 .align(Alignment.TopCenter)
-                .verticalScroll(rememberScrollState())
                 .padding(16.dp),
-            horizontalAlignment = Alignment.CenterHorizontally,
         ) {
-            // Title (only when showing warning)
-            if (shouldShowWarning) {
-                Text(
-                    text = stringResource(R.string.revenuecatui_paywalls_title),
-                    style = MaterialTheme.typography.titleMedium,
-                    fontWeight = FontWeight.Bold,
-                    textAlign = TextAlign.Center,
-                    modifier = Modifier.fillMaxWidth(),
-                )
-            }
-
-            Spacer(modifier = Modifier.weight(1f))
-
-            // Content area - either warning or app icon
-            if (shouldShowWarning && warning != null) {
-                DefaultPaywallWarning(warning = warning, warningColor = RevenueCatBrandRed)
-            } else {
-                AppIconSection(
-                    bitmap = appIconBitmap,
-                    appName = appName,
-                    shadowColor = mainColor,
-                )
-            }
-
-            Spacer(modifier = Modifier.weight(1f))
-
-            // Product list
             Column(
-                verticalArrangement = Arrangement.spacedBy(8.dp),
+                modifier = Modifier
+                    .fillMaxSize()
+                    .verticalScroll(rememberScrollState())
+                    .heightIn(min = maxHeight),
                 horizontalAlignment = Alignment.CenterHorizontally,
+                verticalArrangement = Arrangement.SpaceBetween,
             ) {
-                packages.forEach { pkg ->
-                    DefaultProductCell(
-                        pkg = pkg,
-                        accentColor = mainColor,
-                        selectedFontColor = foregroundOnAccentColor,
-                        isSelected = selectedPackage == pkg,
-                        onSelect = { selectedPackage = pkg },
+                // Title (only when showing warning)
+                if (shouldShowWarning) {
+                    Text(
+                        text = stringResource(R.string.revenuecatui_paywalls_title),
+                        style = MaterialTheme.typography.titleMedium,
+                        fontWeight = FontWeight.Bold,
+                        textAlign = TextAlign.Center,
+                        modifier = Modifier.fillMaxWidth(),
                     )
+                } else {
+                    Spacer(modifier = Modifier)
                 }
+
+                // Content area - either warning or app icon
+                if (shouldShowWarning && warning != null) {
+                    DefaultPaywallWarning(warning = warning, warningColor = RevenueCatBrandRed)
+                } else {
+                    AppIconSection(
+                        bitmap = appIconBitmap,
+                        appName = appName,
+                        shadowColor = mainColor,
+                    )
+                }
+
+                Column(
+                    verticalArrangement = Arrangement.spacedBy(8.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally,
+                ) {
+                    packages.forEach { pkg ->
+                        DefaultProductCell(
+                            pkg = pkg,
+                            accentColor = mainColor,
+                            selectedFontColor = foregroundOnAccentColor,
+                            isSelected = selectedPackage == pkg,
+                            onSelect = { selectedPackage = pkg },
+                        )
+                    }
+                    Spacer(modifier = Modifier.height(16.dp))
+                }
             }
-
-            Spacer(modifier = Modifier.height(16.dp))
         }
 
         // Footer buttons
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@JZDesign JZDesign requested a review from a team March 6, 2026 20:40
Comment thread ui/revenuecatui/src/main/res/values/strings.xml Outdated
Comment thread ui/revenuecatui/src/main/res/values/strings.xml Outdated
Comment thread ui/revenuecatui/src/main/res/drawable/visual_ob_create_paywall.png Outdated
JZDesign added 4 commits March 9, 2026 21:19
Change selectColorWithBestContrast to return a nullable Color instead of falling back to the background color, so callers can handle empty inputs explicitly. Update tests to assert null for an empty list. In AppStyleExtractor, add a CoroutineDispatcher parameter (defaulting to Dispatchers.Default) to getProminentColorsFromBitmap to allow injection/testing, add logging on bitmap-load failures, and adjust exception suppression. These changes improve testability and error visibility.
Add a caller-controlled Dispatcher parameter to getProminentColorsFromBitmap so extraction can run on a provided dispatcher (useful for previews and tests). Remove the synchronous extractProminentColorsSync helper and update unit tests to run under runTest with StandardTestDispatcher(testScheduler), passing the dispatcher into the extraction calls. Adjusted imports accordingly.
Ensure UI always has a usable accent and foreground color when color extraction fails. Use MaterialTheme.colorScheme.primary as a fallback for icon/accent color and MaterialTheme.colorScheme.onPrimary for foreground-on-accent, guarding against selectColorWithBestContrast returning null. Also added brief comments explaining the fallbacks.
@JZDesign JZDesign changed the title Update default paywall Feature: Update default paywall Mar 12, 2026
@JZDesign JZDesign added the pr:feat A new feature label Mar 12, 2026

@vegaro vegaro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great job! Maybe worth it to look into #3133 (comment) to make sure it's not broken 🤔

@vegaro

vegaro commented Mar 12, 2026

Copy link
Copy Markdown
Member

Oh interesting @JZDesign after thinking a bit more about #3133 (comment) I just tested opening an offering without a paywall and it looks like it might be broken. It doesn't matter what's selected as package that it always chooses the default. Try with cesar_tests_2 in the offerings

Screen_recording_20260312_204044.mp4

Copy link
Copy Markdown
Contributor Author

👀

Refactor paywall UI to centralize package selection and purchase handling in the ViewModel.

- InternalPaywall: pass TemplateConfiguration packages, selectedPackage and wire onSelectPackage to viewModel::selectPackage; initiate purchase via viewModel.purchaseSelectedPackage(activity).
- DefaultPaywallView: switch to List<TemplateConfiguration.PackageInfo>, accept selectedPackage and onSelectPackage, remove internal selection state, use rcPackage.identifier as list key, and adjust footer/onPurchase signature.
- DefaultProductCell: add testTag to the package select button for testing.
- DefaultPaywallPreviews: build preview PackageInfo objects (with ProcessedLocalizedConfiguration) and provide selectedPackage and onSelectPackage to the previewed view.
- TestData.MockViewModel: add validationWarning param, propagate it to loaded state, make selectPackage resilient (fallback to legacy state), and track purchaseSelectedPackageIdentifiers when purchaseSelectedPackage is invoked.

These changes centralize state management, align the UI with TemplateConfiguration.PackageInfo, and improve testability via tags and additional test hooks.
Remove rememberCoroutineScope and the coroutine launch used to call the restore action from the UI, and call viewModel.restorePurchases() directly in the legacy paywall. Update tests to assert the new entrypoint: add a test that verifies restore increments restorePurchasesCallCount and does not call handleRestorePurchases, plus required imports and test helpers.
… into jzdesign/fallback-paywall-3-default-paywall-ui

@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 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread .gitignore
Replace strict Color equality checks with approximate comparisons in AppStyleExtractorTests.kt to make tests less brittle. Updated assertions to use colors.any { it.isCloseTo(...) } and added a private Color.isCloseTo extension that compares RGB values via colorDistance with a default tolerance of 0.1.
@codecov

codecov Bot commented Mar 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.41%. Comparing base (054abeb) to head (6ca1786).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3133   +/-   ##
=======================================
  Coverage   79.41%   79.41%           
=======================================
  Files         356      356           
  Lines       14342    14342           
  Branches     1958     1958           
=======================================
  Hits        11389    11389           
  Misses       2149     2149           
  Partials      804      804           

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

@JZDesign JZDesign added this pull request to the merge queue Mar 13, 2026
Merged via the queue into main with commit 62c780a Mar 13, 2026
29 checks passed
@JZDesign JZDesign deleted the jzdesign/fallback-paywall-3-default-paywall-ui branch March 13, 2026 19:21
github-merge-queue Bot pushed a commit that referenced this pull request Mar 16, 2026
**This is an automatic release.**

## RevenueCat SDK
### 🐞 Bugfixes
* Fix & Standardize Galaxy Date Parsing Edge Cases (#3216) via Will
Taylor (@fire-at-will)
* Fix addSuccessfullyPostedToken for new purchases in
PostPendingTransactionsHelper (#3239) via Facundo Menzella
(@facumenzella)
* [Galaxy]: Fix race condition when fetching Galaxy products (#3213) via
Will Taylor (@fire-at-will)
* Fixes double padding in PaywallActivity on Android 15+ when
`edgeToEdge` parameter is false (#3227) via Cesar de la Vega (@vegaro)

## RevenueCatUI SDK
### 🐞 Bugfixes
* Fix bold text not rendering in Markdown lists (#3228) via Cesar de la
Vega (@vegaro)
* Fix: Clear in-memory offerings cache on locale override to prevent
stale paywall data (#3225) via Antonio Pallares (@ajpallares)
### Paywallv2
#### ✨ New Features
* Feature: Update default paywall (#3133) via Jacob Rakidzich
(@JZDesign)
#### 🐞 Bugfixes
* Fix V2 paywall safe area in landscape mode (#3221) via Cesar de la
Vega (@vegaro)

### 🔄 Other Changes
* Run integration tests on all branches (#3242) via Toni Rico
(@tonidero)
* Migrate Firebase Test Lab jobs to CircleCI emulators (#3238) via Toni
Rico (@tonidero)
* Run metalava on galaxy module in test-galaxy job (#3235) via Will
Taylor (@fire-at-will)
* Add offering_id to custom paywall impression event (#3230) via Rick
(@rickvdl)
* Cache isAutoRenewing to detect subscription changes without
syncPurchases (#3198) via Facundo Menzella (@facumenzella)
* Bump fastlane-plugin-revenuecat_internal from `e146447` to `3e8c384`
(#3233) via dependabot[bot] (@dependabot[bot])

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk release housekeeping: version string bumps and
documentation/deployment path updates with no functional runtime logic
changes beyond the exposed version constant.
> 
> **Overview**
> Publishes the `9.26.0` release by removing `-SNAPSHOT` across
build/version metadata (root `VERSION_NAME`, `.version`,
`Config.frameworkVersion`, and sample/test app dependency pins).
> 
> Updates release documentation artifacts by adding the `9.26.0` notes
to `CHANGELOG.md`/`CHANGELOG.latest.md`, switching docs deployment in
CircleCI to sync `docs/9.26.0` to S3, and updating `docs/index.html` to
redirect to the new version.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0a30a45. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
github-merge-queue Bot pushed a commit that referenced this pull request Mar 26, 2026
…ui (#3282)

## Summary
- Adds a Danger rule that warns when a PR introduces a direct
`material-icons` import in `:ui:revenuecatui` (anything from
`androidx.compose.material.icons.*` except
`materialIcon`/`materialPath`)
- The module avoids the `material-icons` runtime dependency by defining
custom inline icon vectors — using `Icons.Filled.*` etc. causes
`NoClassDefFoundError` at runtime in apps that don't bundle the artifact
(see `DefaultProductCell.kt` introduced in #3133)

## Test plan
- [ ] Verify Danger fires a warning on a PR that adds `import
androidx.compose.material.icons.Icons` to a file under
`ui/revenuecatui/`
- [ ] Verify Danger does not warn on `import
androidx.compose.material.icons.materialIcon` / `materialPath`

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk CI-only change that adds a warning rule; it doesn’t affect
runtime behavior, but could introduce noisy/false-positive PR warnings
if the regex misses edge cases.
> 
> **Overview**
> Adds a new Danger rule that scans added/modified
`ui/revenuecatui/*.kt` diffs and emits a warning when a PR introduces
direct `androidx.compose.material.icons.*` imports (except
`materialIcon`/`materialPath`).
> 
> This enforces the module’s intent to avoid a `material-icons` runtime
dependency by steering contributors toward custom inline icons under
`icons/`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e819ec9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <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.

2 participants