Skip to content

Commit 3925c0f

Browse files
committed
feat(RevenueCatUI): seed state store per presentation, shared across workflow screens
1 parent 26c526a commit 3925c0f

4 files changed

Lines changed: 26 additions & 0 deletions

File tree

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallState.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ internal sealed interface PaywallState {
120120
initialSelectedTabIndex: Int? = null,
121121
initialSheetState: SimpleSheetState = SimpleSheetState(),
122122
private val purchases: PurchasesType,
123+
/**
124+
* Presentation-session store for state-driven paywalls, seeded from the paywall's declared state defaults.
125+
*/
126+
val stateStore: PaywallStateStore = PaywallStateStore(emptyMap()),
123127
) : Loaded {
124128

125129
/**

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ internal class PaywallViewModelImpl(
208208
private var currentWorkflowPresentedOfferingContext: PresentedOfferingContext? = null
209209
private var currentWorkflowStepTracksPaywallEvents = true
210210
private val workflowStepStateCache = mutableMapOf<String, PaywallState.Loaded.Components>()
211+
212+
// Shared across all screens of a workflow presentation so state-driven values survive screen navigation.
213+
private var currentWorkflowStateStore: PaywallStateStore? = null
211214
private var preWarmJob: Job? = null
212215
private var transitionIdCounter: Int = 0
213216

@@ -368,6 +371,7 @@ internal class PaywallViewModelImpl(
368371
currentWorkflowPresentedOfferingContext = null
369372
currentWorkflowStepTracksPaywallEvents = true
370373
workflowStepStateCache.clear()
374+
currentWorkflowStateStore = null
371375
_workflowState.value = null
372376
// The dismiss is the session boundary: the next presentation on this ViewModel is a new session,
373377
// so completion from this one must not suppress its abandonment. Runs after closePaywall has
@@ -1008,6 +1012,9 @@ internal class PaywallViewModelImpl(
10081012
_workflowState.value = null
10091013
if (isNewWorkflowImpression) {
10101014
workflowTraceId = UUID.randomUUID().toString()
1015+
// Fresh presentation: start the shared store empty; each step registers its declarations as it builds.
1016+
// Rebuilds (navigation, color change) reuse the existing store so values persist across screens.
1017+
currentWorkflowStateStore = PaywallStateStore(emptyMap())
10111018
}
10121019

10131020
// Pre-compute the package step so its default package is available in cache
@@ -1130,6 +1137,7 @@ internal class PaywallViewModelImpl(
11301137
colorScheme = _colorScheme.value,
11311138
storefrontCountryCode = purchases.storefrontCountryCode,
11321139
mode = options.mode,
1140+
stateStore = currentWorkflowStateStore,
11331141
)
11341142
}
11351143

@@ -1386,6 +1394,7 @@ internal class PaywallViewModelImpl(
13861394
colorScheme: ColorScheme,
13871395
storefrontCountryCode: String?,
13881396
mode: PaywallMode,
1397+
stateStore: PaywallStateStore? = null,
13891398
): PaywallState {
13901399
if (offering.availablePackages.isEmpty()) {
13911400
return PaywallState.Error("No packages available")
@@ -1424,6 +1433,7 @@ internal class PaywallViewModelImpl(
14241433
purchases = purchases,
14251434
customVariables = options.customVariables,
14261435
defaultCustomVariables = extractDefaultCustomVariables(offering),
1436+
stateStore = stateStore,
14271437
)
14281438
}
14291439
}

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/OfferingToStateMapper.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import com.revenuecat.purchases.ui.revenuecatui.components.style.StackComponentS
4343
import com.revenuecat.purchases.ui.revenuecatui.components.style.StyleFactory
4444
import com.revenuecat.purchases.ui.revenuecatui.composables.PaywallIconName
4545
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState
46+
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallStateStore
4647
import com.revenuecat.purchases.ui.revenuecatui.data.PurchasesType
4748
import com.revenuecat.purchases.ui.revenuecatui.data.processed.PackageConfigurationType
4849
import com.revenuecat.purchases.ui.revenuecatui.data.processed.PaywallTemplate
@@ -244,6 +245,7 @@ internal fun Offering.validatePaywallComponentsDataOrNull(
244245
?: headerResult?.defaultTabIndex
245246
?: stickyFooterResult?.defaultTabIndex,
246247
mainStackHasHeroImage = backendRootComponentResult.heroImageDetected,
248+
stateDeclarations = componentsData.stateDeclarations.orEmpty(),
247249
)
248250
}
249251
}
@@ -366,11 +368,18 @@ internal fun Offering.toComponentsPaywallState(
366368
purchases: PurchasesType,
367369
customVariables: Map<String, CustomVariableValue> = emptyMap(),
368370
defaultCustomVariables: Map<String, CustomVariableValue> = emptyMap(),
371+
stateStore: PaywallStateStore? = null,
369372
): PaywallState.Loaded.Components {
370373
val showPricesWithDecimals = storefrontCountryCode?.let {
371374
!validationResult.zeroDecimalPlaceCountries.contains(it)
372375
} ?: true
373376

377+
// A workflow shares one store across its screens, accumulating each screen's declarations; a standalone paywall
378+
// gets its own store seeded from its declarations.
379+
val resolvedStateStore = stateStore
380+
?.also { it.registerDeclarations(validationResult.stateDeclarations) }
381+
?: PaywallStateStore(validationResult.stateDeclarations)
382+
374383
return PaywallState.Loaded.Components(
375384
stack = validationResult.stack,
376385
header = validationResult.header,
@@ -389,6 +398,7 @@ internal fun Offering.toComponentsPaywallState(
389398
initialSelectedTabIndex = validationResult.initialSelectedTabIndex,
390399
mainStackHasHeroImage = validationResult.mainStackHasHeroImage,
391400
purchases = purchases,
401+
stateStore = resolvedStateStore,
392402
)
393403
}
394404

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/helpers/PaywallValidationResult.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.revenuecat.purchases.ui.revenuecatui.helpers
33
import com.revenuecat.purchases.UiConfig
44
import com.revenuecat.purchases.paywalls.PaywallData
55
import com.revenuecat.purchases.paywalls.components.common.LocaleId
6+
import com.revenuecat.purchases.paywalls.components.common.StateDeclaration
67
import com.revenuecat.purchases.ui.revenuecatui.components.properties.BackgroundStyles
78
import com.revenuecat.purchases.ui.revenuecatui.components.style.ComponentStyle
89
import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState.Loaded.Components.AvailablePackages
@@ -53,6 +54,7 @@ internal sealed interface PaywallValidationResult {
5354
val packages: AvailablePackages,
5455
val initialSelectedTabIndex: Int?,
5556
val mainStackHasHeroImage: Boolean = false,
57+
val stateDeclarations: Map<String, StateDeclaration> = emptyMap(),
5658
) : PaywallValidationResult {
5759
// If a Components Paywall has an error, it will be reflected as a Legacy type so we can use the Legacy
5860
// fallback.

0 commit comments

Comments
 (0)