Feature: Update default paywall#3133
Conversation
…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>
2 builds increased size
TestPurchasesUIAndroidCompatibility 1.0 (1)
|
| 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 |
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 |
🛸 Powered by Emerge Tools
…sign/fallback-paywall-2-color-helpers
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.
## 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>
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.
…ithub.com//RevenueCat/purchases-android into jzdesign/fallback-paywall-2-color-helpers
…n/fallback-paywall-3-default-paywall-ui
There was a problem hiding this comment.
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)andArrangement.SpaceBetweenlayout so vertical centering works while preserving scrolling behavior.
- Replaced weighted spacers with a
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 buttonsChange 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.
…n/fallback-paywall-3-default-paywall-ui
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.
There was a problem hiding this comment.
Great job! Maybe worth it to look into #3133 (comment) to make sure it's not broken 🤔
|
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 Screen_recording_20260312_204044.mp4 |
|
👀 |
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.
…s://github.com//RevenueCat/purchases-android into jzdesign/fallback-paywall-3-default-paywall-ui
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
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, enable autofix in the Cursor dashboard.
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
**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 -->
…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>




Summary
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
validationWarningthrough 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-onlyDefaultPaywallWarningpanel (with a Dashboard link) and preview/test utilities.Wires a new
validationWarning: PaywallWarning?throughPaywallValidationResultmapping intoPaywallState.Loaded.Legacy, and updatesInternalPaywallto render the new default paywall when the warning is present (including refactoring background handling viascreenModeBackground).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.