-
Notifications
You must be signed in to change notification settings - Fork 50
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: Add PaywallFooter composable to present a minified paywall UI that allows for custom paywalls #1314
Changes from 1 commit
c34ca05
6240530
b2168f1
a1bb3e5
67e7910
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,24 @@ | ||
package com.revenuecat.purchases.ui.revenuecatui | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.shape.RoundedCornerShape | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.lifecycle.viewmodel.compose.viewModel | ||
import com.revenuecat.purchases.Offering | ||
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModel | ||
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelFactory | ||
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelImpl | ||
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewState | ||
import com.revenuecat.purchases.ui.revenuecatui.data.isInFullScreenMode | ||
import com.revenuecat.purchases.ui.revenuecatui.data.processed.PaywallTemplate | ||
import com.revenuecat.purchases.ui.revenuecatui.extensions.conditional | ||
import com.revenuecat.purchases.ui.revenuecatui.helpers.isInPreviewMode | ||
import com.revenuecat.purchases.ui.revenuecatui.helpers.toAndroidContext | ||
import com.revenuecat.purchases.ui.revenuecatui.templates.Template1 | ||
|
@@ -20,50 +27,63 @@ import com.revenuecat.purchases.ui.revenuecatui.templates.Template3 | |
|
||
@Composable | ||
internal fun InternalPaywallView( | ||
mode: PaywallViewMode = PaywallViewMode.default, | ||
offering: Offering? = null, | ||
listener: PaywallViewListener? = null, | ||
viewModel: PaywallViewModel = getPaywallViewModel(offering = offering, listener = listener, mode = mode), | ||
options: PaywallViewOptions, | ||
viewModel: PaywallViewModel = getPaywallViewModel(options), | ||
) { | ||
viewModel.refreshStateIfLocaleChanged() | ||
val colors = MaterialTheme.colorScheme | ||
viewModel.refreshStateIfColorsChanged(colors) | ||
|
||
when (val state = viewModel.state.collectAsState().value) { | ||
is PaywallViewState.Loading -> { | ||
LoadingPaywallView(mode = mode) | ||
LoadingPaywallView(mode = options.mode) | ||
} | ||
is PaywallViewState.Error -> { | ||
Text(text = "Error: ${state.errorMessage}") | ||
} | ||
is PaywallViewState.Loaded -> { | ||
when (state.templateConfiguration.template) { | ||
PaywallTemplate.TEMPLATE_1 -> Template1(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_2 -> Template2(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_3 -> Template3(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_4 -> Text(text = "Error: Template 4 not supported") | ||
PaywallTemplate.TEMPLATE_5 -> Text(text = "Error: Template 5 not supported") | ||
val backgroundColor = state.templateConfiguration.getCurrentColors().background | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wasn't currently used so I added it here to be able to test the footer better. Note that I'm applying to the view wrapping the templates, so it applies to all templates. We might want to move it inside the templates, depending on its usage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No this is great! iOS does this too for all templates: https://github.com/RevenueCat/purchases-ios/blob/main/RevenueCatUI/Templates/TemplateViewType.swift#L179 |
||
Box( | ||
modifier = Modifier | ||
.conditional(state.isInFullScreenMode) { | ||
Modifier.background(backgroundColor) | ||
} | ||
.conditional(!state.isInFullScreenMode) { | ||
Modifier | ||
.clip( | ||
RoundedCornerShape( | ||
topStart = footerRoundedBorderHeight, | ||
topEnd = footerRoundedBorderHeight, | ||
), | ||
) | ||
.background(backgroundColor) | ||
.padding(top = footerRoundedBorderHeight) | ||
}, | ||
) { | ||
when (state.templateConfiguration.template) { | ||
PaywallTemplate.TEMPLATE_1 -> Template1(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_2 -> Template2(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_3 -> Template3(state = state, viewModel = viewModel) | ||
PaywallTemplate.TEMPLATE_4 -> Text(text = "Error: Template 4 not supported") | ||
PaywallTemplate.TEMPLATE_5 -> Text(text = "Error: Template 5 not supported") | ||
} | ||
tonidero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun getPaywallViewModel( | ||
mode: PaywallViewMode, | ||
offering: Offering?, | ||
listener: PaywallViewListener?, | ||
options: PaywallViewOptions, | ||
): PaywallViewModel { | ||
val applicationContext = LocalContext.current.applicationContext | ||
return viewModel<PaywallViewModelImpl>( | ||
// We need to pass the key in order to create different view models for different offerings when | ||
// trying to load different paywalls for the same view model store owner. | ||
key = offering?.identifier, | ||
key = options.offering?.identifier, | ||
factory = PaywallViewModelFactory( | ||
applicationContext.toAndroidContext(), | ||
mode, | ||
offering, | ||
listener, | ||
options, | ||
MaterialTheme.colorScheme, | ||
preview = isInPreviewMode(), | ||
), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package com.revenuecat.purchases.ui.revenuecatui | ||
|
||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.PaddingValues | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.rememberScrollState | ||
import androidx.compose.foundation.verticalScroll | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
import com.revenuecat.purchases.ui.revenuecatui.helpers.isInPreviewMode | ||
|
||
internal val footerRoundedBorderHeight = 12.dp | ||
tonidero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Composable | ||
fun PaywallFooter( | ||
paywallViewOptions: PaywallViewOptions = PaywallViewOptions.Builder().build(), | ||
condensed: Boolean = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
mainContent: @Composable (PaddingValues) -> Unit, | ||
) { | ||
Column( | ||
modifier = Modifier.fillMaxSize(), | ||
// This is a workaround to make the main content be able to go below the footer, so it's visible through | ||
// the rounded corners. We pass this padding back to the developer so they can add this padding to their content | ||
verticalArrangement = Arrangement.spacedBy(-footerRoundedBorderHeight), | ||
) { | ||
Box( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.weight(1f), | ||
) { | ||
mainContent(PaddingValues(bottom = footerRoundedBorderHeight)) | ||
} | ||
val mode = if (condensed) PaywallViewMode.FOOTER_CONDENSED else PaywallViewMode.FOOTER | ||
tonidero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (isInPreviewMode()) { | ||
Box( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.height(200.dp) | ||
.background(Color.Blue), | ||
) | ||
tonidero marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
PaywallView(paywallViewOptions.changeMode(mode)) | ||
} | ||
} | ||
} | ||
|
||
@Suppress("MagicNumber") | ||
@Preview(showBackground = true) | ||
@Composable | ||
private fun PaywallFooterPreview() { | ||
PaywallFooter { | ||
Column( | ||
modifier = Modifier | ||
.fillMaxWidth() | ||
.verticalScroll(rememberScrollState()) | ||
.padding(it), | ||
) { | ||
// TODO-PAYWALLS: Implement an actual sample paywall | ||
for (i in 1..50) { | ||
Text(text = "Main content $i") | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
package com.revenuecat.purchases.ui.revenuecatui.composables | ||
|
||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.BoxScope | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.BlurredEdgeTreatment | ||
|
@@ -12,12 +12,12 @@ import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfigura | |
import com.revenuecat.purchases.ui.revenuecatui.extensions.conditional | ||
|
||
@Composable | ||
internal fun PaywallBackground(templateConfiguration: TemplateConfiguration) { | ||
internal fun BoxScope.PaywallBackground(templateConfiguration: TemplateConfiguration) { | ||
templateConfiguration.images.backgroundUri?.let { | ||
RemoteImage( | ||
urlString = it.toString(), | ||
modifier = Modifier | ||
.fillMaxSize() | ||
.matchParentSize() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was running into a few issues with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense 👍🏻 |
||
.conditional(templateConfiguration.configuration.blurredBackgroundImage) { | ||
// TODO-PAYWALLS: backwards compatibility for blurring | ||
blur(BackgroundUIConstants.blurSize, edgeTreatment = BlurredEdgeTreatment.Unbounded) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note how I've changed from passing each of these separately to passing them all as part of the
PaywallViewOptions
. Since we need most of these from the view model anyway, I think this is cleaner.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice good refactor.