Skip to content

Commit d4e83d3

Browse files
committed
fix(RevenueCatUI): publish tab state before the visibility check so a hidden tabs still publishes
1 parent 863c2a4 commit d4e83d3

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

  • ui/revenuecatui/src

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabsComponentView.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,18 @@ internal fun TabsComponentView(
8181
style = style,
8282
paywallState = state,
8383
)
84-
if (!tabsState.visible) return
85-
8684
// State-driven paywalls: publish the selected tab id into the state store so components that react to it via a
87-
// `state_condition` override recompose. Runs on first composition (seed) and whenever the selection changes.
85+
// `state_condition` override recompose. Done before the visibility check so a hidden Tabs still publishes its
86+
// selected state. Runs on first composition (seed) and whenever the selection changes.
8887
val selectedTabId = style.tabs[state.selectedTabIndex.coerceIn(0..style.tabs.lastIndex)].id
8988
LaunchedEffect(selectedTabId) {
9089
style.stateUpdates?.takeIf { it.isNotEmpty() }?.let { updates ->
9190
state.stateStore.applyUpdates(updates, payload = JsonPrimitive(selectedTabId))
9291
}
9392
}
9493

94+
if (!tabsState.visible) return
95+
9596
val backgroundStyle = tabsState.background?.let { rememberBackgroundStyle(it) }
9697
val borderStyle = tabsState.border?.let { rememberBorderStyle(border = it) }
9798
val shadowStyle = tabsState.shadow?.let { rememberShadowStyle(shadow = it) }

ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs/TabStateTests.kt

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,70 @@ class TabStateTests {
189189
// Selection republished the new tab id.
190190
assertThat(state.stateStore.values[stateKey]).isEqualTo(JsonPrimitive("annual"))
191191
}
192+
193+
/**
194+
* A hidden tabs container still publishes its selected tab id, so sibling components reacting to it are not
195+
* left on a stale value. The declared default differs from the initial (default_tab_id) selection, so only the
196+
* publish (not the declaration seed) can produce the expected value.
197+
*/
198+
@Test
199+
fun `A hidden tabs component still publishes its selected tab id`(): Unit = with(composeTestRule) {
200+
val tab0LabelKey = LocalizationKey("tab0_label")
201+
val tab1LabelKey = LocalizationKey("tab1_label")
202+
val localizations = nonEmptyMapOf(
203+
localeId to nonEmptyMapOf(
204+
tab0LabelKey to LocalizationData.Text("Monthly"),
205+
tab1LabelKey to LocalizationData.Text("Annual"),
206+
),
207+
)
208+
val tabControlButtons = listOf(tab0LabelKey, tab1LabelKey).mapIndexed { index, labelKey ->
209+
TabControlButtonComponent(
210+
tabIndex = index,
211+
tabId = listOf("monthly", "annual")[index],
212+
stack = StackComponent(components = listOf(TextComponent(text = labelKey, color = textColor))),
213+
)
214+
}
215+
val tabsComponent = TabsComponent(
216+
visible = false,
217+
tabs = listOf("monthly", "annual").map { id ->
218+
TabsComponent.Tab(id = id, stack = StackComponent(components = listOf(TabControlComponent)))
219+
},
220+
control = TabsComponent.TabControl.Buttons(stack = StackComponent(components = tabControlButtons)),
221+
defaultTabId = "annual",
222+
stateUpdates = listOf(StateUpdate.Set(stateKey, StateUpdateValue.PayloadReference)),
223+
)
224+
val data = PaywallComponentsData(
225+
id = "paywall_id",
226+
templateName = "template",
227+
assetBaseURL = assetBaseURL,
228+
componentsConfig = ComponentsConfig(
229+
base = PaywallComponentsConfig(
230+
stack = StackComponent(components = listOf(tabsComponent)),
231+
background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
232+
stickyFooter = null,
233+
),
234+
),
235+
componentsLocalizations = localizations,
236+
defaultLocaleIdentifier = localeId,
237+
stateDeclarations = mapOf(
238+
stateKey to StateDeclaration(type = StateDeclaration.ValueType.STRING, defaultValue = JsonPrimitive("monthly")),
239+
),
240+
)
241+
val offering = Offering(
242+
identifier = "offering-id",
243+
serverDescription = "description",
244+
metadata = emptyMap(),
245+
availablePackages = listOf(TestData.Packages.monthly),
246+
paywallComponents = Offering.PaywallComponents(UiConfig(), data),
247+
)
248+
val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!!
249+
val state = offering.toComponentsPaywallState(validated)
250+
val styleFactory = StyleFactory(localizations = localizations, offering = offering)
251+
val style = styleFactory.create(tabsComponent).getOrThrow().componentStyle as TabsComponentStyle
252+
253+
setContent { TabsComponentView(style = style, state = state, clickHandler = { }) }
254+
waitForIdle()
255+
256+
assertThat(state.stateStore.values[stateKey]).isEqualTo(JsonPrimitive("annual"))
257+
}
192258
}

0 commit comments

Comments
 (0)