diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/PaywallComponent.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/PaywallComponent.kt index 2cbae0651a..7ed445b87a 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/PaywallComponent.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/PaywallComponent.kt @@ -47,6 +47,10 @@ internal class PaywallComponentSerializer : KSerializer { "icon" -> jsonDecoder.json.decodeFromString(json.toString()) "timeline" -> jsonDecoder.json.decodeFromString(json.toString()) "carousel" -> jsonDecoder.json.decodeFromString(json.toString()) + "tab_control_button" -> jsonDecoder.json.decodeFromString(json.toString()) + "tab_control_toggle" -> jsonDecoder.json.decodeFromString(json.toString()) + "tab_control" -> jsonDecoder.json.decodeFromString(json.toString()) + "tabs" -> jsonDecoder.json.decodeFromString(json.toString()) else -> json["fallback"] ?.let { it as? JsonObject } ?.toString() diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/TabsComponent.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/TabsComponent.kt new file mode 100644 index 0000000000..e51428e72f --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/TabsComponent.kt @@ -0,0 +1,131 @@ +@file:Suppress("LongParameterList") + +package com.revenuecat.purchases.paywalls.components + +import com.revenuecat.purchases.InternalRevenueCatAPI +import com.revenuecat.purchases.paywalls.components.common.ComponentOverrides +import com.revenuecat.purchases.paywalls.components.properties.Border +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shadow +import com.revenuecat.purchases.paywalls.components.properties.Shape +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fill +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint.Fit +import dev.drewhamilton.poko.Poko +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@InternalRevenueCatAPI +@Poko +@Serializable +@SerialName("tab_control_button") +class TabControlButtonComponent( + @get:JvmSynthetic + @SerialName("tab_index") + val tabIndex: Int, + @get:JvmSynthetic + val stack: StackComponent, +) : PaywallComponent + +@InternalRevenueCatAPI +@Poko +@Serializable +@SerialName("tab_control_toggle") +class TabControlToggleComponent( + @get:JvmSynthetic + @SerialName("default_value") + val defaultValue: Boolean, + @get:JvmSynthetic + @SerialName("thumb_color_on") + val thumbColorOn: ColorScheme, + @get:JvmSynthetic + @SerialName("thumb_color_off") + val thumbColorOff: ColorScheme, + @get:JvmSynthetic + @SerialName("track_color_on") + val trackColorOn: ColorScheme, + @get:JvmSynthetic + @SerialName("track_color_off") + val trackColorOff: ColorScheme, +) : PaywallComponent + +@InternalRevenueCatAPI +@Serializable +@SerialName("tab_control") +object TabControlComponent : PaywallComponent + +@InternalRevenueCatAPI +@Poko +@Serializable +@SerialName("tabs") +class TabsComponent( + @get:JvmSynthetic + val size: Size = Size(width = Fill, height = Fit), + @get:JvmSynthetic + val padding: Padding = Padding.zero, + @get:JvmSynthetic + val margin: Padding = Padding.zero, + @get:JvmSynthetic + @SerialName("background_color") + val backgroundColor: ColorScheme? = null, + @get:JvmSynthetic + val shape: Shape? = null, + @get:JvmSynthetic + val border: Border? = null, + @get:JvmSynthetic + val shadow: Shadow? = null, + @get:JvmSynthetic + val control: TabControl, + @get:JvmSynthetic + val tabs: List, + @get:JvmSynthetic + val overrides: ComponentOverrides? = null, +) : PaywallComponent { + @InternalRevenueCatAPI + @Poko + @Serializable + class Tab( + @get:JvmSynthetic + val stack: StackComponent, + ) + + @InternalRevenueCatAPI + @Serializable + sealed interface TabControl { + @InternalRevenueCatAPI + @Poko + @Serializable + @SerialName("buttons") + class Buttons(@get:JvmSynthetic val stack: StackComponent) : TabControl + + @InternalRevenueCatAPI + @Poko + @Serializable + @SerialName("toggle") + class Toggle(@get:JvmSynthetic val stack: StackComponent) : TabControl + } +} + +@InternalRevenueCatAPI +@Poko +@Serializable +class PartialTabsComponent( + @get:JvmSynthetic + val visible: Boolean? = true, + @get:JvmSynthetic + val size: Size? = null, + @get:JvmSynthetic + val padding: Padding? = null, + @get:JvmSynthetic + val margin: Padding? = null, + @get:JvmSynthetic + @SerialName("background_color") + val backgroundColor: ColorScheme? = null, + @get:JvmSynthetic + val shape: Shape? = null, + @get:JvmSynthetic + val border: Border? = null, + @get:JvmSynthetic + val shadow: Shadow? = null, +) : PartialComponent diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/TabsComponentTests.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/TabsComponentTests.kt new file mode 100644 index 0000000000..f549a68c12 --- /dev/null +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/TabsComponentTests.kt @@ -0,0 +1,386 @@ +package com.revenuecat.purchases.paywalls.components + +import com.revenuecat.purchases.ColorAlias +import com.revenuecat.purchases.common.OfferingParser +import com.revenuecat.purchases.paywalls.components.common.LocalizationKey +import com.revenuecat.purchases.paywalls.components.properties.Border +import com.revenuecat.purchases.paywalls.components.properties.ColorInfo +import com.revenuecat.purchases.paywalls.components.properties.ColorScheme +import com.revenuecat.purchases.paywalls.components.properties.Padding +import com.revenuecat.purchases.paywalls.components.properties.Shadow +import com.revenuecat.purchases.paywalls.components.properties.Shape +import com.revenuecat.purchases.paywalls.components.properties.Size +import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint +import org.intellij.lang.annotations.Language +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +internal class TabsComponentTests( + @Suppress("UNUSED_PARAMETER") name: String, + private val args: Args, +) { + + class Args( + @Language("json") + val json: String, + val expected: PaywallComponent, + ) + + companion object { + + @Suppress("LongMethod") + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters(): Collection<*> = listOf( + arrayOf( + "tab_control_button", + Args( + json = """ + { + "type": "tab_control_button", + "tab_index": 0, + "stack": { + "type": "stack", + "components": [ + { + "color": { + "light": { + "type": "alias", + "value": "primary" + } + }, + "components": [], + "id": "xmpgCrN9Rb", + "name": "Text", + "text_lid": "7bkohQjzIE", + "type": "text" + } + ] + } + } + """.trimIndent(), + expected = TabControlButtonComponent( + tabIndex = 0, + stack = StackComponent( + components = listOf( + TextComponent( + text = LocalizationKey("7bkohQjzIE"), + color = ColorScheme(light = ColorInfo.Alias(ColorAlias("primary"))) + ) + ), + ) + ) + ), + ), + arrayOf( + "tab_control_toggle", + Args( + json = """ + { + "type": "tab_control_toggle", + "default_value": true, + "thumb_color_on": { + "light": { + "type": "alias", + "value": "primary" + } + }, + "thumb_color_off": { + "light": { + "type": "alias", + "value": "secondary" + } + }, + "track_color_on": { + "light": { + "type": "alias", + "value": "tertiary" + } + }, + "track_color_off": { + "light": { + "type": "alias", + "value": "primary_alt" + } + } + } + """.trimIndent(), + expected = TabControlToggleComponent( + defaultValue = true, + thumbColorOn = ColorScheme(light = ColorInfo.Alias(ColorAlias("primary"))), + thumbColorOff = ColorScheme(light = ColorInfo.Alias(ColorAlias("secondary"))), + trackColorOn = ColorScheme(light = ColorInfo.Alias(ColorAlias("tertiary"))), + trackColorOff = ColorScheme(light = ColorInfo.Alias(ColorAlias("primary_alt"))), + ) + ), + ), + arrayOf( + "tab_control", + Args( + json = """ + { + "type": "tab_control" + } + """.trimIndent(), + expected = TabControlComponent, + ), + ), + arrayOf( + "stack with tab_control", + Args( + json = """ + { + "type": "stack", + "components": [ + { + "type": "tab_control" + } + ] + } + """.trimIndent(), + expected = StackComponent( + components = listOf( + TabControlComponent, + ), + ) + ), + ), + arrayOf( + "tabs - all values present", + Args( + json = """ + { + "type": "tabs", + "control": { + "type": "buttons", + "stack": { + "type": "stack", + "components": [ + { + "type": "tab_control_button", + "tab_index": 0, + "stack": { + "components": [] + } + }, + { + "type": "tab_control_button", + "tab_index": 1, + "stack": { + "components": [] + } + } + ] + } + }, + "tabs": [ + { + "stack": { + "components": [ + { + "type": "tab_control" + } + ] + } + }, + { + "stack": { + "components": [ + { + "type": "tab_control" + } + ] + } + } + ], + "background_color": { + "light": { + "type": "alias", + "value": "secondary" + } + }, + "border": { + "color": { + "light": { + "type": "alias", + "value": "primary" + } + }, + "width": 123 + }, + "size": { + "width": { + "type": "fill", + "value": null + }, + "height": { + "type": "fit", + "value": null + } + }, + "id": "WLbwQoNUKF", + "margin": { + "bottom": 10, + "leading": 12, + "top": 14, + "trailing": 16 + }, + "name": "Tabs", + "padding": { + "bottom": 16, + "leading": 14, + "top": 12, + "trailing": 10 + }, + "shape": { + "type": "pill" + }, + "shadow": { + "color": { + "light": { + "type": "alias", + "value": "tertiary" + } + }, + "radius": 20.1, + "x": 23.6, + "y": 45.2 + } + } + """.trimIndent(), + expected = TabsComponent( + control = TabsComponent.TabControl.Buttons( + stack = StackComponent( + components = listOf( + TabControlButtonComponent( + tabIndex = 0, + stack = StackComponent(components = emptyList()) + ), + TabControlButtonComponent( + tabIndex = 1, + stack = StackComponent(components = emptyList()) + ), + ) + ) + ), + tabs = listOf( + TabsComponent.Tab(stack = StackComponent(components = listOf(TabControlComponent))), + TabsComponent.Tab(stack = StackComponent(components = listOf(TabControlComponent))), + ), + size = Size(width = SizeConstraint.Fill, height = SizeConstraint.Fit), + backgroundColor = ColorScheme(light = ColorInfo.Alias(ColorAlias("secondary"))), + padding = Padding(top = 12.0, leading = 14.0, bottom = 16.0, trailing = 10.0), + margin = Padding(top = 14.0, leading = 12.0, bottom = 10.0, trailing = 16.0), + shape = Shape.Pill, + border = Border( + color = ColorScheme(light = ColorInfo.Alias(ColorAlias("primary"))), + width = 123.0 + ), + shadow = Shadow( + color = ColorScheme(light = ColorInfo.Alias(ColorAlias("tertiary"))), + radius = 20.1, + x = 23.6, + y = 45.2 + ), + ) + ), + ), + arrayOf( + "tabs - optional values absent", + Args( + json = """ + { + "type": "tabs", + "control": { + "type": "toggle", + "stack": { + "type": "stack", + "components": [ + { + "type": "tab_control_toggle", + "default_value": true, + "thumb_color_on": { + "light": { + "type": "alias", + "value": "primary" + } + }, + "thumb_color_off": { + "light": { + "type": "alias", + "value": "secondary" + } + }, + "track_color_on": { + "light": { + "type": "alias", + "value": "tertiary" + } + }, + "track_color_off": { + "light": { + "type": "alias", + "value": "primary_alt" + } + } + } + ] + } + }, + "tabs": [ + { + "stack": { + "components": [ + { + "type": "tab_control" + } + ] + } + }, + { + "stack": { + "components": [ + { + "type": "tab_control" + } + ] + } + } + ] + } + """.trimIndent(), + expected = TabsComponent( + control = TabsComponent.TabControl.Toggle( + stack = StackComponent( + components = listOf( + TabControlToggleComponent( + defaultValue = true, + thumbColorOn = ColorScheme(ColorInfo.Alias(ColorAlias("primary"))), + thumbColorOff = ColorScheme(ColorInfo.Alias(ColorAlias("secondary"))), + trackColorOn = ColorScheme(ColorInfo.Alias(ColorAlias("tertiary"))), + trackColorOff = ColorScheme(ColorInfo.Alias(ColorAlias("primary_alt"))) + ) + ) + ) + ), + tabs = listOf( + TabsComponent.Tab(stack = StackComponent(components = listOf(TabControlComponent))), + TabsComponent.Tab(stack = StackComponent(components = listOf(TabControlComponent))), + ), + ) + ), + ), + ) + } + + @Test + fun `Should properly deserialize as PaywallComponent`() { + // Arrange, Act + val actual = OfferingParser.json.decodeFromString(args.json) + + // Assert + assert(actual == args.expected) + } +} diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt index f2d4d36170..ff2262aed2 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt @@ -14,6 +14,10 @@ import com.revenuecat.purchases.paywalls.components.PaywallComponent 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.TabControlButtonComponent +import com.revenuecat.purchases.paywalls.components.TabControlComponent +import com.revenuecat.purchases.paywalls.components.TabControlToggleComponent +import com.revenuecat.purchases.paywalls.components.TabsComponent import com.revenuecat.purchases.paywalls.components.TextComponent import com.revenuecat.purchases.paywalls.components.TimelineComponent import com.revenuecat.purchases.paywalls.components.common.LocaleId @@ -73,6 +77,7 @@ internal class StyleFactory( private val DEFAULT_SHAPE = Shape.Rectangle() } + @Suppress("CyclomaticComplexMethod") fun create( component: PaywallComponent, rcPackage: Package? = null, @@ -88,6 +93,10 @@ internal class StyleFactory( is IconComponent -> createIconComponentStyle(component, rcPackage) is TimelineComponent -> createTimelineComponentStyle(component, rcPackage) is CarouselComponent -> createCarouselComponentStyle(component, rcPackage) + is TabControlButtonComponent -> TODO() + is TabControlComponent -> TODO() + is TabControlToggleComponent -> TODO() + is TabsComponent -> TODO() } private fun createStickyFooterComponentStyle(