Skip to content

Commit 9859294

Browse files
committed
Merge branch 'main' into fallback-api-host
2 parents 1b98352 + be4ef49 commit 9859294

File tree

11 files changed

+169
-161
lines changed

11 files changed

+169
-161
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ jobs:
301301
command: sudo pip install awscli
302302
- run:
303303
name: Deploy to S3
304-
command: aws s3 sync ~/project/docs/8.17.0 s3://purchases-docs/android/8.17.0 --delete
304+
command: aws s3 sync ~/project/docs/8.18.0-SNAPSHOT s3://purchases-docs/android/8.18.0-SNAPSHOT --delete
305305
- run:
306306
name: Update index.html
307307
command: aws s3 cp ~/project/docs/index.html s3://purchases-docs/android/index.html

.version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.17.0
1+
8.18.0-SNAPSHOT

examples/CustomEntitlementComputationSample/gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
agp = "8.1.3"
33
androidxNavigation = "2.5.3"
44
kotlin = "1.7.20"
5-
purchases = "8.17.0"
5+
purchases = "8.18.0-SNAPSHOT"
66
lifecycle = "2.5.0"
77
androidxCore = "1.10.1"
88

examples/MagicWeather/gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
agp = "8.1.3"
33
androidxNavigation = "2.6.0"
44
kotlin = "1.9.0"
5-
purchases = "8.17.0"
5+
purchases = "8.18.0-SNAPSHOT"
66
lifecycle = "2.6.1"
77
androidxCore = "1.10.1"
88

examples/MagicWeatherCompose/gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
agp = "8.1.3"
33
androidxNavigation = "2.5.3"
44
kotlin = "1.8.22"
5-
purchases = "8.17.0"
5+
purchases = "8.18.0-SNAPSHOT"
66
lifecycle = "2.5.0"
77
androidxCore = "1.10.1"
88

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#Fri Mar 31 10:31:20 PDT 2023
1414
GROUP=com.revenuecat.purchases
1515

16-
VERSION_NAME=8.17.0
16+
VERSION_NAME=8.18.0-SNAPSHOT
1717

1818
POM_DESCRIPTION=Mobile subscriptions in hours, not months.
1919
POM_URL=https://github.com/RevenueCat/purchases-android

library.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ android {
55
minSdkVersion obtainMinSdkVersion()
66
targetSdkVersion compileVersion
77
versionCode 1
8-
versionName "8.17.0"
8+
versionName "8.18.0-SNAPSHOT"
99
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1010
consumerProguardFiles "consumer-rules.pro"
1111
}

purchases/src/main/kotlin/com/revenuecat/purchases/common/Config.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ import com.revenuecat.purchases.api.BuildConfig
66
internal object Config {
77
var logLevel = LogLevel.debugLogsEnabled(BuildConfig.DEBUG)
88

9-
const val frameworkVersion = "8.17.0"
9+
const val frameworkVersion = "8.18.0-SNAPSHOT"
1010
}

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/image/ImageComponentView.kt

+2
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ private fun ImageComponentView_Bigger_Container_Fit_Fill_FitModeFill_Preview() {
275275
}
276276
}
277277

278+
@EmergeSnapshotConfig(precision = 0.99f)
278279
@Preview
279280
@Composable
280281
private fun ImageComponentView_Preview_SmallerContainer() {
@@ -391,6 +392,7 @@ private fun ImageComponentView_Preview_LinearGradient() {
391392
}
392393

393394
@Suppress("MagicNumber")
395+
@EmergeSnapshotConfig(precision = 0.99f)
394396
@Preview
395397
@Composable
396398
private fun ImageComponentView_Preview_RadialGradient() {

ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/style/StyleFactory.kt

+153-18
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import com.revenuecat.purchases.paywalls.components.common.LocaleId
2525
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
2626
import com.revenuecat.purchases.paywalls.components.common.VariableLocalizationKey
2727
import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
28+
import com.revenuecat.purchases.paywalls.components.properties.Dimension
2829
import com.revenuecat.purchases.paywalls.components.properties.Shape
30+
import com.revenuecat.purchases.paywalls.components.properties.SizeConstraint
2931
import com.revenuecat.purchases.paywalls.components.properties.ThemeImageUrls
3032
import com.revenuecat.purchases.ui.revenuecatui.components.LocalizedTextPartial
3133
import com.revenuecat.purchases.ui.revenuecatui.components.PresentedCarouselPartial
@@ -102,6 +104,91 @@ internal class StyleFactory(
102104
*/
103105
var tabIndex: Int? = null,
104106
) {
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+
105192
var defaultTabIndex: Int? = null
106193
val rcPackage: Package?
107194
get() = packageInfo?.pkg
@@ -170,6 +257,41 @@ internal class StyleFactory(
170257
return result
171258
}
172259

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+
173295
private fun recordPackage(pkg: AvailablePackages.Info) {
174296
val currentTabIndex = tabIndex
175297
if (currentTabIndex == null) {
@@ -186,29 +308,39 @@ internal class StyleFactory(
186308
val defaultTabIndex: Int?,
187309
)
188310

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,
196334
)
197-
}
198-
.map { componentStyle ->
199-
StyleResult(
200-
componentStyle = componentStyle,
201-
availablePackages = scope.packages,
202-
defaultTabIndex = scope.defaultTabIndex,
203-
)
204-
}
205-
}
335+
}
336+
}
206337

207338
@Suppress("CyclomaticComplexMethod")
208339
private fun StyleFactoryScope.createInternal(
209340
component: PaywallComponent,
210-
): Result<ComponentStyle?, NonEmptyList<PaywallValidationError>> =
211-
when (component) {
341+
): Result<ComponentStyle?, NonEmptyList<PaywallValidationError>> {
342+
recordComponent(component)
343+
return when (component) {
212344
is ButtonComponent -> createButtonComponentStyleOrNull(component)
213345
is ImageComponent -> createImageComponentStyle(component)
214346
is PackageComponent -> createPackageComponentStyle(component)
@@ -224,6 +356,7 @@ internal class StyleFactory(
224356
is TabControlComponent -> tabControl.errorIfNull(nonEmptyListOf(PaywallValidationError.TabControlNotInTab))
225357
is TabsComponent -> createTabsComponentStyle(component)
226358
}
359+
}
227360

228361
private fun StyleFactoryScope.createStickyFooterComponentStyle(
229362
component: StickyFooterComponent,
@@ -382,6 +515,7 @@ internal class StyleFactory(
382515
rcPackage = rcPackage,
383516
tabIndex = tabControlIndex,
384517
overrides = presentedOverrides,
518+
applyTopWindowInsets = applyTopWindowInsets,
385519
)
386520
}
387521

@@ -462,6 +596,7 @@ internal class StyleFactory(
462596
rcPackage = rcPackage,
463597
tabIndex = tabControlIndex,
464598
overrides = presentedOverrides,
599+
ignoreTopWindowInsets = ignoreTopWindowInsets,
465600
)
466601
}
467602

0 commit comments

Comments
 (0)