@@ -25,7 +25,9 @@ import com.revenuecat.purchases.paywalls.components.common.LocaleId
25
25
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
26
26
import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey
27
27
import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
28
+ import com.revenuecat.purchases.paywalls.components.properties.Dimension
28
29
import com.revenuecat.purchases.paywalls.components.properties.Shape
30
+ import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint
29
31
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls
30
32
import com.revenuecat.purchases.ui.revenuecatui.components.LocalizedTextPartial
31
33
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedCarouselPartial
@@ -102,6 +104,91 @@ internal class StyleFactory(
102
104
*/
103
105
var tabIndex : Int? = null ,
104
106
) {
107
+ private class WindowInsetsState {
108
+ /* *
109
+ * Whether the current component should apply the top window insets. This field is reset when it is read,
110
+ * as it should only be set on a single component.
111
+ */
112
+ var applyTopWindowInsets = false
113
+ get() {
114
+ val value = field
115
+ field = false
116
+ return value
117
+ }
118
+
119
+ /* *
120
+ * Whether the current component should ignore the top window insets. This field is reset when it is read,
121
+ * as it should only be set on a single component.
122
+ */
123
+ var ignoreTopWindowInsets = false
124
+ get() {
125
+ val value = field
126
+ field = false
127
+ return value
128
+ }
129
+
130
+ /* *
131
+ * Whether we have applied the top window insets to any component.
132
+ */
133
+ var topWindowInsetsApplied = false
134
+
135
+ /* *
136
+ * We're only interested in the first non-container component. After that, we can stop looking.
137
+ */
138
+ private var stillLookingForHeaderImage = true
139
+
140
+ /* *
141
+ * This will be called for every component in the tree, and will determine whether we have a header image
142
+ * that needs special top-window-insets treatment. A header image is found if the first non-container
143
+ * component is an image component with a Fill width and a ZLayer parent stack.
144
+ */
145
+ fun handleHeaderImageWindowInsets (component : PaywallComponent ) {
146
+ when (component) {
147
+ is StackComponent -> if (stillLookingForHeaderImage) {
148
+ applyTopWindowInsets = when (component.dimension) {
149
+ is Dimension .ZLayer -> {
150
+ topWindowInsetsApplied = component.components.firstOrNull()?.isHeaderImage == true
151
+ topWindowInsetsApplied
152
+ }
153
+ is Dimension .Horizontal ,
154
+ is Dimension .Vertical ,
155
+ -> false
156
+ }
157
+ }
158
+
159
+ is ImageComponent -> {
160
+ if (stillLookingForHeaderImage) {
161
+ ignoreTopWindowInsets = component.isHeaderImage
162
+ }
163
+ stillLookingForHeaderImage = false
164
+ }
165
+
166
+ else -> stillLookingForHeaderImage = false
167
+ }
168
+ }
169
+
170
+ private val PaywallComponent .isHeaderImage: Boolean
171
+ get() = this is ImageComponent &&
172
+ when (size.width) {
173
+ is SizeConstraint .Fill -> true
174
+ is SizeConstraint .Fit ,
175
+ is SizeConstraint .Fixed ,
176
+ -> false
177
+ }
178
+ }
179
+
180
+ val windowInsetsState = WindowInsetsState ()
181
+
182
+ /* *
183
+ * Whether the current component should apply the top window insets.
184
+ */
185
+ val applyTopWindowInsets by windowInsetsState::applyTopWindowInsets
186
+
187
+ /* *
188
+ * Whether the current component should ignore the top window insets.
189
+ */
190
+ val ignoreTopWindowInsets by windowInsetsState::ignoreTopWindowInsets
191
+
105
192
var defaultTabIndex: Int? = null
106
193
val rcPackage: Package ?
107
194
get() = packageInfo?.pkg
@@ -170,6 +257,41 @@ internal class StyleFactory(
170
257
return result
171
258
}
172
259
260
+ /* *
261
+ * Tells the StyleFactoryScope about a component. This should be called for every component in the tree.
262
+ */
263
+ fun recordComponent (component : PaywallComponent ) {
264
+ windowInsetsState.handleHeaderImageWindowInsets(component)
265
+ }
266
+
267
+ /* *
268
+ * Applies the top window insets to the provided ComponentStyle if they haven't been applied to any other
269
+ * component yet.
270
+ */
271
+ fun applyTopWindowInsetsIfNotYetApplied (to : ComponentStyle ): ComponentStyle =
272
+ when (to) {
273
+ is StackComponentStyle -> to.copy(applyTopWindowInsets = ! windowInsetsState.topWindowInsetsApplied)
274
+ else -> to
275
+ }
276
+
277
+ /* *
278
+ * Applies the bottom window insets to this ComponentStyle if [shouldApply] is true and this is a stack or
279
+ * sticky footer.
280
+ */
281
+ @Suppress(" UNCHECKED_CAST" )
282
+ fun <T : ComponentStyle > T.applyBottomWindowInsetsIfNecessary (shouldApply : Boolean ): T =
283
+ if (shouldApply) {
284
+ when (this ) {
285
+ is StackComponentStyle -> copy(applyBottomWindowInsets = true )
286
+ is StickyFooterComponentStyle -> copy(
287
+ stackComponentStyle = stackComponentStyle.copy(applyBottomWindowInsets = true ),
288
+ )
289
+ else -> this
290
+ } as T
291
+ } else {
292
+ this
293
+ }
294
+
173
295
private fun recordPackage (pkg : AvailablePackages .Info ) {
174
296
val currentTabIndex = tabIndex
175
297
if (currentTabIndex == null ) {
@@ -186,29 +308,39 @@ internal class StyleFactory(
186
308
val defaultTabIndex : Int? ,
187
309
)
188
310
189
- fun create (component : PaywallComponent ): Result <StyleResult , NonEmptyList <PaywallValidationError >> {
190
- val scope = StyleFactoryScope ()
191
- return scope.createInternal(component)
192
- .flatMap { componentStyle ->
193
- componentStyle?.let { Result .Success (it) }
194
- ? : Result .Error (
195
- nonEmptyListOf(PaywallValidationError .RootComponentUnsupportedProperties (component)),
311
+ /* *
312
+ * @param applyBottomWindowInsets Whether to apply bottom window insets to the root of this tree (i.e. the
313
+ * passed-in [component]).
314
+ */
315
+ fun create (
316
+ component : PaywallComponent ,
317
+ applyBottomWindowInsets : Boolean = false,
318
+ ): Result <StyleResult , NonEmptyList <PaywallValidationError >> =
319
+ with (StyleFactoryScope ()) {
320
+ createInternal(component)
321
+ .flatMap { componentStyle ->
322
+ componentStyle?.let { Result .Success (it) }
323
+ ? : Result .Error (
324
+ nonEmptyListOf(PaywallValidationError .RootComponentUnsupportedProperties (component)),
325
+ )
326
+ }
327
+ .map { componentStyle -> applyTopWindowInsetsIfNotYetApplied(to = componentStyle) }
328
+ .map { componentStyle -> componentStyle.applyBottomWindowInsetsIfNecessary(applyBottomWindowInsets) }
329
+ .map { componentStyle ->
330
+ StyleResult (
331
+ componentStyle = componentStyle,
332
+ availablePackages = packages,
333
+ defaultTabIndex = defaultTabIndex,
196
334
)
197
- }
198
- .map { componentStyle ->
199
- StyleResult (
200
- componentStyle = componentStyle,
201
- availablePackages = scope.packages,
202
- defaultTabIndex = scope.defaultTabIndex,
203
- )
204
- }
205
- }
335
+ }
336
+ }
206
337
207
338
@Suppress(" CyclomaticComplexMethod" )
208
339
private fun StyleFactoryScope.createInternal (
209
340
component : PaywallComponent ,
210
- ): Result <ComponentStyle ?, NonEmptyList <PaywallValidationError >> =
211
- when (component) {
341
+ ): Result <ComponentStyle ?, NonEmptyList <PaywallValidationError >> {
342
+ recordComponent(component)
343
+ return when (component) {
212
344
is ButtonComponent -> createButtonComponentStyleOrNull(component)
213
345
is ImageComponent -> createImageComponentStyle(component)
214
346
is PackageComponent -> createPackageComponentStyle(component)
@@ -224,6 +356,7 @@ internal class StyleFactory(
224
356
is TabControlComponent -> tabControl.errorIfNull(nonEmptyListOf(PaywallValidationError .TabControlNotInTab ))
225
357
is TabsComponent -> createTabsComponentStyle(component)
226
358
}
359
+ }
227
360
228
361
private fun StyleFactoryScope.createStickyFooterComponentStyle (
229
362
component : StickyFooterComponent ,
@@ -382,6 +515,7 @@ internal class StyleFactory(
382
515
rcPackage = rcPackage,
383
516
tabIndex = tabControlIndex,
384
517
overrides = presentedOverrides,
518
+ applyTopWindowInsets = applyTopWindowInsets,
385
519
)
386
520
}
387
521
@@ -462,6 +596,7 @@ internal class StyleFactory(
462
596
rcPackage = rcPackage,
463
597
tabIndex = tabControlIndex,
464
598
overrides = presentedOverrides,
599
+ ignoreTopWindowInsets = ignoreTopWindowInsets,
465
600
)
466
601
}
467
602
0 commit comments