Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paywalls V2] Add Badge's overlay style layout #2009

Merged
merged 15 commits into from
Jan 10, 2025
Merged

Conversation

tonidero
Copy link
Contributor

@tonidero tonidero commented Dec 23, 2024

Description

This adds the layout for Badge to display correctly with the overlay layout.

This PR moves the existing Stack layout inside another composable: MainStackComponent. And at the top-level, we now decide what layout to use depending on the badge style. In this PR, we are implementing the first one: overlay.
image

state,
modifier = Modifier
.align(alignment.toAlignment())
.onGloballyPositioned {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't really like using this, since emerge/snapshot tests usually don't pick these changes correctly... Not sure if there is a better way to do this, while allowing arbitrary contents inside the badge though...

Copy link
Member

Choose a reason for hiding this comment

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

Generally influencing the layout of a composable can be done in a .layout { } modifier. That allows us to keep layout logic in the layout phase, and avoids an extra composition. Instead of .onGloballyPositioned { }, we can do something like this:

.layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        placeable.placeRelative(
            x = getOverlaidBadgeOffsetX(placeable.width, alignment),
            y = getOverlaidBadgeOffsetY(placeable.height, alignment),
        )
    }
}

with:

private fun getOverlaidBadgeOffsetX(width: Int, alignment: TwoDimensionalAlignment) =
    when (alignment) {
        TwoDimensionalAlignment.CENTER,
        TwoDimensionalAlignment.TOP,
        TwoDimensionalAlignment.BOTTOM -> 0
        TwoDimensionalAlignment.LEADING,
        TwoDimensionalAlignment.TOP_LEADING,
        TwoDimensionalAlignment.BOTTOM_LEADING -> (-(width.toFloat() / 2)).roundToInt()
        TwoDimensionalAlignment.TRAILING,
        TwoDimensionalAlignment.TOP_TRAILING,
        TwoDimensionalAlignment.BOTTOM_TRAILING -> (width.toFloat() / 2).roundToInt()
    }

private fun getOverlaidBadgeOffsetY(height: Int, alignment: TwoDimensionalAlignment) =
    when (alignment) {
        TwoDimensionalAlignment.CENTER,
        TwoDimensionalAlignment.LEADING,
        TwoDimensionalAlignment.TRAILING -> 0
        TwoDimensionalAlignment.TOP,
        TwoDimensionalAlignment.TOP_LEADING,
        TwoDimensionalAlignment.TOP_TRAILING -> (-(height.toFloat() / 2)).roundToInt()
        TwoDimensionalAlignment.BOTTOM,
        TwoDimensionalAlignment.BOTTOM_LEADING,
        TwoDimensionalAlignment.BOTTOM_TRAILING -> (height.toFloat() / 2).roundToInt()
    }

(We should then also remove the padding from the MainStackComponent.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh nice one! With this code, it can get a bit out of the parent container like:
image

But I can probably fix that, and it's much better, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it's just making getOverlaidBadgeOffsetX be 0 always :P

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did this in 3215712

Copy link
Member

Choose a reason for hiding this comment

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

Oh of course! For some reason I thought that was the requirement haha 😅

StackComponentView(
badgeStack,
state,
modifier = Modifier
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wondering if we should automatically add some start/end paddings if the "root" stack has rounded corners (as it is in the designs), or just rely on given margins for the "badge's stack". This would mean we need to add those margins from the backend, on the other hand, it's more flexible, since less logic in the SDK.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should do it on the backend, for the flexibility reason you mention + otherwise it becomes very hard to reason about SDK behavior. Also, that would be in line with our "a badge is actually a stack" approach. The backend should provide all properties to be able to render that badge stack correctly imo, just as it does for normal stacks.

Copy link

codecov bot commented Dec 23, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.89%. Comparing base (02e87d9) to head (1129ef4).
Report is 9 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2009   +/-   ##
=======================================
  Coverage   81.88%   81.89%           
=======================================
  Files         260      261    +1     
  Lines        8514     8524   +10     
  Branches     1226     1227    +1     
=======================================
+ Hits         6972     6981    +9     
  Misses       1043     1043           
- Partials      499      500    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

# Conflicts:
#	ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentView.kt
#	ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt
#	ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StackComponentStyle.kt
#	ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt
#	ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/button/ButtonComponentViewTests.kt
# Conflicts:
#	ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/stack/StackComponentView.kt
@JayShortway
Copy link
Member

@tonidero I fixed conflicts by merging instead of rebasing and force pushing, so you can squash, revert or do whatever you want with my commits. 😄

Copy link

emerge-tools bot commented Dec 30, 2024

📸 Snapshot Test

6 added, 137 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
TestPurchasesUIAndroidCompatibility
com.revenuecat.testpurchasesuiandroidcompatibility
6 0 0 0 137 5 ⏳ Needs approval

🛸 Powered by Emerge Tools

Base automatically changed from add-badge-component to main January 8, 2025 15:19
@@ -108,6 +108,9 @@ internal class StackComponentState(
@get:JvmSynthetic
val shadow by derivedStateOf { presentedPartial?.partial?.shadow ?: style.shadow }

@get:JvmSynthetic
val badge by derivedStateOf { style.badge }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I went a lot of back and forth on this... I think it's fine even if this doesn't use the presentedPartial? The overrides on the inner stack would already be applied in the style itself I believe. The only thing that wouldn't be overriden is the style and alignment, but not sure if it's worth the refactor that would be required to support that... Lmk if I misunderstood something though!

Copy link
Member

Choose a reason for hiding this comment

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

I think we should support overrides for the style and alignment 🤔 I can imagine someone wanting to change the badge style on a larger screen, for instance.

Maybe we can separate out the style, alignment and content like so. Maybe that's easier?

@get:JvmSynthetic
val badgeAlignment by derivedStateOf { presentedPartial?.partial?.badge?.alignment ?: style.badge?.alignment }
    
@get:JvmSynthetic
val badgeStyle by derivedStateOf { presentedPartial?.partial?.badge?.style ?: style.badge?.style }

@get:JvmSynthetic
val badgeContent = style.badge?.stackStyle

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up changing it to override the style and alignment in a single field by creating a new instance of the style if needed: 6d0d6ee. This makes it simpler since we only have a single nullable field instead of 3. Lmk what you think!

Copy link
Member

Choose a reason for hiding this comment

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

I ... think this is perfect? Nice one!

@tonidero tonidero marked this pull request as ready for review January 8, 2025 18:10
@tonidero tonidero requested review from JayShortway and a team January 8, 2025 18:11
Copy link
Member

@JayShortway JayShortway left a comment

Choose a reason for hiding this comment

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

I like it a lot! Just the 2 comments on the overrides and the layout modifier.

@@ -108,6 +108,9 @@ internal class StackComponentState(
@get:JvmSynthetic
val shadow by derivedStateOf { presentedPartial?.partial?.shadow ?: style.shadow }

@get:JvmSynthetic
val badge by derivedStateOf { style.badge }
Copy link
Member

Choose a reason for hiding this comment

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

I think we should support overrides for the style and alignment 🤔 I can imagine someone wanting to change the badge style on a larger screen, for instance.

Maybe we can separate out the style, alignment and content like so. Maybe that's easier?

@get:JvmSynthetic
val badgeAlignment by derivedStateOf { presentedPartial?.partial?.badge?.alignment ?: style.badge?.alignment }
    
@get:JvmSynthetic
val badgeStyle by derivedStateOf { presentedPartial?.partial?.badge?.style ?: style.badge?.style }

@get:JvmSynthetic
val badgeContent = style.badge?.stackStyle

state,
modifier = Modifier
.align(alignment.toAlignment())
.onGloballyPositioned {
Copy link
Member

Choose a reason for hiding this comment

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

Generally influencing the layout of a composable can be done in a .layout { } modifier. That allows us to keep layout logic in the layout phase, and avoids an extra composition. Instead of .onGloballyPositioned { }, we can do something like this:

.layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        placeable.placeRelative(
            x = getOverlaidBadgeOffsetX(placeable.width, alignment),
            y = getOverlaidBadgeOffsetY(placeable.height, alignment),
        )
    }
}

with:

private fun getOverlaidBadgeOffsetX(width: Int, alignment: TwoDimensionalAlignment) =
    when (alignment) {
        TwoDimensionalAlignment.CENTER,
        TwoDimensionalAlignment.TOP,
        TwoDimensionalAlignment.BOTTOM -> 0
        TwoDimensionalAlignment.LEADING,
        TwoDimensionalAlignment.TOP_LEADING,
        TwoDimensionalAlignment.BOTTOM_LEADING -> (-(width.toFloat() / 2)).roundToInt()
        TwoDimensionalAlignment.TRAILING,
        TwoDimensionalAlignment.TOP_TRAILING,
        TwoDimensionalAlignment.BOTTOM_TRAILING -> (width.toFloat() / 2).roundToInt()
    }

private fun getOverlaidBadgeOffsetY(height: Int, alignment: TwoDimensionalAlignment) =
    when (alignment) {
        TwoDimensionalAlignment.CENTER,
        TwoDimensionalAlignment.LEADING,
        TwoDimensionalAlignment.TRAILING -> 0
        TwoDimensionalAlignment.TOP,
        TwoDimensionalAlignment.TOP_LEADING,
        TwoDimensionalAlignment.TOP_TRAILING -> (-(height.toFloat() / 2)).roundToInt()
        TwoDimensionalAlignment.BOTTOM,
        TwoDimensionalAlignment.BOTTOM_LEADING,
        TwoDimensionalAlignment.BOTTOM_TRAILING -> (height.toFloat() / 2).roundToInt()
    }

(We should then also remove the padding from the MainStackComponent.)

Comment on lines 160 to 163
StackComponentView(
badgeStack,
state,
clickHandler,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can create a Badge composable here that takes a TwoDimensionalAlignment and a Badge.Style, and is implemented as a StackComponentView with the right (layout) modifier? That might simplify this file a bit.

Alternatively, as a first step we could create an OverlaidBadge composable that does the same thing, but only implements the Overlay style, and just takes a TwoDimensionalAlignment.

Just a suggestion. Let me know what you think!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 1129ef4

StackComponentView(
badgeStack,
state,
modifier = Modifier
Copy link
Member

Choose a reason for hiding this comment

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

I think we should do it on the backend, for the flexibility reason you mention + otherwise it becomes very hard to reason about SDK behavior. Also, that would be in line with our "a badge is actually a stack" approach. The backend should provide all properties to be able to render that badge stack correctly imo, just as it does for normal stacks.

Copy link
Member

@JayShortway JayShortway left a comment

Choose a reason for hiding this comment

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

Looks amazing! And thanks for making the changes!

Comment on lines +140 to +143
Box(modifier = modifier) {
MainStackComponent(stackState, state, clickHandler, selected = selected)
OverlaidBadge(badgeStack, state, alignment, selected = selected)
}
Copy link
Member

Choose a reason for hiding this comment

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

Beautiful!

state,
modifier = Modifier
.align(alignment.toAlignment())
.onGloballyPositioned {
Copy link
Member

Choose a reason for hiding this comment

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

Oh of course! For some reason I thought that was the requirement haha 😅

@@ -108,6 +108,9 @@ internal class StackComponentState(
@get:JvmSynthetic
val shadow by derivedStateOf { presentedPartial?.partial?.shadow ?: style.shadow }

@get:JvmSynthetic
val badge by derivedStateOf { style.badge }
Copy link
Member

Choose a reason for hiding this comment

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

I ... think this is perfect? Nice one!

@tonidero tonidero merged commit 3b60d92 into main Jan 10, 2025
12 checks passed
@tonidero tonidero deleted the add-badge-layout branch January 10, 2025 10:10
tonidero added a commit that referenced this pull request Jan 14, 2025
…le layout (#2039)

### Description
Follow-up to #2009 
This PR adds the `edgeToEdge` top/bottom style for badges.
<img width="911" alt="image"
src="https://github.com/user-attachments/assets/a2722e94-8bd1-4867-a8a6-0edfceac2ed6"
/>

---------

Co-authored-by: JayShortway <29483617+JayShortway@users.noreply.github.com>
tonidero added a commit that referenced this pull request Jan 15, 2025
### Description
Followup to #2009 and #2039.

This adds support for `nested` style badges.

As discussed over Slack, the nested badges won't move the content of the
stack, and just be placed on top.
<img width="906" alt="image"
src="https://github.com/user-attachments/assets/4cc2e2d9-a336-4890-b40a-a065b247a1b7"
/>
This was referenced Jan 15, 2025
tonidero pushed a commit that referenced this pull request Jan 17, 2025
**This is an automatic release.**

## RevenueCat SDK
### ✨ New Features
* Add `subscriptionsByProductIdentifier` to `CustomerInfo` (#2052) via
Cesar de la Vega (@vegaro)
### 🐞 Bugfixes
* Fix `OwnershipType` enum serialization (#2061) via Cesar de la Vega
(@vegaro)

## RevenueCatUI SDK
### 🐞 Bugfixes
* Allow repurchasing custom packages (#2044) via Toni Rico (@tonidero)

### 🔄 Other Changes
* [Paywalls V2] Do not attempt to purchase if currently subscribed
(#2062) via JayShortway (@JayShortway)
* [Trusted Entitlements] Enable `Trusted Entitlements` by default
(#2050) via Toni Rico (@tonidero)
* [Trusted Entitlements] Do not clear CustomerInfo upon enabling Trusted
Entitlements (#2049) via Toni Rico (@tonidero)
* [Paywalls V2] Removes `MaskShape.Pill` in favor of `MaskShape.Circle`.
(#2063) via JayShortway (@JayShortway)
* [Paywalls V2] Font sizes are integers now. (#2059) via JayShortway
(@JayShortway)
* [Paywalls V2] Handles intro offer eligibility overrides (#2058) via
JayShortway (@JayShortway)
* [Paywalls V2] Implements `Convex` and `Concave` image masks (#2055)
via JayShortway (@JayShortway)
* [Paywalls V2] Add new `ImageComponent` properties (#2056) via Toni
Rico (@tonidero)
* [Paywalls V2] Add `Badge`'s `nested` style layout (#2041) via Toni
Rico (@tonidero)
* [Paywalls V2] Add `Badge`'s `edgeToEdge` `Top`/`Bottom` alignment
style layout (#2039) via Toni Rico (@tonidero)
* [Paywalls V2] Various `PaywallViewModel` fixes and tests (#2051) via
JayShortway (@JayShortway)
* [Paywalls V2] Fixes minimum spacing when distribution is
`SPACE_BETWEEN`, `SPACE_AROUND` or `SPACE_EVENLY` (#2053) via
JayShortway (@JayShortway)
* [Paywalls V2] Correctly determines when to show or hide decimals for
prices (#2048) via JayShortway (@JayShortway)
* [Paywalls V2] `TextComponentView` uses the correct `Package` for
variable values (#2042) via JayShortway (@JayShortway)
* [Paywalls V2] Adds Custom Tabs to support in-app browser URL
destinations (#2035) via JayShortway (@JayShortway)
* Update `agp` to 8.8.0 (#2045) via Toni Rico (@tonidero)
* [Paywalls V2] Add `Badge`'s `overlay` style layout (#2009) via Toni
Rico (@tonidero)
* [Paywalls V2] Implements all button actions (#2034) via JayShortway
(@JayShortway)
* Convert error message property into computed property (#2038) via Toni
Rico (@tonidero)

Co-authored-by: revenuecat-ops <ops@revenuecat.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