Skip to content

Commit

Permalink
[Paywalls V2] TextComponentState handles locale changes (#2000)
Browse files Browse the repository at this point in the history
  • Loading branch information
JayShortway authored Dec 23, 2024
1 parent 1d2b0de commit a58fa83
Show file tree
Hide file tree
Showing 24 changed files with 517 additions and 177 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ internal fun LoadedPaywallComponents(
val windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val windowSize = ScreenCondition.from(windowSizeClass.windowWidthSizeClass)

val styleFactory = remember(state.locale, windowSize) { StyleFactory(state.localizationDictionary) }
val styleFactory = remember(state.locale, windowSize) {
StyleFactory(state.data.componentsLocalizations)
}

val config = state.data.componentsConfig.base
val actionHandler: suspend (PaywallAction) -> Unit = { /* TODO Implement action handler */ }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.revenuecat.purchases.ui.revenuecatui.components

import com.revenuecat.purchases.paywalls.components.PartialTextComponent
import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.common.LocalizationDictionary
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.stringForAllLocales
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
import com.revenuecat.purchases.ui.revenuecatui.helpers.Result
import com.revenuecat.purchases.ui.revenuecatui.helpers.map
import com.revenuecat.purchases.ui.revenuecatui.helpers.orSuccessfullyNull
import dev.drewhamilton.poko.Poko

@Poko
internal class LocalizedTextPartial private constructor(
@get:JvmSynthetic val text: String?,
@get:JvmSynthetic val texts: Map<LocaleId, String>?,
@get:JvmSynthetic val partial: PartialTextComponent,
) : PresentedPartial<LocalizedTextPartial> {

Expand All @@ -24,14 +26,14 @@ internal class LocalizedTextPartial private constructor(
@JvmSynthetic
operator fun invoke(
from: PartialTextComponent,
using: LocalizationDictionary,
): Result<LocalizedTextPartial, PaywallValidationError> =
using: Map<LocaleId, LocalizationDictionary>,
): Result<LocalizedTextPartial, NonEmptyList<PaywallValidationError>> =
from.text
?.let(using::string)
?.let { localizationKey -> using.stringForAllLocales(localizationKey) }
.orSuccessfullyNull()
.map { string: String? ->
.map { texts ->
LocalizedTextPartial(
text = string,
texts = texts,
partial = from,
)
}
Expand All @@ -42,7 +44,7 @@ internal class LocalizedTextPartial private constructor(
val otherPartial = with?.partial

return LocalizedTextPartial(
text = with?.text ?: text,
texts = with?.texts ?: texts,
partial = PartialTextComponent(
visible = otherPartial?.visible ?: partial.visible,
text = otherPartial?.text ?: partial.text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.revenuecat.purchases.ui.revenuecatui.components
import com.revenuecat.purchases.paywalls.components.PartialComponent
import com.revenuecat.purchases.paywalls.components.common.ComponentOverrides
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError
import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
import com.revenuecat.purchases.ui.revenuecatui.helpers.Result
import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrElse
import dev.drewhamilton.poko.Poko
Expand Down Expand Up @@ -83,21 +84,21 @@ internal class PresentedConditions<T : PresentedPartial<T>>(
@Suppress("ReturnCount")
@JvmSynthetic
internal fun <T : PartialComponent, P : PresentedPartial<P>> ComponentOverrides<T>.toPresentedOverrides(
transform: (T) -> Result<P, PaywallValidationError>,
transform: (T) -> Result<P, NonEmptyList<PaywallValidationError>>,
): Result<PresentedOverrides<P>, PaywallValidationError> {
val introOffer = introOffer?.let(transform)
?.getOrElse { return Result.Error(it) }
?.getOrElse { return Result.Error(it.head) }

val selectedState = states?.selected?.let(transform)
?.getOrElse { return Result.Error(it) }
?.getOrElse { return Result.Error(it.head) }

val states = states?.let { PresentedStates(selected = selectedState) }

val conditions = conditions?.let { conditions ->
PresentedConditions(
compact = conditions.compact?.let(transform)?.getOrElse { return Result.Error(it) },
medium = conditions.medium?.let(transform)?.getOrElse { return Result.Error(it) },
expanded = conditions.expanded?.let(transform)?.getOrElse { return Result.Error(it) },
compact = conditions.compact?.let(transform)?.getOrElse { return Result.Error(it.head) },
medium = conditions.medium?.let(transform)?.getOrElse { return Result.Error(it.head) },
expanded = conditions.expanded?.let(transform)?.getOrElse { return Result.Error(it.head) },
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private fun previewButtonComponentStyle(
stackComponentStyle: StackComponentStyle = StackComponentStyle(
children = listOf(
TextComponentStyle(
text = "Restore purchases",
texts = mapOf(LocaleId("en_US") to "Restore purchases"),
color = ColorScheme(
light = ColorInfo.Hex(Color.Black.toArgb()),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.revenuecat.purchases.ui.revenuecatui.components.ktx

import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.common.LocalizationData
import com.revenuecat.purchases.paywalls.components.common.LocalizationDictionary
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError.MissingImageLocalization
import com.revenuecat.purchases.ui.revenuecatui.errors.PaywallValidationError.MissingStringLocalization
import com.revenuecat.purchases.ui.revenuecatui.helpers.NonEmptyList
import com.revenuecat.purchases.ui.revenuecatui.helpers.Result
import com.revenuecat.purchases.ui.revenuecatui.helpers.mapError
import com.revenuecat.purchases.ui.revenuecatui.helpers.mapValuesOrAccumulate
import com.revenuecat.purchases.ui.revenuecatui.helpers.nonEmptyListOf
import androidx.compose.ui.text.intl.Locale as ComposeLocale
import java.util.Locale as JavaLocale

/**
* Retrieves a string from this [LocalizationDictionary] associated with the provided [key].
Expand All @@ -20,6 +27,22 @@ internal fun LocalizationDictionary.string(key: LocalizationKey): Result<String,
?.let { Result.Success(it) }
?: Result.Error(MissingStringLocalization(key))

/**
* Retrieves a string for all locales in this map, associated with the provided [key].
*
* @return A successful result containing the string keyed by the locale if it was found for all locales, or an error
* result containing a [MissingStringLocalization] error for each locale the [key] wasn't found for.
*/
@JvmSynthetic
internal fun Map<LocaleId, LocalizationDictionary>.stringForAllLocales(
key: LocalizationKey,
): Result<Map<LocaleId, String>, NonEmptyList<MissingStringLocalization>> =
mapValues { (locale, localizationDictionary) ->
localizationDictionary
.string(key)
.mapError { nonEmptyListOf(MissingStringLocalization(key, locale)) }
}.mapValuesOrAccumulate { it }

/**
* Retrieves an image from this [LocalizationDictionary] associated with the provided [key].
*
Expand All @@ -31,3 +54,15 @@ internal fun LocalizationDictionary.image(key: LocalizationKey): Result<ThemeIma
(get(key) as? LocalizationData.Image)?.value
?.let { Result.Success(it) }
?: Result.Error(MissingImageLocalization(key))

@JvmSynthetic
internal fun LocaleId.toComposeLocale(): ComposeLocale =
ComposeLocale(value.replace('_', '-'))

@JvmSynthetic
internal fun LocaleId.toJavaLocale(): JavaLocale =
JavaLocale.forLanguageTag(value.replace('_', '-'))

@JvmSynthetic
internal fun ComposeLocale.toLocaleId(): LocaleId =
LocaleId(toLanguageTag().replace('-', '_'))
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private fun StackComponentView_Preview_ZLayer() {
style = StackComponentStyle(
children = listOf(
TextComponentStyle(
text = "Hello",
texts = mapOf(LocaleId("en_US") to "Hello"),
color = ColorScheme(
light = ColorInfo.Hex(Color.Black.toArgb()),
),
Expand All @@ -230,7 +230,7 @@ private fun StackComponentView_Preview_ZLayer() {
overrides = null,
),
TextComponentStyle(
text = "World",
texts = mapOf(LocaleId("en_US") to "World"),
color = ColorScheme(
light = ColorInfo.Hex(Color.Black.toArgb()),
),
Expand Down Expand Up @@ -275,7 +275,7 @@ private fun StackComponentView_Preview_ZLayer() {
@Composable
private fun previewChildren() = listOf(
TextComponentStyle(
text = "Hello",
texts = mapOf(LocaleId("en_US") to "Hello"),
color = ColorScheme(
light = ColorInfo.Hex(Color.Black.toArgb()),
),
Expand All @@ -293,7 +293,7 @@ private fun previewChildren() = listOf(
overrides = null,
),
TextComponentStyle(
text = "World",
texts = mapOf(LocaleId("en_US") to "World"),
color = ColorScheme(
light = ColorInfo.Hex(Color.Black.toArgb()),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import com.revenuecat.purchases.paywalls.components.PurchaseButtonComponent
import com.revenuecat.purchases.paywalls.components.StackComponent
import com.revenuecat.purchases.paywalls.components.StickyFooterComponent
import com.revenuecat.purchases.paywalls.components.TextComponent
import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.common.LocalizationDictionary
import com.revenuecat.purchases.ui.revenuecatui.components.LocalizedTextPartial
import com.revenuecat.purchases.ui.revenuecatui.components.PaywallAction
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedStackPartial
import com.revenuecat.purchases.ui.revenuecatui.components.SystemFontFamily
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.string
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.stringForAllLocales
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues
Expand All @@ -33,7 +34,7 @@ import com.revenuecat.purchases.ui.revenuecatui.helpers.orSuccessfullyNull
import com.revenuecat.purchases.ui.revenuecatui.helpers.zipOrAccumulate

internal class StyleFactory(
private val localizationDictionary: LocalizationDictionary,
private val localizations: Map<LocaleId, LocalizationDictionary>,
) {

private companion object {
Expand Down Expand Up @@ -131,17 +132,17 @@ internal class StyleFactory(
private fun createTextComponentStyle(
component: TextComponent,
): Result<TextComponentStyle, NonEmptyList<PaywallValidationError>> = zipOrAccumulate(
// Get our text from the localization dictionary.
first = localizationDictionary.string(component.text).mapError { nonEmptyListOf(it) },
// Get our texts from the localization dictionary.
first = localizations.stringForAllLocales(component.text),
second = component.overrides
// Map all overrides to PresentedOverrides.
?.toPresentedOverrides { LocalizedTextPartial(from = it, using = localizationDictionary) }
?.toPresentedOverrides { LocalizedTextPartial(from = it, using = localizations) }
.orSuccessfullyNull()
.mapError { nonEmptyListOf(it) },
) { text, presentedOverrides ->
) { texts, presentedOverrides ->
val weight = component.fontWeight.toFontWeight()
TextComponentStyle(
text = text,
texts = texts,
color = component.color,
fontSize = component.fontSize,
fontWeight = weight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
import com.revenuecat.purchases.paywalls.components.properties.FontSize
import com.revenuecat.purchases.paywalls.components.properties.Size
Expand All @@ -16,7 +17,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.PresentedOverrides
@Immutable
internal class TextComponentStyle(
@get:JvmSynthetic
val text: String,
val texts: Map<LocaleId, String>,
@get:JvmSynthetic
val color: ColorScheme,
@get:JvmSynthetic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.intl.Locale
import androidx.window.core.layout.WindowWidthSizeClass
import com.revenuecat.purchases.ui.revenuecatui.components.ComponentViewState
import com.revenuecat.purchases.ui.revenuecatui.components.ScreenCondition
import com.revenuecat.purchases.ui.revenuecatui.components.SystemFontFamily
import com.revenuecat.purchases.ui.revenuecatui.components.buildPresentedPartial
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toAlignment
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toFontWeight
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toLocaleId
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toPaddingValues
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toTextAlign
import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle
Expand All @@ -31,6 +33,7 @@ internal fun rememberUpdatedTextComponentState(
): TextComponentState =
rememberUpdatedTextComponentState(
style = style,
localeProvider = { paywallState.locale },
isEligibleForIntroOffer = paywallState.isEligibleForIntroOffer,
selected = selected,
)
Expand All @@ -39,6 +42,7 @@ internal fun rememberUpdatedTextComponentState(
@Composable
internal fun rememberUpdatedTextComponentState(
style: TextComponentStyle,
localeProvider: () -> Locale,
isEligibleForIntroOffer: Boolean = false,
selected: Boolean = false,
): TextComponentState {
Expand All @@ -50,6 +54,7 @@ internal fun rememberUpdatedTextComponentState(
initialIsEligibleForIntroOffer = isEligibleForIntroOffer,
initialSelected = selected,
style = style,
localeProvider = localeProvider,
)
}.apply {
update(
Expand All @@ -66,6 +71,7 @@ internal class TextComponentState(
initialIsEligibleForIntroOffer: Boolean,
initialSelected: Boolean,
private val style: TextComponentStyle,
private val localeProvider: () -> Locale,
) {
private var windowSize by mutableStateOf(initialWindowSize)
private var isEligibleForIntroOffer by mutableStateOf(initialIsEligibleForIntroOffer)
Expand All @@ -81,7 +87,12 @@ internal class TextComponentState(
val visible by derivedStateOf { presentedPartial?.partial?.visible ?: true }

@get:JvmSynthetic
val text by derivedStateOf { presentedPartial?.text ?: style.text }
val text by derivedStateOf {
val localeId = localeProvider().toLocaleId()
// TODO Fallback to default locale
presentedPartial?.texts?.getValue(localeId)
?: style.texts.getValue(localeId)
}

@get:JvmSynthetic
val color by derivedStateOf { presentedPartial?.partial?.color ?: style.color }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ private fun previewTextComponentStyle(
): TextComponentStyle {
val weight = fontWeight.toFontWeight()
return TextComponentStyle(
text = text,
texts = mapOf(LocaleId("en_US") to text),
color = color,
fontSize = fontSize,
fontWeight = weight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.intl.LocaleList
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.paywalls.components.common.LocaleId
import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toComposeLocale
import com.revenuecat.purchases.ui.revenuecatui.components.ktx.toLocaleId
import com.revenuecat.purchases.ui.revenuecatui.data.processed.ProcessedLocalizedConfiguration
import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration
import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger
Expand Down Expand Up @@ -67,8 +68,7 @@ internal sealed interface PaywallState {
) : Loaded {
private var localeId by mutableStateOf(initialLocaleList.toLocaleId())

val localizationDictionary by derivedStateOf { data.componentsLocalizations.getValue(localeId) }
val locale by derivedStateOf { localeId.toLocale() }
val locale by derivedStateOf { localeId.toComposeLocale() }

var isEligibleForIntroOffer by mutableStateOf(initialIsEligibleForIntroOffer)
private set
Expand All @@ -94,12 +94,6 @@ internal sealed interface PaywallState {
// Find the first locale we have a LocalizationDictionary for.
.first { id -> data.componentsLocalizations.containsKey(id) }

private fun LocaleId.toLocale(): Locale =
Locale(value.replace('_', '-'))

private fun Locale.toLocaleId(): LocaleId =
LocaleId(toLanguageTag().replace('-', '_'))

private fun List<Package>.mostExpensivePricePerMonthMicros(): Long? =
asSequence()
.map { pkg -> pkg.product }
Expand Down
Loading

0 comments on commit a58fa83

Please sign in to comment.