Skip to content

Add color helpers, AppStyleExtractor, and CircleOutlined icon#3132

Merged
JZDesign merged 14 commits into
mainfrom
jzdesign/fallback-paywall-2-color-helpers
Mar 10, 2026
Merged

Add color helpers, AppStyleExtractor, and CircleOutlined icon#3132
JZDesign merged 14 commits into
mainfrom
jzdesign/fallback-paywall-2-color-helpers

Conversation

@JZDesign

@JZDesign JZDesign commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Important

This PR is all dark code, nothing is used yet. PR 5 will wire it all together

This PR is prepping for color extraction and paywall styling. Unused code.

Summary

  • Adds ColorComputationHelpers with WCAG 2.0 contrast ratio and luminance calculations
  • Adds AppStyleExtractor to extract app name, icon bitmap, and prominent colors from the app icon at runtime
  • Adds CircleOutlined vector icon for unselected product cells
  • Includes tests for color extraction, contrast computation, and luminance

Part 2 of the fallback paywall feature breaking up #2945. Stacked on #3131.


Note

Low Risk
Adds new, currently-unused helper utilities and a vector icon with unit tests; no changes to runtime paywall flow or persisted data.

Overview
Prepares groundwork for styling by adding AppStyleExtractor to fetch app name/icon and to asynchronously extract a small set of prominent colors from a bitmap via sampling, quantization, and similarity/brightness/alpha filtering.

Introduces ColorComputationHelpers with WCAG-based luminance/contrast utilities (plus best-contrast selection) and adds a new CircleOutlined Compose ImageVector.

Adds Robolectric tests covering prominent-color extraction edge cases and the new color math helpers (distance, luminance, contrast, and selection).

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

JZDesign and others added 2 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>
@JZDesign JZDesign requested a review from a team as a code owner February 23, 2026 20:56
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>
Base automatically changed from jzdesign/fallback-paywall-1-foundation-types to main March 2, 2026 15:38
@codecov

codecov Bot commented Mar 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.32%. Comparing base (5f639a1) to head (3619218).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3132   +/-   ##
=======================================
  Coverage   79.32%   79.32%           
=======================================
  Files         351      351           
  Lines       14164    14164           
  Branches     1933     1933           
=======================================
  Hits        11235    11235           
  Misses       2126     2126           
  Partials      803      803           

☔ 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 added 2 commits March 3, 2026 09:42
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.
@JZDesign JZDesign requested a review from vegaro March 3, 2026 17:17
@JZDesign JZDesign requested a review from a team March 6, 2026 20:40
JZDesign added 2 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.
@JZDesign JZDesign requested a review from vegaro March 10, 2026 02:27

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

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

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

Some suggestions for improved performance

Comment on lines +31 to +32
val drawable = context.applicationInfo.loadIcon(packageManager)
drawable.toBitmap(config = Bitmap.Config.ARGB_8888)

@vegaro vegaro Mar 10, 2026

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.

Apparently in API 26+loadIcon returns AdaptiveIconDrawable (which have a background and a foreground), so it's probably a good idea to only grab the foreground in those cases:

Suggested change
val drawable = context.applicationInfo.loadIcon(packageManager)
drawable.toBitmap(config = Bitmap.Config.ARGB_8888)
val drawable = context.applicationInfo.loadIcon(packageManager)
val foreground = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
drawable.foreground
} else {
drawable
}
foreground.toBitmap(config = Bitmap.Config.ARGB_8888)

Also there's no width/height specified here in the toBitmap call, and the drawable's intrinsic size varies by device density.

That means that on an xxxhdpi device, this could allocate a 432×432 bitmap for something you're about to downsample to 10,000 pixels anyway. You can pass explicit dimensions here like 64×64.

EDIT: Actually, feel free to ignore this suggestion around sizes, since in the second PR I noticed you're actually displaying the icon you load here, so better to just load full size

Suggested change
val drawable = context.applicationInfo.loadIcon(packageManager)
drawable.toBitmap(config = Bitmap.Config.ARGB_8888)
val drawable = context.applicationInfo.loadIcon(packageManager)
val foreground = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
drawable.foreground
} else {
drawable
}
foreground.toBitmap(
width = ColorExtractionConstants.ICON_SAMPLE_SIZE,
height = ColorExtractionConstants.ICON_SAMPLE_SIZE,
config = Bitmap.Config.ARGB_8888,
)

@JZDesign JZDesign Mar 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The more I think on this the less I'm inclined to make the change. My reasoning is that we cannot guarantee that the foreground will be the segment of the app icon that contains the brand colors. This could be a black logo, and all the color is in the background, right?

So, to get the best chances of scraping the brand colors, we should use the whole drawable

Add a guard in AppStyleExtractor.extractProminentColors to return an empty list when count <= 0 to avoid unnecessary processing for invalid input. Also add a unit test verifying getProminentColorsFromBitmap returns an empty list when count is zero (uses a test bitmap and StandardTestDispatcher).
@JZDesign JZDesign enabled auto-merge March 10, 2026 14:30
@JZDesign JZDesign added this pull request to the merge queue Mar 10, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 10, 2026
@JZDesign JZDesign added this pull request to the merge queue Mar 10, 2026
Merged via the queue into main with commit 144849b Mar 10, 2026
29 checks passed
@JZDesign JZDesign deleted the jzdesign/fallback-paywall-2-color-helpers branch March 10, 2026 15:32
JZDesign added a commit that referenced this pull request Mar 12, 2026
<!-- Thank you for contributing to Purchases! Before pressing the
"Create Pull Request" button, please provide the following: -->


Wires in the default paywall into the sdk for use. Adds previews for
emerge snapshot testing

Part 5 of the fallback paywall feature breaking up
#2945. Stacked on
#3134

- Add PaywallWarning type for fallback paywall
#3131
- 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
#3178 (This PR)
- Remove Big background image and clean up CI

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a new paywall rendering path that can trigger
purchase/restore actions from the fallback UI, so regressions could
affect paywall display and transaction initiation when validation
warnings occur.
> 
> **Overview**
> Adds a *fallback “default paywall”* flow: `InternalPaywall` now
renders `DefaultPaywallView` whenever a legacy paywall loads with a
`validationWarning`, wiring its purchase/restore actions through the
existing `PaywallViewModel` and reusing a shared `screenModeBackground`
modifier.
> 
> Refactors `DefaultPaywallView` layout to use `Scaffold` with a fixed
footer and `LazyColumn`, adds preview-only overrides plus new Compose
previews, and introduces a deterministic `DualColorImageGenerator` used
by previews and a new `AppStyleExtractor` prominent-colors test. Also
removes developer-facing debug strings from `strings.xml` in favor of
local constants, and updates `.gitignore` to exclude `.cursor/`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
42d3af2. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
JZDesign added a commit that referenced this pull request Mar 12, 2026
## Summary
- Adds validationWarning: PaywallWarning? field to
PaywallState.Loaded.Legacy
- Threads validationWarning through
OfferingToStateMapper.toLegacyPaywallState
- Passes validationResult.warning from PaywallViewModel.calculateState
to the state mapper

When a paywall fails validation, the warning is now carried on the
loaded state, ready for the UI layer (PR 5) to check and render the
DefaultPaywallView instead of the template paywall.

Part 4 of the fallback paywall feature breaking up #2945. Stacked on
#3133.

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


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk additive change: it only carries an optional `PaywallWarning`
through state creation and does not alter validation or rendering logic
yet.
> 
> **Overview**
> **Threads paywall validation warnings through the Legacy paywall
state.** `PaywallState.Loaded.Legacy` now includes an optional
`validationWarning: PaywallWarning?`.
> 
> `PaywallViewModel.calculateState` passes `validationResult.warning`
into `Offering.toLegacyPaywallState`, and the mapper stores it on the
created legacy state so the UI can later decide to show a
fallback/default paywall when validation issues occur.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
64f72f8. 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>
github-merge-queue Bot pushed a commit that referenced this pull request Mar 13, 2026
## 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.

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



<!-- CURSOR_SUMMARY -->
---

> [!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`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6ca1786. 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>
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