├── adapty-ui ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── attrs.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── adapty │ │ │ └── ui │ │ │ ├── internal │ │ │ ├── utils │ │ │ │ ├── ProductLoadingFailureCallback.kt │ │ │ │ ├── ContentWrapper.kt │ │ │ │ ├── consts.kt │ │ │ │ ├── EventCallback.kt │ │ │ │ ├── utils.kt │ │ │ │ ├── bitmap.kt │ │ │ │ └── InsetWrapper.kt │ │ │ ├── ui │ │ │ │ ├── attributes │ │ │ │ │ ├── Offset.kt │ │ │ │ │ ├── DimSpec.kt │ │ │ │ │ ├── AspectRatio.kt │ │ │ │ │ ├── TextAlign.kt │ │ │ │ │ ├── Pager.kt │ │ │ │ │ ├── Transition.kt │ │ │ │ │ ├── EdgeEntities.kt │ │ │ │ │ ├── DimUnit.kt │ │ │ │ │ ├── Align.kt │ │ │ │ │ └── Shape.kt │ │ │ │ ├── Indication.kt │ │ │ │ ├── element │ │ │ │ │ ├── SpaceElement.kt │ │ │ │ │ ├── BoxWithoutContentElement.kt │ │ │ │ │ ├── TextElement.kt │ │ │ │ │ ├── ZStackElement.kt │ │ │ │ │ ├── BoxElement.kt │ │ │ │ │ ├── UIElement.kt │ │ │ │ │ ├── ColumnElement.kt │ │ │ │ │ ├── RowElement.kt │ │ │ │ │ ├── HStackElement.kt │ │ │ │ │ ├── VStackElement.kt │ │ │ │ │ ├── ToggleElement.kt │ │ │ │ │ ├── ImageElement.kt │ │ │ │ │ ├── ButtonElement.kt │ │ │ │ │ ├── GridItem.kt │ │ │ │ │ └── SectionElement.kt │ │ │ │ ├── BottomSheet.kt │ │ │ │ ├── Loading.kt │ │ │ │ └── Shapes.kt │ │ │ ├── cache │ │ │ │ ├── MediaCacheConfigManager.kt │ │ │ │ ├── SingleMediaHandlerFactory.kt │ │ │ │ ├── CacheFileManager.kt │ │ │ │ ├── MediaFetchService.kt │ │ │ │ ├── MediaSaver.kt │ │ │ │ ├── MediaDownloader.kt │ │ │ │ ├── CacheCleanupService.kt │ │ │ │ └── SingleMediaHandler.kt │ │ │ ├── text │ │ │ │ ├── TimerSegment.kt │ │ │ │ ├── StringId.kt │ │ │ │ ├── TypefaceHolder.kt │ │ │ │ ├── PriceConverter.kt │ │ │ │ ├── StringWrapper.kt │ │ │ │ └── ComposeTextAttrs.kt │ │ │ └── mapping │ │ │ │ ├── attributes │ │ │ │ ├── TextAttributeMapper.kt │ │ │ │ ├── PagerAttributeMapper.kt │ │ │ │ └── InteractiveAttributeMapper.kt │ │ │ │ ├── element │ │ │ │ ├── SpaceElementMapper.kt │ │ │ │ ├── ReferenceElementMapper.kt │ │ │ │ ├── IfElementMapper.kt │ │ │ │ ├── ImageElementMapper.kt │ │ │ │ ├── RowElementMapper.kt │ │ │ │ ├── ColumnElementMapper.kt │ │ │ │ ├── BoxElementMapper.kt │ │ │ │ ├── ZStackElementMapper.kt │ │ │ │ ├── HStackElementMapper.kt │ │ │ │ ├── VStackElementMapper.kt │ │ │ │ ├── SectionElementMapper.kt │ │ │ │ ├── PagerElementMapper.kt │ │ │ │ ├── ButtonElementMapper.kt │ │ │ │ ├── TextElementMapper.kt │ │ │ │ ├── ToggleElementMapper.kt │ │ │ │ ├── UIElementMapper.kt │ │ │ │ └── BaseUIElementMapper.kt │ │ │ │ └── viewconfig │ │ │ │ ├── ViewConfigurationTextMapper.kt │ │ │ │ └── ViewConfigurationMapper.kt │ │ │ ├── listeners │ │ │ ├── AdaptyUiTimerResolver.kt │ │ │ ├── AdaptyUiTagResolver.kt │ │ │ ├── AdaptyUiPersonalizedOfferResolver.kt │ │ │ ├── AdaptyUiObserverModeHandler.kt │ │ │ └── AdaptyUiDefaultEventListener.kt │ │ │ ├── AdaptyPaywallInsets.kt │ │ │ └── AdaptyPaywallScreen.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── adapty │ │ │ └── ui │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── adapty │ │ └── ui │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── integer.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── anim │ │ │ │ ├── slide_down.xml │ │ │ │ └── slide_up.xml │ │ │ ├── layout │ │ │ │ └── fragment_paywall_ui.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-v27 │ │ │ │ └── styles.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── adaptyui │ │ │ │ └── example │ │ │ │ ├── App.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── Extensions.kt │ │ │ │ └── PaywallUiFragment.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── adaptyui │ │ │ └── example │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── adaptyui │ │ └── example │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── .gitignore ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── LICENSE ├── gradle.properties ├── README.md └── gradlew /adapty-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /adapty-ui/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .gradle/ 3 | gradlew.bat 4 | local.properties 5 | build/ 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':adapty-ui' 2 | rootProject.name='AdaptyUIExample' 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 300 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AdaptyUIExample 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyteam/AdaptyUI-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /adapty-ui/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /adapty-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_down.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_paywall_ui.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/ProductLoadingFailureCallback.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.utils 2 | 3 | import com.adapty.errors.AdaptyError 4 | 5 | internal fun interface ProductLoadingFailureCallback { 6 | fun onLoadingProductsFailure(error: AdaptyError): Boolean 7 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/Offset.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.attributes 2 | 3 | import androidx.compose.runtime.Stable 4 | 5 | @Stable 6 | internal data class Offset( 7 | val y: Float, 8 | val x: Float, 9 | ) { 10 | constructor(value: Float): this(value, value) 11 | 12 | @Transient 13 | var consumed: Boolean = false 14 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/MediaCacheConfigManager.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.cache 2 | 3 | import androidx.annotation.RestrictTo 4 | import com.adapty.ui.AdaptyUI 5 | 6 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 7 | internal class MediaCacheConfigManager { 8 | 9 | @Volatile 10 | var currentCacheConfig = AdaptyUI.MediaCacheConfiguration.Builder().build() 11 | } -------------------------------------------------------------------------------- /adapty-ui/src/test/java/com/adapty/ui/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/test/java/com/adaptyui/example/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/TimerSegment.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.text 2 | 3 | internal enum class TimerSegment(val strValue: String) { 4 | DAYS("d"), 5 | HOURS("h"), 6 | MINUTES("m"), 7 | SECONDS("s"), 8 | DECISECONDS("ds"), 9 | CENTISECONDS("cs"), 10 | MILLISECONDS("ms"), 11 | UNKNOWN(""); 12 | 13 | companion object { 14 | fun from(string: String) = values().firstOrNull { it.strValue == string } ?: UNKNOWN 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/adaptyui/example/App.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import android.app.Application 4 | import com.adapty.Adapty 5 | import com.adapty.models.AdaptyConfig 6 | import com.adapty.utils.AdaptyLogLevel 7 | 8 | class App : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | Adapty.logLevel = if (BuildConfig.DEBUG) AdaptyLogLevel.VERBOSE else AdaptyLogLevel.NONE 14 | Adapty.activate( 15 | this, 16 | AdaptyConfig.Builder("YOUR_ADAPTY_KEY").build(), 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/adaptyui/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | class MainActivity : AppCompatActivity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | 11 | if (savedInstanceState == null) { 12 | supportFragmentManager 13 | .beginTransaction() 14 | .add(android.R.id.content, MainFragment.newInstance()) 15 | .commit() 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/ContentWrapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.utils 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.ui.attributes.Align 7 | import com.adapty.ui.internal.ui.attributes.Offset 8 | import com.adapty.ui.internal.ui.attributes.Shape 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | 11 | internal class ContentWrapper( 12 | val content: UIElement, 13 | val contentAlign: Align, 14 | val background: Shape?, 15 | val offset: Offset?, 16 | ) -------------------------------------------------------------------------------- /app/src/main/res/values-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/adaptyui/example/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import android.view.View 4 | import androidx.core.graphics.Insets 5 | import androidx.core.view.ViewCompat 6 | import androidx.core.view.WindowInsetsCompat 7 | 8 | fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { 9 | ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> 10 | val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 11 | 12 | ViewCompat.setOnApplyWindowInsetsListener(this, null) 13 | action(systemBarInsets) 14 | insets 15 | } 16 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/attributes/TextAttributeMapper.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.mapping.attributes 2 | 3 | import com.adapty.ui.internal.ui.attributes.TextAlign 4 | 5 | internal class TextAttributeMapper { 6 | fun mapTextAlign(item: Any?, default: TextAlign = TextAlign.START): TextAlign { 7 | return when (item) { 8 | "leading" -> TextAlign.START 9 | "left" -> TextAlign.LEFT 10 | "trailing" -> TextAlign.END 11 | "right" -> TextAlign.RIGHT 12 | "center" -> TextAlign.CENTER 13 | "justified" -> TextAlign.JUSTIFY 14 | else -> default 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/DimSpec.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.attributes 2 | 3 | import com.adapty.internal.utils.InternalAdaptyApi 4 | 5 | @InternalAdaptyApi 6 | public sealed class DimSpec(internal val axis: Axis) { 7 | internal enum class Axis { X, Y } 8 | 9 | public class Min internal constructor(internal val value: DimUnit, axis: Axis): DimSpec(axis) 10 | public class FillMax internal constructor(axis: Axis): DimSpec(axis) 11 | public class Specified internal constructor(internal val value: DimUnit, axis: Axis): DimSpec(axis) 12 | public class Shrink internal constructor(internal val min: DimUnit, axis: Axis): DimSpec(axis) 13 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/AspectRatio.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.attributes 2 | 3 | import androidx.compose.ui.Alignment 4 | import androidx.compose.ui.layout.ContentScale 5 | 6 | internal enum class AspectRatio { 7 | FIT, FILL, STRETCH 8 | } 9 | 10 | internal fun AspectRatio.toComposeContentScale() = 11 | when (this) { 12 | AspectRatio.STRETCH -> ContentScale.FillBounds 13 | AspectRatio.FILL -> ContentScale.Crop 14 | else -> ContentScale.Fit 15 | } 16 | 17 | internal fun AspectRatio.evaluateComposeImageAlignment() = 18 | when (this) { 19 | AspectRatio.FILL -> Alignment.TopCenter 20 | else -> Alignment.Center 21 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/Indication.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui 2 | 3 | import androidx.compose.foundation.Indication 4 | import androidx.compose.material.ripple.rememberRipple 5 | import androidx.compose.material3.ripple 6 | import androidx.compose.runtime.Composable 7 | import com.adapty.ui.internal.utils.LOG_PREFIX 8 | import com.adapty.ui.internal.utils.log 9 | import com.adapty.utils.AdaptyLogLevel.Companion.ERROR 10 | 11 | @Suppress("DEPRECATION_ERROR") 12 | @Composable 13 | internal fun clickIndication(): Indication = runCatching { ripple() }.getOrElse { e -> 14 | log(ERROR) { "$LOG_PREFIX Switching to fallback indication (${e.localizedMessage})" } 15 | rememberRipple() 16 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/consts.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.utils 2 | 3 | internal const val LOADING_BG_COLOR = 0x80000000 4 | internal const val LOADING_SIZE = 64 5 | internal const val LOADING_PRODUCTS_RETRY_DELAY = 2000L 6 | internal const val DEFAULT_PRODUCT_GROUP = "group_A" 7 | internal const val OPENED_ADDITIONAL_SCREEN_KEY = "opened_additional_screen" 8 | internal const val NO_SHRINK = 0b0 9 | internal const val HOUR_MILLIS = 3600 * 1000L 10 | internal const val LOG_PREFIX = "UI v${com.adapty.ui.BuildConfig.VERSION_NAME}:" 11 | internal const val LOG_PREFIX_ERROR = "UI v${com.adapty.ui.BuildConfig.VERSION_NAME} error:" 12 | internal const val CONFIGURATION_FORMAT_VERSION = "4.0.0" 13 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/TextAlign.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.attributes 2 | 3 | public enum class TextAlign { 4 | CENTER, 5 | JUSTIFY, 6 | START, 7 | LEFT, 8 | END, 9 | RIGHT, 10 | } 11 | 12 | internal fun TextAlign.toComposeTextAlign() = 13 | when(this) { 14 | TextAlign.CENTER -> androidx.compose.ui.text.style.TextAlign.Center 15 | TextAlign.JUSTIFY -> androidx.compose.ui.text.style.TextAlign.Justify 16 | TextAlign.START -> androidx.compose.ui.text.style.TextAlign.Start 17 | TextAlign.END -> androidx.compose.ui.text.style.TextAlign.End 18 | TextAlign.LEFT -> androidx.compose.ui.text.style.TextAlign.Left 19 | TextAlign.RIGHT -> androidx.compose.ui.text.style.TextAlign.Right 20 | } -------------------------------------------------------------------------------- /adapty-ui/src/androidTest/java/com/adapty/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.adapty.ui.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/adaptyui/example/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.adapty.example", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/listeners/AdaptyUiTimerResolver.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.listeners 2 | 3 | import com.adapty.ui.internal.utils.HOUR_MILLIS 4 | import java.util.Date 5 | 6 | /** 7 | * Implement this interface to to use custom timer functionality 8 | */ 9 | public fun interface AdaptyUiTimerResolver { 10 | /** 11 | * A function that returns the date the timer with [timerId] should end at. 12 | * 13 | * @param[timerId] ID of the timer. 14 | * 15 | * @return The date the timer with [timerId] should end at. 16 | */ 17 | public fun timerEndAtDate(timerId: String): Date 18 | 19 | public companion object { 20 | @JvmField 21 | public val DEFAULT: AdaptyUiTimerResolver = 22 | AdaptyUiTimerResolver { _ -> Date(System.currentTimeMillis() + HOUR_MILLIS) } 23 | } 24 | } -------------------------------------------------------------------------------- /adapty-ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/listeners/AdaptyUiTagResolver.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.listeners 2 | 3 | /** 4 | * Implement this interface to specify the string values with which custom tags should be replaced 5 | */ 6 | public fun interface AdaptyUiTagResolver { 7 | /** 8 | * A function that maps a custom tag to the string value it should be replaced with. 9 | * If `null` is returned, the tag will not be replaced. 10 | * 11 | * @param[tag] The custom tag to be replaced. 12 | * 13 | * @return The value the [tag] should be replaced with, or `null` if there is no mapping for the [tag]. 14 | */ 15 | public fun replacement(tag: String): String? 16 | 17 | public companion object { 18 | /** 19 | * The default implementation that has no replacements. 20 | */ 21 | @JvmField 22 | public val DEFAULT: AdaptyUiTagResolver = 23 | AdaptyUiTagResolver { _ -> null } 24 | } 25 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/SpaceElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.element.BaseProps 8 | import com.adapty.ui.internal.ui.element.SpaceElement 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | 11 | internal class SpaceElementMapper( 12 | commonAttributeMapper: CommonAttributeMapper, 13 | ) : BaseUIElementMapper("space", commonAttributeMapper), UIPlainElementMapper { 14 | 15 | override fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement { 16 | return SpaceElement(BaseProps(weight = config["count"]?.toFloatOrNull() ?: 1f)) 17 | .also { element -> 18 | addToReferenceTargetsIfNeeded(config, element, refBundles) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/SpaceElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.ui.internal.mapping.element.Assets 8 | import com.adapty.ui.internal.text.StringId 9 | import com.adapty.ui.internal.text.StringWrapper 10 | import com.adapty.ui.internal.utils.EventCallback 11 | 12 | @InternalAdaptyApi 13 | public class SpaceElement internal constructor( 14 | override val baseProps: BaseProps, 15 | ) : UIElement { 16 | 17 | override fun toComposable( 18 | resolveAssets: () -> Assets, 19 | resolveText: @Composable (StringId) -> StringWrapper?, 20 | resolveState: () -> Map, 21 | eventCallback: EventCallback, 22 | modifier: Modifier, 23 | ): @Composable () -> Unit = { 24 | Spacer(modifier = modifier) 25 | } 26 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/SingleMediaHandlerFactory.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.cache 2 | 3 | import androidx.annotation.RestrictTo 4 | import java.util.concurrent.Executors 5 | 6 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 7 | internal class SingleMediaHandlerFactory( 8 | private val mediaDownloader: MediaDownloader, 9 | private val mediaSaver: MediaSaver, 10 | private val cacheFileManager: CacheFileManager, 11 | private val cacheCleanupService: CacheCleanupService, 12 | ) { 13 | 14 | private val executor = Executors.newCachedThreadPool() 15 | 16 | private val handlers = hashMapOf() 17 | 18 | fun get(mediaKey: String): SingleMediaHandler = 19 | handlers.getOrPut(mediaKey) { 20 | SingleMediaHandler( 21 | mediaDownloader, 22 | mediaSaver, 23 | cacheFileManager, 24 | cacheCleanupService, 25 | executor, 26 | mediaKey, 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adapty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ReferenceElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.ui.element.ReferenceElement 10 | import com.adapty.ui.internal.ui.element.UIElement 11 | 12 | internal class ReferenceElementMapper( 13 | commonAttributeMapper: CommonAttributeMapper, 14 | ) : BaseUIElementMapper("reference", commonAttributeMapper), UIPlainElementMapper { 15 | override fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement { 16 | return ReferenceElement( 17 | (config["element_id"] as? String)?.takeIf { it.isNotEmpty() } 18 | ?: throw adaptyError( 19 | message = "element_id in Reference must not be empty", 20 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 21 | ), 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/Pager.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | 7 | internal class PagerAnimation( 8 | internal val startDelayMillis: Long, 9 | internal val afterInteractionDelayMillis: Long, 10 | internal val pageTransition: Transition.Slide, 11 | internal val repeatTransition: Transition.Slide?, 12 | ) 13 | 14 | internal enum class InteractionBehavior { 15 | NONE, CANCEL_ANIMATION, PAUSE_ANIMATION 16 | } 17 | 18 | internal class PagerIndicator( 19 | val layout: Layout, 20 | val vAlign: VerticalAlign, 21 | val padding: EdgeEntities, 22 | val dotSize: Float, 23 | val spacing: Float, 24 | val color: Shape.Fill?, 25 | val selectedColor: Shape.Fill?, 26 | ) { 27 | enum class Layout { 28 | STACKED, OVERLAID 29 | } 30 | } 31 | 32 | @InternalAdaptyApi 33 | public sealed class PageSize { 34 | public class Unit internal constructor(internal val value: DimUnit): PageSize() 35 | public class PageFraction internal constructor(internal val fraction: Float): PageSize() 36 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/listeners/AdaptyUiPersonalizedOfferResolver.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.listeners 2 | 3 | import com.adapty.models.AdaptyPaywallProduct 4 | 5 | /** 6 | * Implement this interface to indicate whether the price is personalized, [read more](https://developer.android.com/google/play/billing/integrate#personalized-price). 7 | */ 8 | public fun interface AdaptyUiPersonalizedOfferResolver { 9 | /** 10 | * Function that maps a product to a boolean value that indicates whether the price is personalized, [read more](https://developer.android.com/google/play/billing/integrate#personalized-price). 11 | * 12 | * @param[product] An [AdaptyPaywallProduct] to be purchased. 13 | * 14 | * @return `true`, if the price of the [product] is personalized, otherwise `false`. 15 | */ 16 | public fun resolve(product: AdaptyPaywallProduct): Boolean 17 | 18 | public companion object { 19 | /** 20 | * The default implementation that returns `false`. 21 | */ 22 | @JvmField 23 | public val DEFAULT: AdaptyUiPersonalizedOfferResolver = 24 | AdaptyUiPersonalizedOfferResolver { _ -> false } 25 | } 26 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 34 8 | defaultConfig { 9 | applicationId "com.adaptyui.example" 10 | minSdk 21 11 | targetSdk 31 12 | versionCode 1 13 | versionName "1.0.0" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | kotlinOptions { 26 | jvmTarget = '1.8' 27 | } 28 | namespace 'com.adaptyui.example' 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation project(':adapty-ui') 34 | implementation 'io.adapty:android-sdk:3.0.2' 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlin_version}" 36 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 37 | implementation 'androidx.appcompat:appcompat:1.4.2' 38 | testImplementation 'junit:junit:4.13.2' 39 | } 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | #android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | #android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.useAndroidX=true 23 | android.enableJetifier=true 24 | android.defaults.buildfeatures.buildconfig=true 25 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/BottomSheet.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.adapty.ui.internal.ui 4 | 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.WindowInsets 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.ModalBottomSheet 9 | import androidx.compose.material3.SheetState 10 | import androidx.compose.material3.rememberModalBottomSheetState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.RectangleShape 15 | 16 | @Composable 17 | internal fun BottomSheet( 18 | onDismissRequest: () -> Unit, 19 | modifier: Modifier = Modifier, 20 | sheetState: SheetState, 21 | content: @Composable ColumnScope.() -> Unit, 22 | ) { 23 | ModalBottomSheet( 24 | sheetState = sheetState, 25 | modifier = modifier, 26 | onDismissRequest = onDismissRequest, 27 | shape = RectangleShape, 28 | containerColor = Color.Transparent, 29 | dragHandle = null, 30 | contentWindowInsets = { WindowInsets(0,0,0,0) }, 31 | content = content, 32 | ) 33 | } 34 | 35 | @Composable 36 | internal fun rememberBottomSheetState() = rememberModalBottomSheetState(skipPartiallyExpanded = true) -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/BoxWithoutContentElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxHeight 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.internal.mapping.element.Assets 10 | import com.adapty.ui.internal.text.StringId 11 | import com.adapty.ui.internal.text.StringWrapper 12 | import com.adapty.ui.internal.ui.attributes.Align 13 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 14 | import com.adapty.ui.internal.utils.EventCallback 15 | 16 | @InternalAdaptyApi 17 | public class BoxWithoutContentElement internal constructor( 18 | internal val align: Align, 19 | override val baseProps: BaseProps, 20 | ) : UIElement { 21 | 22 | override fun toComposable( 23 | resolveAssets: () -> Assets, 24 | resolveText: @Composable (StringId) -> StringWrapper?, 25 | resolveState: () -> Map, 26 | eventCallback: EventCallback, 27 | modifier: Modifier 28 | ): @Composable () -> Unit = { 29 | Box( 30 | contentAlignment = align.toComposeAlignment(), 31 | modifier = modifier.fillMaxWidth().fillMaxHeight(), 32 | ) { } 33 | } 34 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/IfElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | import com.adapty.ui.internal.utils.CONFIGURATION_FORMAT_VERSION 11 | 12 | internal class IfElementMapper( 13 | commonAttributeMapper: CommonAttributeMapper, 14 | ) : BaseUIComplexElementMapper("if", commonAttributeMapper), UIComplexShrinkableElementMapper { 15 | override fun map( 16 | config: Map<*, *>, 17 | assets: Assets, 18 | refBundles: ReferenceBundles, 19 | stateMap: MutableMap, 20 | inheritShrink: Int, 21 | childMapper: ChildMapperShrinkable, 22 | ): UIElement { 23 | val key = when { 24 | config["platform"] == "android" || config["version"] == CONFIGURATION_FORMAT_VERSION -> "then" 25 | else -> "else" 26 | } 27 | return (config[key] as? Map<*, *>)?.let { item -> childMapper(item, inheritShrink) } 28 | ?: throw adaptyError( 29 | message = "$key in If must not be empty", 30 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/Transition.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import androidx.compose.animation.core.Easing 6 | import androidx.compose.animation.core.FastOutLinearInEasing 7 | import androidx.compose.animation.core.FastOutSlowInEasing 8 | import androidx.compose.animation.core.LinearEasing 9 | import androidx.compose.animation.core.LinearOutSlowInEasing 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | 12 | @InternalAdaptyApi 13 | public sealed class Transition( 14 | internal val durationMillis: Int, 15 | internal val startDelayMillis: Int, 16 | internal val interpolatorName: String, 17 | ) { 18 | public class Fade( 19 | durationMillis: Int, 20 | startDelayMillis: Int, 21 | interpolatorName: String, 22 | ): Transition(durationMillis, startDelayMillis, interpolatorName) 23 | 24 | public class Slide( 25 | durationMillis: Int, 26 | startDelayMillis: Int, 27 | interpolatorName: String, 28 | ): Transition(durationMillis, startDelayMillis, interpolatorName) 29 | } 30 | 31 | internal val Transition.easing: Easing 32 | get() = when (interpolatorName) { 33 | "ease_in_out" -> FastOutSlowInEasing 34 | "ease_in" -> FastOutLinearInEasing 35 | "ease_out" -> LinearOutSlowInEasing 36 | "linear" -> LinearEasing 37 | else -> FastOutSlowInEasing 38 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/TextElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.element.Assets 7 | import com.adapty.ui.internal.text.StringId 8 | import com.adapty.ui.internal.text.StringWrapper 9 | import com.adapty.ui.internal.ui.attributes.TextAlign 10 | import com.adapty.ui.internal.utils.EventCallback 11 | 12 | @InternalAdaptyApi 13 | public class TextElement internal constructor( 14 | internal val stringId: StringId, 15 | textAlign: TextAlign, 16 | internal val maxLines: Int?, 17 | internal val onOverflow: OnOverflowMode?, 18 | attributes: Attributes, 19 | baseProps: BaseProps, 20 | ) : BaseTextElement(textAlign, attributes, baseProps) { 21 | 22 | override fun toComposable( 23 | resolveAssets: () -> Assets, 24 | resolveText: @Composable (StringId) -> StringWrapper?, 25 | resolveState: () -> Map, 26 | eventCallback: EventCallback, 27 | modifier: Modifier, 28 | ): @Composable () -> Unit = { 29 | renderTextInternal( 30 | attributes, 31 | textAlign, 32 | maxLines, 33 | onOverflow, 34 | modifier, 35 | resolveAssets, 36 | ) { 37 | resolveText(stringId) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/EventCallback.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.utils 2 | 3 | import com.adapty.errors.AdaptyError 4 | import com.adapty.internal.utils.InternalAdaptyApi 5 | import com.adapty.models.AdaptyPaywallProduct 6 | import com.adapty.models.AdaptyProfile 7 | import com.adapty.models.AdaptyPurchasedInfo 8 | import com.adapty.models.AdaptySubscriptionUpdateParameters 9 | import com.adapty.ui.internal.ui.element.Action 10 | import java.util.Date 11 | 12 | @InternalAdaptyApi 13 | public interface EventCallback { 14 | public fun onRestoreStarted() 15 | public fun onRestoreSuccess(profile: AdaptyProfile) 16 | public fun onRestoreFailure(error: AdaptyError) 17 | public fun onAwaitingSubscriptionUpdateParams(product: AdaptyPaywallProduct): AdaptySubscriptionUpdateParameters? 18 | public fun onPurchaseStarted(product: AdaptyPaywallProduct) 19 | public fun onPurchaseSuccess( 20 | purchasedInfo: AdaptyPurchasedInfo?, 21 | product: AdaptyPaywallProduct, 22 | ) 23 | public fun onPurchaseCanceled(product: AdaptyPaywallProduct) 24 | public fun onPurchaseFailure( 25 | error: AdaptyError, 26 | product: AdaptyPaywallProduct, 27 | ) 28 | public fun onActions(actions: List) 29 | public fun getTimerStartTimestamp(timerId: String, isPersisted: Boolean): Long? 30 | public fun setTimerStartTimestamp(timerId: String, value: Long, isPersisted: Boolean) 31 | public fun timerEndAtDate(timerId: String): Date 32 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/EdgeEntities.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.unit.dp 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | 10 | internal data class EdgeEntities( 11 | val start: DimUnit, 12 | val top: DimUnit, 13 | val end: DimUnit, 14 | val bottom: DimUnit, 15 | ) { 16 | constructor(horizontal: DimUnit, vertical: DimUnit): this(horizontal, vertical, horizontal, vertical) 17 | constructor(all: DimUnit): this(all, all) 18 | constructor(all: Float): this(DimUnit.Exact(all)) 19 | } 20 | 21 | @Composable 22 | internal fun EdgeEntities.toPaddingValues(): PaddingValues { 23 | return PaddingValues( 24 | start.toExactDp(DimSpec.Axis.X), 25 | top.toExactDp(DimSpec.Axis.Y), 26 | end.toExactDp(DimSpec.Axis.X), 27 | bottom.toExactDp(DimSpec.Axis.Y), 28 | ) 29 | } 30 | 31 | internal val EdgeEntities.horizontalSum @Composable get() = start.toExactDp(DimSpec.Axis.X) + end.toExactDp( 32 | DimSpec.Axis.X 33 | ) 34 | internal val EdgeEntities.verticalSum @Composable get() = top.toExactDp(DimSpec.Axis.Y) + bottom.toExactDp( 35 | DimSpec.Axis.Y 36 | ) 37 | 38 | internal val EdgeEntities?.horizontalSumOrDefault @Composable get() = this?.horizontalSum ?: 0.dp 39 | internal val EdgeEntities?.verticalSumOrDefault @Composable get() = this?.verticalSum ?: 0.dp -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ImageElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.ui.attributes.Shape 10 | import com.adapty.ui.internal.ui.element.ImageElement 11 | import com.adapty.ui.internal.ui.element.UIElement 12 | 13 | internal class ImageElementMapper( 14 | commonAttributeMapper: CommonAttributeMapper, 15 | ) : BaseUIElementMapper("image", commonAttributeMapper), UIPlainElementMapper { 16 | override fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement { 17 | val assetId = (config["asset_id"] as? String)?.takeIf { it.isNotEmpty() } 18 | ?: throw adaptyError( 19 | message = "asset_id in Image must not be empty", 20 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 21 | ) 22 | checkAsset(assetId, assets) 23 | return ImageElement( 24 | assetId, 25 | commonAttributeMapper.mapAspectRatio(config["aspect"]), 26 | (config["tint"] as? String)?.let { assetId -> 27 | checkAsset(assetId, assets) 28 | Shape.Fill(assetId) 29 | }, 30 | config.extractBaseProps(), 31 | ) 32 | .also { element -> 33 | addToReferenceTargetsIfNeeded(config, element, refBundles) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/StringId.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.text 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.utils.DEFAULT_PRODUCT_GROUP 9 | 10 | @InternalAdaptyApi 11 | public sealed class StringId { 12 | public class Str internal constructor(internal val value: String): StringId() 13 | public class Product internal constructor( 14 | internal val productId: String?, 15 | internal val productGroupId: String, 16 | internal val suffix: String?, 17 | ): StringId() 18 | } 19 | 20 | internal fun Any.toStringId(): StringId? { 21 | return (this as? String)?.takeIf { it.isNotEmpty() } 22 | ?.let { value -> StringId.Str(value) } 23 | ?: run { 24 | (this as? Map<*, *>)?.let { stringIdObject -> 25 | val type = stringIdObject["type"] 26 | if (type != "product") 27 | throw adaptyError( 28 | message = "Unsupported type in string_id in Text ($type)", 29 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 30 | ) 31 | val productId = stringIdObject["id"] as? String 32 | val productGroup = (stringIdObject["group_id"] as? String)?.takeIf { it.isNotEmpty() } 33 | ?: DEFAULT_PRODUCT_GROUP 34 | val suffix = stringIdObject["suffix"] as? String 35 | StringId.Product(productId, productGroup, suffix) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/CacheFileManager.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.cache 4 | 5 | import android.content.Context 6 | import android.system.Os 7 | import androidx.annotation.RestrictTo 8 | import com.adapty.internal.utils.HashingHelper 9 | import com.adapty.internal.utils.InternalAdaptyApi 10 | import com.adapty.utils.TimeInterval 11 | import java.io.File 12 | 13 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 14 | internal class CacheFileManager( 15 | private val context: Context, 16 | private val hashingHelper: HashingHelper, 17 | ) { 18 | 19 | fun getFile(url: String, isTemp: Boolean = false): File { 20 | val fileName = hashingHelper.md5(url) 21 | val file = File(context.cacheDir, "/$CACHE_FOLDER/${if (isTemp) "." else ""}$fileName") 22 | return file.also { it.parentFile?.mkdirs() } 23 | } 24 | 25 | fun getDir(): File { 26 | return File(context.cacheDir, "/$CACHE_FOLDER").also { it.mkdir() } 27 | } 28 | 29 | fun isTemp(file: File) = file.name.startsWith(".") 30 | 31 | fun getSize(file: File) = runCatching { Os.lstat(file.absolutePath).st_size }.getOrDefault(0L) 32 | 33 | fun isOlderThan(age: TimeInterval, file: File): Boolean { 34 | val currentMillis = System.currentTimeMillis() 35 | val lastModifiedAt = getLastModifiedAt(file) 36 | 37 | return (currentMillis - lastModifiedAt) > age.duration.inWholeMilliseconds 38 | } 39 | 40 | private fun getLastModifiedAt(file: File) = 41 | runCatching { Os.lstat(file.absolutePath).st_mtime }.getOrDefault(0L) 42 | 43 | private companion object { 44 | const val CACHE_FOLDER = "AdaptyUI" 45 | } 46 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/ZStackElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.ui.internal.mapping.element.Assets 8 | import com.adapty.ui.internal.text.StringId 9 | import com.adapty.ui.internal.text.StringWrapper 10 | import com.adapty.ui.internal.ui.attributes.Align 11 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 12 | import com.adapty.ui.internal.ui.fillWithBaseParams 13 | import com.adapty.ui.internal.utils.EventCallback 14 | 15 | @InternalAdaptyApi 16 | public class ZStackElement internal constructor( 17 | override var content: List, 18 | internal val align: Align, 19 | override val baseProps: BaseProps, 20 | ) : UIElement, MultiContainer { 21 | 22 | override fun toComposable( 23 | resolveAssets: () -> Assets, 24 | resolveText: @Composable (StringId) -> StringWrapper?, 25 | resolveState: () -> Map, 26 | eventCallback: EventCallback, 27 | modifier: Modifier, 28 | ): @Composable () -> Unit = { 29 | Box( 30 | contentAlignment = align.toComposeAlignment(), 31 | modifier = modifier, 32 | ) { 33 | content.forEach { item -> 34 | item.toComposable( 35 | resolveAssets, 36 | resolveText, 37 | resolveState, 38 | eventCallback, 39 | Modifier.fillWithBaseParams(item, resolveAssets) 40 | ).invoke() 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/MediaFetchService.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.cache 4 | 5 | import androidx.annotation.RestrictTo 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.ui.AdaptyUI 8 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset.Image 9 | import com.adapty.ui.internal.utils.LOG_PREFIX 10 | import com.adapty.ui.internal.utils.log 11 | import com.adapty.utils.AdaptyLogLevel.Companion.VERBOSE 12 | 13 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 14 | internal class MediaFetchService( 15 | private val singleMediaHandlerFactory: SingleMediaHandlerFactory, 16 | ) { 17 | 18 | fun preloadMedia(configId: String, urls: Collection) { 19 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# preloading media from config with id: $configId" } 20 | urls.forEach { url -> 21 | load(url) 22 | } 23 | } 24 | 25 | private fun load(url: String) { 26 | val handler = singleMediaHandlerFactory.get(url) 27 | handler.loadMedia() 28 | } 29 | 30 | fun loadImage( 31 | remoteImage: AdaptyUI.LocalizedViewConfiguration.Asset.RemoteImage, 32 | handlePreview: ((preview: Image) -> Unit)?, 33 | handleResult: ((image: Image) -> Unit)?, 34 | ) { 35 | remoteImage.preview?.let { preview -> handlePreview?.invoke(preview) } 36 | 37 | val handler = singleMediaHandlerFactory.get(remoteImage.url) 38 | 39 | handler.loadMedia( 40 | onResult = { result -> 41 | result.mapCatching { file -> 42 | handleResult?.invoke(Image(source = Image.Source.File(file))) 43 | } 44 | }, 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/TypefaceHolder.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.text 4 | 5 | import android.content.Context 6 | import android.graphics.Typeface 7 | import androidx.annotation.RestrictTo 8 | import androidx.core.content.res.ResourcesCompat 9 | import androidx.core.graphics.TypefaceCompat 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset 12 | import java.util.Locale 13 | 14 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 15 | internal object TypefaceHolder { 16 | 17 | private val typefaceCache = mutableMapOf() 18 | 19 | fun getOrPut(context: Context, font: Asset.Font): Typeface { 20 | val fontKey = "${font.resources.joinToString()}-${font.familyName}-${font.weight}-${font.isItalic}" 21 | 22 | return typefaceCache.getOrPut(fontKey) { 23 | TypefaceCompat.create( 24 | context, 25 | getFontFromResOrNull(context, font.resources) 26 | ?: Typeface.create(font.familyName.lowercase(Locale.ENGLISH), Typeface.NORMAL), 27 | font.weight, 28 | font.isItalic 29 | ) 30 | } 31 | } 32 | 33 | private fun getFontFromResOrNull(context: Context, resourceIds: Iterable): Typeface? { 34 | for (fontRes in resourceIds) { 35 | val resId = context.resources.getIdentifier(fontRes, "font", context.packageName) 36 | 37 | if (resId != 0) { 38 | val font = kotlin.runCatching { ResourcesCompat.getFont(context, resId) }.getOrNull() 39 | if (font != null) return font 40 | } 41 | } 42 | return null 43 | } 44 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/RowElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.attributes.DimSpec 8 | import com.adapty.ui.internal.ui.element.RowElement 9 | import com.adapty.ui.internal.ui.element.SkippedElement 10 | import com.adapty.ui.internal.ui.element.UIElement 11 | 12 | internal class RowElementMapper( 13 | commonAttributeMapper: CommonAttributeMapper, 14 | ) : BaseUIComplexElementMapper("row", commonAttributeMapper), UIComplexElementMapper { 15 | override fun map( 16 | config: Map<*, *>, 17 | assets: Assets, 18 | refBundles: ReferenceBundles, 19 | stateMap: MutableMap, 20 | inheritShrink: Int, 21 | childMapper: ChildMapper, 22 | ): UIElement { 23 | val referenceIds = mutableSetOf() 24 | return RowElement( 25 | content = (config["items"] as? List<*>)?.mapNotNull { item -> 26 | processContentItem((item as? Map<*, *>)?.asGridItem(DimSpec.Axis.X, refBundles, childMapper), referenceIds, refBundles.targetElements) 27 | } 28 | ?.takeIf { it.isNotEmpty() } 29 | ?: return SkippedElement, 30 | spacing = config.extractSpacingOrNull(), 31 | baseProps = config.extractBaseProps(), 32 | ) 33 | .also { container -> 34 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 35 | addToReferenceTargetsIfNeeded(config, container, refBundles) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ColumnElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.attributes.DimSpec 8 | import com.adapty.ui.internal.ui.element.ColumnElement 9 | import com.adapty.ui.internal.ui.element.SkippedElement 10 | import com.adapty.ui.internal.ui.element.UIElement 11 | 12 | internal class ColumnElementMapper( 13 | commonAttributeMapper: CommonAttributeMapper, 14 | ) : BaseUIComplexElementMapper("column", commonAttributeMapper), UIComplexElementMapper { 15 | override fun map( 16 | config: Map<*, *>, 17 | assets: Assets, 18 | refBundles: ReferenceBundles, 19 | stateMap: MutableMap, 20 | inheritShrink: Int, 21 | childMapper: ChildMapper, 22 | ): UIElement { 23 | val referenceIds = mutableSetOf() 24 | return ColumnElement( 25 | content = (config["items"] as? List<*>)?.mapNotNull { item -> 26 | processContentItem((item as? Map<*, *>)?.asGridItem(DimSpec.Axis.Y, refBundles, childMapper), referenceIds, refBundles.targetElements) 27 | } 28 | ?.takeIf { it.isNotEmpty() } 29 | ?: return SkippedElement, 30 | spacing = config.extractSpacingOrNull(), 31 | baseProps = config.extractBaseProps(), 32 | ) 33 | .also { container -> 34 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 35 | addToReferenceTargetsIfNeeded(config, container, refBundles) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/BoxElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.element.BoxElement 8 | import com.adapty.ui.internal.ui.element.BoxWithoutContentElement 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | 11 | internal class BoxElementMapper( 12 | commonAttributeMapper: CommonAttributeMapper, 13 | ) : BaseUIComplexElementMapper("box", commonAttributeMapper), UIComplexShrinkableElementMapper { 14 | override fun map( 15 | config: Map<*, *>, 16 | assets: Assets, 17 | refBundles: ReferenceBundles, 18 | stateMap: MutableMap, 19 | inheritShrink: Int, 20 | childMapper: ChildMapperShrinkable, 21 | ): UIElement { 22 | val referenceIds = mutableSetOf() 23 | val (baseProps, nextInheritShrink) = extractBasePropsWithShrinkInheritance(config, inheritShrink) 24 | val content = processContentItem( 25 | (config["content"] as? Map<*, *>)?.let { content -> childMapper(content, nextInheritShrink) }, 26 | referenceIds, 27 | refBundles.targetElements 28 | ) 29 | val align = commonAttributeMapper.mapAlign(config) 30 | if (content == null) 31 | return BoxWithoutContentElement(align, baseProps) 32 | return BoxElement(content, align, baseProps) 33 | .also { container -> 34 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 35 | addToReferenceTargetsIfNeeded(config, container, refBundles) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/Loading.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui 2 | 3 | import android.util.TypedValue 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.ProgressIndicatorDefaults 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.unit.dp 18 | import com.adapty.ui.R 19 | import com.adapty.ui.internal.utils.LOADING_BG_COLOR 20 | import com.adapty.ui.internal.utils.LOADING_SIZE 21 | 22 | @Composable 23 | internal fun Loading(modifier: Modifier = Modifier) { 24 | Box( 25 | contentAlignment = Alignment.Center, 26 | modifier = modifier 27 | .fillMaxSize() 28 | .clickable(enabled = false, onClick = {}) 29 | .background(Color(LOADING_BG_COLOR)) 30 | ) { 31 | val context = LocalContext.current 32 | 33 | val circularColor = remember { 34 | val typedValue = TypedValue() 35 | if (context.theme.resolveAttribute(R.attr.adapty_progressIndicatorColor, typedValue, true)) { 36 | kotlin.runCatching { Color(typedValue.data) }.getOrNull() 37 | } else { 38 | null 39 | } 40 | } 41 | 42 | CircularProgressIndicator( 43 | modifier = Modifier.width(LOADING_SIZE.dp), 44 | color = circularColor ?: ProgressIndicatorDefaults.circularColor, 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /adapty-ui/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | minSdk 21 11 | targetSdk 31 12 | buildConfigField 'String', 'VERSION_NAME', "\"3.0.2\"" 13 | buildConfigField 'String', 'BUILDER_VERSION', "\"4_0\"" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_17 27 | targetCompatibility JavaVersion.VERSION_17 28 | } 29 | buildFeatures { 30 | compose = true 31 | } 32 | composeOptions { 33 | kotlinCompilerExtensionVersion = "1.4.1" 34 | } 35 | kotlinOptions { 36 | freeCompilerArgs += ['-Xexplicit-api=strict', '-Xopt-in=kotlin.RequiresOptIn'] 37 | freeCompilerArgs += ['-Xopt-in=kotlin.RequiresOptIn'] 38 | } 39 | namespace 'com.adapty.ui' 40 | } 41 | 42 | def composeBom = '2024.09.02' 43 | 44 | dependencies { 45 | 46 | compileOnly 'io.adapty:android-sdk:3.0.2' 47 | testImplementation 'junit:junit:4.13.2' 48 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 49 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 50 | 51 | implementation platform("androidx.compose:compose-bom:$composeBom") 52 | api 'androidx.compose.ui:ui' 53 | implementation 'androidx.compose.material3:material3' 54 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0' 55 | 56 | debugCompileOnly 'androidx.compose.ui:ui-tooling' 57 | debugCompileOnly 'androidx.compose.ui:ui-test-manifest' 58 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ZStackElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.element.SkippedElement 8 | import com.adapty.ui.internal.ui.element.UIElement 9 | import com.adapty.ui.internal.ui.element.ZStackElement 10 | 11 | internal class ZStackElementMapper( 12 | commonAttributeMapper: CommonAttributeMapper, 13 | ) : BaseUIComplexElementMapper("z_stack", commonAttributeMapper), 14 | UIComplexShrinkableElementMapper { 15 | 16 | override fun map( 17 | config: Map<*, *>, 18 | assets: Assets, 19 | refBundles: ReferenceBundles, 20 | stateMap: MutableMap, 21 | inheritShrink: Int, 22 | childMapper: ChildMapperShrinkable, 23 | ): UIElement { 24 | val referenceIds = mutableSetOf() 25 | val (baseProps, nextInheritShrink) = extractBasePropsWithShrinkInheritance(config, inheritShrink) 26 | return ZStackElement( 27 | content = (config["content"] as? List<*>)?.mapNotNull { item -> 28 | processContentItem( 29 | (item as? Map<*, *>)?.let { content -> childMapper(content, nextInheritShrink) }, 30 | referenceIds, 31 | refBundles.targetElements, 32 | ) 33 | }?.takeIf { it.isNotEmpty() } 34 | ?: return SkippedElement, 35 | align = commonAttributeMapper.mapAlign(config), 36 | baseProps = baseProps, 37 | ) 38 | .also { container -> 39 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 40 | addToReferenceTargetsIfNeeded(config, container, refBundles) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/HStackElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.element.HStackElement 8 | import com.adapty.ui.internal.ui.element.SkippedElement 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | 11 | internal class HStackElementMapper( 12 | commonAttributeMapper: CommonAttributeMapper, 13 | ) : BaseUIComplexElementMapper("h_stack", commonAttributeMapper), 14 | UIComplexShrinkableElementMapper { 15 | 16 | override fun map( 17 | config: Map<*, *>, 18 | assets: Assets, 19 | refBundles: ReferenceBundles, 20 | stateMap: MutableMap, 21 | inheritShrink: Int, 22 | childMapper: ChildMapperShrinkable, 23 | ): UIElement { 24 | val referenceIds = mutableSetOf() 25 | val (baseProps, nextInheritShrink) = extractBasePropsWithShrinkInheritance(config, inheritShrink) 26 | return HStackElement( 27 | content = (config["content"] as? List<*>)?.mapNotNull { item -> 28 | processContentItem( 29 | (item as? Map<*, *>)?.let { content -> childMapper(content, nextInheritShrink) }, 30 | referenceIds, 31 | refBundles.targetElements, 32 | ) 33 | }?.takeIf { it.isNotEmpty() } 34 | ?: return SkippedElement, 35 | align = commonAttributeMapper.mapVerticalAlign(config["v_align"]), 36 | spacing = config.extractSpacingOrNull(), 37 | baseProps = baseProps, 38 | ) 39 | .also { container -> 40 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 41 | addToReferenceTargetsIfNeeded(config, container, refBundles) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/VStackElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.ui.element.SkippedElement 8 | import com.adapty.ui.internal.ui.element.UIElement 9 | import com.adapty.ui.internal.ui.element.VStackElement 10 | 11 | internal class VStackElementMapper( 12 | commonAttributeMapper: CommonAttributeMapper, 13 | ) : BaseUIComplexElementMapper("v_stack", commonAttributeMapper), 14 | UIComplexShrinkableElementMapper { 15 | 16 | override fun map( 17 | config: Map<*, *>, 18 | assets: Assets, 19 | refBundles: ReferenceBundles, 20 | stateMap: MutableMap, 21 | inheritShrink: Int, 22 | childMapper: ChildMapperShrinkable, 23 | ): UIElement { 24 | val referenceIds = mutableSetOf() 25 | val (baseProps, nextInheritShrink) = extractBasePropsWithShrinkInheritance(config, inheritShrink) 26 | return VStackElement( 27 | content = (config["content"] as? List<*>)?.mapNotNull { item -> 28 | processContentItem( 29 | (item as? Map<*, *>)?.let { content -> childMapper(content, nextInheritShrink) }, 30 | referenceIds, 31 | refBundles.targetElements, 32 | ) 33 | }?.takeIf { it.isNotEmpty() } 34 | ?: return SkippedElement, 35 | align = commonAttributeMapper.mapHorizontalAlign(config["h_align"]), 36 | spacing = config.extractSpacingOrNull(), 37 | baseProps = baseProps, 38 | ) 39 | .also { container -> 40 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 41 | addToReferenceTargetsIfNeeded(config, container, refBundles) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/MediaSaver.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.cache 2 | 3 | import androidx.annotation.RestrictTo 4 | import com.adapty.ui.internal.utils.LOG_PREFIX 5 | import com.adapty.ui.internal.utils.log 6 | import com.adapty.utils.AdaptyLogLevel.Companion.ERROR 7 | import com.adapty.utils.AdaptyLogLevel.Companion.VERBOSE 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | import java.io.InputStream 11 | import java.net.HttpURLConnection 12 | 13 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 14 | internal class MediaSaver( 15 | private val cacheFileManager: CacheFileManager, 16 | ) { 17 | 18 | fun save(url: String, connection: HttpURLConnection): Result { 19 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# saving media \"...${url.takeLast(10)}\"" } 20 | try { 21 | val tempFile = cacheFileManager.getFile(url, isTemp = true) 22 | copyStreamToFile(connection.inputStream, tempFile) 23 | val file = cacheFileManager.getFile(url) 24 | if (tempFile.exists()) 25 | tempFile.renameTo(file) 26 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# saved media \"...${url.takeLast(10)}\"" } 27 | return Result.success(file) 28 | } catch (e: Throwable) { 29 | log(ERROR) { "$LOG_PREFIX #AdaptyMediaCache# saving media \"...${url.takeLast(10)}\" failed: ${e.localizedMessage}" } 30 | return Result.failure(e) 31 | } finally { 32 | connection.disconnect() 33 | } 34 | } 35 | 36 | private fun copyStreamToFile(inputStream: InputStream, outputFile: File) { 37 | inputStream.use { input -> 38 | val outputStream = FileOutputStream(outputFile) 39 | outputStream.use { output -> 40 | val buffer = ByteArray(4 * 1024) 41 | while (true) { 42 | val byteCount = input.read(buffer) 43 | if (byteCount < 0) break 44 | output.write(buffer, 0, byteCount) 45 | } 46 | output.flush() 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/SectionElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.ui.element.SectionElement 10 | import com.adapty.ui.internal.ui.element.UIElement 11 | 12 | internal class SectionElementMapper( 13 | commonAttributeMapper: CommonAttributeMapper, 14 | ) : BaseUIComplexElementMapper("section", commonAttributeMapper), UIComplexElementMapper { 15 | 16 | override fun map( 17 | config: Map<*, *>, 18 | assets: Assets, 19 | refBundles: ReferenceBundles, 20 | stateMap: MutableMap, 21 | inheritShrink: Int, 22 | childMapper: ChildMapper, 23 | ): UIElement { 24 | val referenceIds = mutableSetOf() 25 | return SectionElement( 26 | (config["id"] as? String)?.takeIf { it.isNotEmpty() } 27 | ?: throw adaptyError( 28 | message = "id in Section must not be empty", 29 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 30 | ), 31 | (config["index"] as? Number)?.toInt() ?: 0, 32 | (config["content"] as? List<*>)?.mapNotNull { item -> 33 | processContentItem( 34 | (item as? Map<*, *>)?.let { content -> childMapper(content) }, 35 | referenceIds, 36 | refBundles.targetElements, 37 | ) 38 | } ?: throw adaptyError( 39 | message = "content in Section must not be null", 40 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 41 | ), 42 | ).also { section -> 43 | stateMap[section.key] = section.index 44 | addToAwaitingReferencesIfNeeded(referenceIds, section, refBundles.awaitingElements) 45 | addToReferenceTargetsIfNeeded(config, section, refBundles) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/BoxElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.wrapContentHeight 5 | import androidx.compose.foundation.layout.wrapContentWidth 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.internal.mapping.element.Assets 10 | import com.adapty.ui.internal.text.StringId 11 | import com.adapty.ui.internal.text.StringWrapper 12 | import com.adapty.ui.internal.ui.attributes.Align 13 | import com.adapty.ui.internal.ui.attributes.DimSpec 14 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 15 | import com.adapty.ui.internal.ui.fillWithBaseParams 16 | import com.adapty.ui.internal.utils.EventCallback 17 | 18 | @InternalAdaptyApi 19 | public class BoxElement internal constructor( 20 | override var content: UIElement, 21 | internal val align: Align, 22 | override val baseProps: BaseProps, 23 | ) : UIElement, SingleContainer { 24 | 25 | override fun toComposable( 26 | resolveAssets: () -> Assets, 27 | resolveText: @Composable (StringId) -> StringWrapper?, 28 | resolveState: () -> Map, 29 | eventCallback: EventCallback, 30 | modifier: Modifier 31 | ): @Composable () -> Unit = { 32 | var localModifier: Modifier = Modifier 33 | if (baseProps.widthSpec is DimSpec.Specified) 34 | localModifier = localModifier.wrapContentWidth(unbounded = true) 35 | if (baseProps.heightSpec is DimSpec.Specified) 36 | localModifier = localModifier.wrapContentHeight(unbounded = true) 37 | Box( 38 | contentAlignment = align.toComposeAlignment(), 39 | modifier = localModifier.then(modifier), 40 | ) { 41 | content.toComposable( 42 | resolveAssets, 43 | resolveText, 44 | resolveState, 45 | eventCallback, 46 | Modifier.fillWithBaseParams(content, resolveAssets), 47 | ).invoke() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/UIElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.Stable 7 | import androidx.compose.ui.Modifier 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.internal.mapping.element.Assets 10 | import com.adapty.ui.internal.text.StringId 11 | import com.adapty.ui.internal.text.StringWrapper 12 | import com.adapty.ui.internal.utils.EventCallback 13 | 14 | @InternalAdaptyApi 15 | @Stable 16 | public interface UIElement { 17 | public val baseProps: BaseProps 18 | 19 | public fun toComposable( 20 | resolveAssets: () -> Assets, 21 | resolveText: @Composable (StringId) -> StringWrapper?, 22 | resolveState: () -> Map, 23 | eventCallback: EventCallback, 24 | modifier: Modifier, 25 | ): @Composable () -> Unit 26 | 27 | public fun RowScope.toComposableInRow( 28 | resolveAssets: () -> Assets, 29 | resolveText: @Composable (StringId) -> StringWrapper?, 30 | resolveState: () -> Map, 31 | eventCallback: EventCallback, 32 | modifier: Modifier, 33 | ): @Composable () -> Unit { 34 | return toComposable( 35 | resolveAssets, 36 | resolveText, 37 | resolveState, 38 | eventCallback, 39 | baseProps.weight?.let { modifier.weight(it) } ?: modifier, 40 | ) 41 | } 42 | 43 | public fun ColumnScope.toComposableInColumn( 44 | resolveAssets: () -> Assets, 45 | resolveText: @Composable (StringId) -> StringWrapper?, 46 | resolveState: () -> Map, 47 | eventCallback: EventCallback, 48 | modifier: Modifier, 49 | ): @Composable () -> Unit { 50 | return toComposable( 51 | resolveAssets, 52 | resolveText, 53 | resolveState, 54 | eventCallback, 55 | baseProps.weight?.let { modifier.weight(it) } ?: modifier, 56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/MediaDownloader.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.cache 4 | 5 | import androidx.annotation.RestrictTo 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.internal.utils.errorCodeFromNetwork 9 | import com.adapty.ui.internal.utils.LOG_PREFIX 10 | import com.adapty.ui.internal.utils.log 11 | import com.adapty.utils.AdaptyLogLevel.Companion.ERROR 12 | import com.adapty.utils.AdaptyLogLevel.Companion.VERBOSE 13 | import java.net.HttpURLConnection 14 | import java.net.URL 15 | 16 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 17 | internal class MediaDownloader { 18 | 19 | private companion object { 20 | const val TIMEOUT = 30 * 1000 21 | } 22 | 23 | fun download(url: String): Result { 24 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# downloading media \"...${url.takeLast(10)}\"" } 25 | try { 26 | val connection = (URL(url).openConnection() as HttpURLConnection).apply { 27 | connectTimeout = TIMEOUT 28 | readTimeout = TIMEOUT 29 | requestMethod = "GET" 30 | } 31 | connection.connect() 32 | 33 | if (connection.responseCode in 200..299) { 34 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# downloaded media \"...${url.takeLast(10)}\"" } 35 | return Result.success(connection) 36 | } else { 37 | log(ERROR) { "$LOG_PREFIX #AdaptyMediaCache# downloading media \"...${url.takeLast(10)}\" failed: code ${connection.responseCode}" } 38 | return Result.failure( 39 | adaptyError( 40 | message = "Request failed with code ${connection.responseCode}", 41 | adaptyErrorCode = errorCodeFromNetwork(connection.responseCode) 42 | ) 43 | ) 44 | } 45 | } catch (e: Exception) { 46 | log(ERROR) { "$LOG_PREFIX #AdaptyMediaCache# downloading media \"...${url.takeLast(10)}\" failed: code ${e.localizedMessage}" } 47 | return Result.failure(e) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/utils.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.utils 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.platform.LocalConfiguration 7 | import androidx.compose.ui.platform.LocalDensity 8 | import androidx.compose.ui.platform.LocalLayoutDirection 9 | import com.adapty.internal.utils.InternalAdaptyApi 10 | import com.adapty.models.AdaptyEligibility.ELIGIBLE 11 | import com.adapty.models.AdaptyPaywallProduct 12 | import com.adapty.models.AdaptyProductDiscountPhase 13 | import com.adapty.utils.AdaptyLogLevel 14 | import java.util.concurrent.Executors 15 | 16 | internal fun Context.getCurrentLocale() = 17 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 18 | resources.configuration.locales.get(0) 19 | } else { 20 | resources.configuration.locale 21 | } 22 | 23 | internal fun AdaptyPaywallProduct.firstDiscountOfferOrNull(): AdaptyProductDiscountPhase? { 24 | return subscriptionDetails?.let { subDetails -> 25 | subDetails.introductoryOfferPhases.firstOrNull()?.takeIf { subDetails.introductoryOfferEligibility == ELIGIBLE } 26 | } 27 | } 28 | 29 | internal fun getProductGroupKey(groupId: String) = "group_${groupId}" 30 | 31 | internal inline fun Map<*, *>.getAs(key: String) = this[key] as? T 32 | 33 | @Composable 34 | internal fun getScreenHeightDp(): Float { 35 | val insets = getInsets() 36 | return with(LocalDensity.current) { 37 | LocalConfiguration.current.screenHeightDp + (insets.getTop(this) + insets.getBottom(this)).toDp().value 38 | } 39 | } 40 | 41 | @Composable 42 | internal fun getScreenWidthDp(): Float { 43 | val insets = getInsets() 44 | return with(LocalDensity.current) { 45 | val layoutDirection = LocalLayoutDirection.current 46 | LocalConfiguration.current.screenWidthDp + (insets.getLeft(this, layoutDirection) + insets.getRight(this, layoutDirection)).toDp().value 47 | } 48 | } 49 | 50 | private val logExecutor = Executors.newSingleThreadExecutor() 51 | 52 | @OptIn(InternalAdaptyApi::class) 53 | internal fun log(messageLogLevel: AdaptyLogLevel, msg: () -> String) { 54 | logExecutor.execute { com.adapty.internal.utils.log(messageLogLevel, msg) } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/adaptyui/example/PaywallUiFragment.kt: -------------------------------------------------------------------------------- 1 | package com.adaptyui.example 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.fragment.app.Fragment 7 | import com.adapty.models.AdaptyPaywallProduct 8 | import com.adapty.models.AdaptyProfile 9 | import com.adapty.ui.AdaptyPaywallView 10 | import com.adapty.ui.AdaptyUI 11 | import com.adapty.ui.listeners.AdaptyUiDefaultEventListener 12 | 13 | class PaywallUiFragment : Fragment(R.layout.fragment_paywall_ui) { 14 | 15 | companion object { 16 | fun newInstance( 17 | viewConfig: AdaptyUI.LocalizedViewConfiguration, 18 | products: List, 19 | ) = 20 | PaywallUiFragment().apply { 21 | this.products = products 22 | this.viewConfiguration = viewConfig 23 | } 24 | } 25 | 26 | private var viewConfiguration: AdaptyUI.LocalizedViewConfiguration? = null 27 | private var products = listOf() 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | val paywallView = view as? AdaptyPaywallView ?: return 33 | val viewConfig = viewConfiguration ?: return 34 | 35 | val eventListener = object: AdaptyUiDefaultEventListener() { 36 | 37 | /** 38 | * You can override more methods if needed 39 | */ 40 | 41 | override fun onRestoreSuccess( 42 | profile: AdaptyProfile, 43 | context: Context, 44 | ) { 45 | if (profile.accessLevels["premium"]?.isActive == true) { 46 | parentFragmentManager.popBackStack() 47 | } 48 | } 49 | } 50 | 51 | val customTags = mapOf("USERNAME" to "Bruce", "CITY" to "Philadelphia") 52 | paywallView.showPaywall( 53 | viewConfig, 54 | products, 55 | eventListener, 56 | tagResolver = { tag -> customTags[tag] } 57 | ) 58 | 59 | /** 60 | * Also you can get the `AdaptyPaywallView` and set paywall right away 61 | * by calling `AdaptyUi.getPaywallView()` 62 | */ 63 | } 64 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/ColumnElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.IntrinsicSize 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.internal.mapping.element.Assets 12 | import com.adapty.ui.internal.text.StringId 13 | import com.adapty.ui.internal.text.StringWrapper 14 | import com.adapty.ui.internal.ui.fillWithBaseParams 15 | import com.adapty.ui.internal.utils.EventCallback 16 | 17 | @InternalAdaptyApi 18 | public class ColumnElement internal constructor( 19 | override var content: List, 20 | internal val spacing: Float?, 21 | override val baseProps: BaseProps, 22 | ) : UIElement, MultiContainer { 23 | internal val items get() = content.filterIsInstance() 24 | 25 | override fun toComposable( 26 | resolveAssets: () -> Assets, 27 | resolveText: @Composable (StringId) -> StringWrapper?, 28 | resolveState: () -> Map, 29 | eventCallback: EventCallback, 30 | modifier: Modifier, 31 | ): @Composable () -> Unit = { 32 | val spacing = spacing?.dp 33 | Column( 34 | verticalArrangement = when { 35 | spacing != null -> Arrangement.spacedBy(spacing) 36 | else -> Arrangement.Top 37 | }, 38 | modifier = modifier 39 | .width(IntrinsicSize.Min), 40 | ) { 41 | items.forEach { item -> 42 | item.run { 43 | toComposableInColumn( 44 | resolveAssets, 45 | resolveText, 46 | resolveState, 47 | eventCallback, 48 | fillModifierWithScopedParams( 49 | item, 50 | Modifier.fillWithBaseParams(item, resolveAssets), 51 | ) 52 | ).invoke() 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/RowElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.IntrinsicSize 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.internal.mapping.element.Assets 12 | import com.adapty.ui.internal.text.StringId 13 | import com.adapty.ui.internal.text.StringWrapper 14 | import com.adapty.ui.internal.ui.fillWithBaseParams 15 | import com.adapty.ui.internal.utils.EventCallback 16 | 17 | @InternalAdaptyApi 18 | public class RowElement internal constructor( 19 | override var content: List, 20 | internal val spacing: Float?, 21 | override val baseProps: BaseProps, 22 | ) : UIElement, MultiContainer { 23 | internal val items get() = content.filterIsInstance() 24 | 25 | override fun toComposable( 26 | resolveAssets: () -> Assets, 27 | resolveText: @Composable (StringId) -> StringWrapper?, 28 | resolveState: () -> Map, 29 | eventCallback: EventCallback, 30 | modifier: Modifier, 31 | ): @Composable () -> Unit = { 32 | val spacing = spacing?.dp 33 | Row( 34 | horizontalArrangement = when { 35 | spacing != null -> Arrangement.spacedBy(spacing) 36 | else -> Arrangement.Start 37 | }, 38 | modifier = modifier 39 | .height(IntrinsicSize.Min), 40 | ) { 41 | items.forEach { item -> 42 | item.run { 43 | this@Row.toComposableInRow( 44 | resolveAssets, 45 | resolveText, 46 | resolveState, 47 | eventCallback, 48 | fillModifierWithScopedParams( 49 | item, 50 | Modifier.fillWithBaseParams(item, resolveAssets), 51 | ) 52 | ).invoke() 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/HStackElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.internal.mapping.element.Assets 10 | import com.adapty.ui.internal.text.StringId 11 | import com.adapty.ui.internal.text.StringWrapper 12 | import com.adapty.ui.internal.ui.attributes.VerticalAlign 13 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 14 | import com.adapty.ui.internal.ui.fillWithBaseParams 15 | import com.adapty.ui.internal.utils.EventCallback 16 | 17 | @InternalAdaptyApi 18 | public class HStackElement internal constructor( 19 | override var content: List, 20 | internal val align: VerticalAlign, 21 | internal val spacing: Float?, 22 | override val baseProps: BaseProps, 23 | ) : UIElement, MultiContainer { 24 | 25 | override fun toComposable( 26 | resolveAssets: () -> Assets, 27 | resolveText: @Composable (StringId) -> StringWrapper?, 28 | resolveState: () -> Map, 29 | eventCallback: EventCallback, 30 | modifier: Modifier, 31 | ): @Composable () -> Unit = { 32 | val spacing = spacing?.dp 33 | Row( 34 | horizontalArrangement = when { 35 | spacing != null -> Arrangement.spacedBy(spacing) 36 | else -> Arrangement.Start 37 | }, 38 | verticalAlignment = align.toComposeAlignment(), 39 | modifier = modifier, 40 | ) { 41 | content.forEach { item -> 42 | item.run { 43 | this@Row.toComposableInRow( 44 | resolveAssets, 45 | resolveText, 46 | resolveState, 47 | eventCallback, 48 | fillModifierWithScopedParams( 49 | item, 50 | Modifier.fillWithBaseParams(item, resolveAssets), 51 | ) 52 | ).invoke() 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/VStackElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.internal.mapping.element.Assets 10 | import com.adapty.ui.internal.text.StringId 11 | import com.adapty.ui.internal.text.StringWrapper 12 | import com.adapty.ui.internal.ui.attributes.HorizontalAlign 13 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 14 | import com.adapty.ui.internal.ui.fillWithBaseParams 15 | import com.adapty.ui.internal.utils.EventCallback 16 | 17 | @InternalAdaptyApi 18 | public class VStackElement internal constructor( 19 | override var content: List, 20 | internal val align: HorizontalAlign, 21 | internal val spacing: Float?, 22 | override val baseProps: BaseProps, 23 | ) : UIElement, MultiContainer { 24 | 25 | override fun toComposable( 26 | resolveAssets: () -> Assets, 27 | resolveText: @Composable (StringId) -> StringWrapper?, 28 | resolveState: () -> Map, 29 | eventCallback: EventCallback, 30 | modifier: Modifier, 31 | ): @Composable () -> Unit = { 32 | val spacing = spacing?.dp 33 | Column( 34 | verticalArrangement = when { 35 | spacing != null -> Arrangement.spacedBy(spacing) 36 | else -> Arrangement.Top 37 | }, 38 | horizontalAlignment = align.toComposeAlignment(), 39 | modifier = modifier, 40 | ) { 41 | content.forEach { item -> 42 | item.run { 43 | this@Column.toComposableInColumn( 44 | resolveAssets, 45 | resolveText, 46 | resolveState, 47 | eventCallback, 48 | fillModifierWithScopedParams( 49 | item, 50 | Modifier.fillWithBaseParams(item, resolveAssets), 51 | ), 52 | ).invoke() 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/PagerElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 7 | import com.adapty.ui.internal.mapping.attributes.PagerAttributeMapper 8 | import com.adapty.ui.internal.ui.element.PagerElement 9 | import com.adapty.ui.internal.ui.element.SkippedElement 10 | import com.adapty.ui.internal.ui.element.UIElement 11 | 12 | internal class PagerElementMapper( 13 | private val pagerAttributeMapper: PagerAttributeMapper, 14 | commonAttributeMapper: CommonAttributeMapper, 15 | ) : BaseUIComplexElementMapper("pager", commonAttributeMapper), UIComplexElementMapper { 16 | 17 | override fun map( 18 | config: Map<*, *>, 19 | assets: Assets, 20 | refBundles: ReferenceBundles, 21 | stateMap: MutableMap, 22 | inheritShrink: Int, 23 | childMapper: ChildMapper, 24 | ): UIElement { 25 | val referenceIds = mutableSetOf() 26 | return PagerElement( 27 | pagerAttributeMapper.mapPageSize(config["page_width"]), 28 | pagerAttributeMapper.mapPageSize(config["page_height"]), 29 | config["page_padding"]?.let(commonAttributeMapper::mapEdgeEntities), 30 | config.extractSpacingOrNull(), 31 | (config["content"] as? List<*>)?.mapNotNull { item -> 32 | processContentItem( 33 | (item as? Map<*, *>)?.let { content -> childMapper(content) }, 34 | referenceIds, 35 | refBundles.targetElements, 36 | ) 37 | }?.takeIf { it.isNotEmpty() } 38 | ?: return SkippedElement, 39 | (config["page_control"] as? Map<*, *>)?.let(pagerAttributeMapper::mapPagerIndicator), 40 | (config["animation"] as? Map<*, *>)?.let(pagerAttributeMapper::mapPagerAnimation), 41 | pagerAttributeMapper.mapInteractionBehavior(config["interaction"]), 42 | config.extractBaseProps(), 43 | ) 44 | .also { container -> 45 | addToAwaitingReferencesIfNeeded(referenceIds, container, refBundles.awaitingElements) 46 | addToReferenceTargetsIfNeeded(config, container, refBundles) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ButtonElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.mapping.attributes.InteractiveAttributeMapper 10 | import com.adapty.ui.internal.ui.element.ButtonElement 11 | import com.adapty.ui.internal.ui.element.UIElement 12 | 13 | internal class ButtonElementMapper( 14 | private val interactiveAttributeMapper: InteractiveAttributeMapper, 15 | commonAttributeMapper: CommonAttributeMapper, 16 | ) : BaseUIComplexElementMapper("button", commonAttributeMapper), UIComplexElementMapper { 17 | override fun map( 18 | config: Map<*, *>, 19 | assets: Assets, 20 | refBundles: ReferenceBundles, 21 | stateMap: MutableMap, 22 | inheritShrink: Int, 23 | childMapper: ChildMapper 24 | ): UIElement { 25 | val referenceIds = mutableSetOf() 26 | return ButtonElement( 27 | (config["action"] as? Iterable<*>)?.mapNotNull { item -> (item as? Map<*, *>)?.let(interactiveAttributeMapper::mapAction) } 28 | ?: (config["action"] as? Map<*, *>)?.let(interactiveAttributeMapper::mapAction)?.let { action -> listOf(action) }.orEmpty(), 29 | processContentItem( 30 | (config["normal"] as? Map<*, *>)?.let { content -> childMapper(content) }, 31 | referenceIds, 32 | refBundles.targetElements, 33 | ) 34 | ?: throw adaptyError( 35 | message = "normal state in Button must not be null", 36 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 37 | ), 38 | processContentItem( 39 | (config["selected"] as? Map<*, *>)?.let { content -> childMapper(content) }, 40 | referenceIds, 41 | refBundles.targetElements, 42 | ), 43 | (config["selected_condition"] as? Map<*, *>)?.let(interactiveAttributeMapper::mapCondition), 44 | config.extractBaseProps(), 45 | ) 46 | .also { element -> 47 | addToReferenceTargetsIfNeeded(config, element, refBundles) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/DimUnit.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import androidx.compose.foundation.layout.WindowInsets 6 | import androidx.compose.foundation.layout.safeContent 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.platform.LocalDensity 9 | import androidx.compose.ui.platform.LocalLayoutDirection 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | import com.adapty.internal.utils.InternalAdaptyApi 13 | import com.adapty.ui.internal.utils.getInsets 14 | import com.adapty.ui.internal.utils.getScreenHeightDp 15 | import com.adapty.ui.internal.utils.getScreenWidthDp 16 | 17 | @Composable 18 | internal fun DimUnit.toExactDp(axis: DimSpec.Axis): Dp { 19 | when (this) { 20 | is DimUnit.Exact -> return this.value.dp 21 | is DimUnit.SafeArea -> { 22 | val density = LocalDensity.current 23 | val layoutDirection = LocalLayoutDirection.current 24 | val safeContent = getInsets() 25 | return with(density) { 26 | when (this@toExactDp.side) { 27 | DimUnit.SafeArea.Side.START -> when (axis) { 28 | DimSpec.Axis.X -> safeContent.getLeft(density, layoutDirection) 29 | DimSpec.Axis.Y -> safeContent.getTop(density) 30 | } 31 | DimUnit.SafeArea.Side.END -> when (axis) { 32 | DimSpec.Axis.X -> safeContent.getRight(density, layoutDirection) 33 | DimSpec.Axis.Y -> safeContent.getBottom(density) 34 | } 35 | }.toDp() 36 | } 37 | } 38 | is DimUnit.ScreenFraction -> { 39 | val maxAvailableWidth = getScreenWidthDp() 40 | val maxAvailableHeight = getScreenHeightDp() 41 | val screenSize = 42 | if (axis == DimSpec.Axis.Y) maxAvailableHeight else maxAvailableWidth 43 | 44 | return (this.fraction * screenSize).dp 45 | } 46 | } 47 | } 48 | 49 | @InternalAdaptyApi 50 | public sealed class DimUnit { 51 | public data class Exact internal constructor(internal val value: Float): DimUnit() 52 | public data class ScreenFraction internal constructor(internal val fraction: Float): DimUnit() 53 | public data class SafeArea internal constructor(internal val side: Side): DimUnit() { 54 | internal enum class Side { START, END } 55 | } 56 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/TextElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.mapping.attributes.TextAttributeMapper 10 | import com.adapty.ui.internal.text.toStringId 11 | import com.adapty.ui.internal.ui.attributes.Shape 12 | import com.adapty.ui.internal.ui.element.BaseTextElement.Attributes 13 | import com.adapty.ui.internal.ui.element.BaseTextElement.OnOverflowMode 14 | import com.adapty.ui.internal.ui.element.TextElement 15 | import com.adapty.ui.internal.ui.element.UIElement 16 | 17 | internal class TextElementMapper( 18 | private val textAttributeMapper: TextAttributeMapper, 19 | commonAttributeMapper: CommonAttributeMapper, 20 | ) : BaseUIElementMapper("text", commonAttributeMapper), UIPlainElementMapper { 21 | 22 | override fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement { 23 | return TextElement( 24 | config["string_id"]?.toStringId() 25 | ?: throw adaptyError( 26 | message = "string_id in Text must not be empty", 27 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 28 | ), 29 | textAttributeMapper.mapTextAlign(config["align"]), 30 | (config["max_rows"] as? Number)?.toInt()?.takeIf { it > 0 }, 31 | (config["on_overflow"] as? List<*>)?.let { 32 | if ("scale" !in it) return@let null 33 | OnOverflowMode.SCALE 34 | }, 35 | config.toTextAttributes(), 36 | config.extractBaseProps(), 37 | ).also { element -> 38 | addToReferenceTargetsIfNeeded(config, element, refBundles) 39 | } 40 | } 41 | 42 | private fun Map<*, *>.toTextAttributes(): Attributes { 43 | return Attributes( 44 | this["font"] as? String, 45 | this["size"]?.toFloatOrNull(), 46 | (this["strike"] as? Boolean) ?: false, 47 | (this["underline"] as? Boolean) ?: false, 48 | (this["color"] as? String)?.let { assetId -> Shape.Fill(assetId) }, 49 | (this["background"] as? String)?.let { assetId -> Shape.Fill(assetId) }, 50 | (this["tint"] as? String)?.let { assetId -> Shape.Fill(assetId) }, 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/CacheCleanupService.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.cache 2 | 3 | import androidx.annotation.RestrictTo 4 | import com.adapty.ui.internal.utils.LOG_PREFIX 5 | import com.adapty.ui.internal.utils.log 6 | import com.adapty.utils.AdaptyLogLevel.Companion.ERROR 7 | import com.adapty.utils.hours 8 | import java.util.concurrent.Executors 9 | 10 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 11 | internal class CacheCleanupService( 12 | private val cacheFileManager: CacheFileManager, 13 | private val cacheConfigManager: MediaCacheConfigManager, 14 | ) { 15 | 16 | private val executor = Executors.newSingleThreadExecutor() 17 | 18 | fun clearExpired() { 19 | val currentCacheConfig = 20 | cacheConfigManager.currentCacheConfig.takeIf { config -> 21 | config.diskStorageSizeLimit > 0L 22 | } ?: return 23 | executor.execute { 24 | try { 25 | val cacheDir = cacheFileManager.getDir() 26 | if (cacheFileManager.getSize(cacheDir) <= currentCacheConfig.diskStorageSizeLimit) 27 | return@execute 28 | cacheDir.listFiles()?.forEach { file -> 29 | val exists = file?.exists() ?: return@forEach 30 | val expired = 31 | if (!cacheFileManager.isTemp(file)) 32 | cacheFileManager.isOlderThan(currentCacheConfig.discCacheValidityTime, file) 33 | else 34 | cacheFileManager.isOlderThan(TEMP_FILE_VALIDITY_TIME, file) 35 | 36 | if (exists && expired) 37 | file.delete() 38 | } 39 | } catch (e: Throwable) { 40 | log(ERROR) { "$LOG_PREFIX #AdaptyMediaCache# couldn't clear cache. ${e.localizedMessage}" } 41 | } 42 | } 43 | } 44 | 45 | fun clearAll() { 46 | executor.execute { 47 | try { 48 | cacheFileManager.getDir().listFiles() 49 | ?.forEach { file -> 50 | if (file?.exists() == true && (!cacheFileManager.isTemp(file) || cacheFileManager.isOlderThan(TEMP_FILE_VALIDITY_TIME, file))) 51 | file.delete() 52 | } 53 | } catch (e: Throwable) { 54 | log(ERROR) { "$LOG_PREFIX #AdaptyMediaCache# couldn't clear cache. ${e.localizedMessage}" } 55 | } 56 | } 57 | } 58 | 59 | private companion object { 60 | val TEMP_FILE_VALIDITY_TIME = 1.hours 61 | } 62 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/listeners/AdaptyUiObserverModeHandler.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.listeners 2 | 3 | import android.content.Context 4 | import com.adapty.models.AdaptyPaywall 5 | import com.adapty.models.AdaptyPaywallProduct 6 | 7 | /** 8 | * If you use Adapty in [Observer mode](https://adapty.io/docs/observer-vs-full-mode), 9 | * implement this interface to handle purchases on your own. 10 | */ 11 | public fun interface AdaptyUiObserverModeHandler { 12 | 13 | /** 14 | * This callback is invoked when the user initiates a purchase. 15 | * You can trigger your custom purchase flow in response to this callback, [read more](https://adapty.io/docs/android-present-paywall-builder-paywalls-in-observer-mode). 16 | * 17 | * @param[product] An [AdaptyPaywallProduct] of the purchase. 18 | * 19 | * @param[paywall] An [AdaptyPaywall] within which the purchase is initiated. 20 | * 21 | * @param[context] A UI [Context] within which the the purchase is initiated. 22 | * 23 | * @param[onStartPurchase] A [PurchaseStartCallback] that should be invoked to notify AdaptyUI 24 | * that the purchase is started. 25 | * 26 | * From Kotlin: 27 | * 28 | * ```Kotlin 29 | * onStartPurchase() 30 | * ``` 31 | * 32 | * From Java: 33 | * 34 | * ```Java 35 | * onStartPurchase.invoke() 36 | * ``` 37 | * 38 | * @param[onFinishPurchase] A [PurchaseFinishCallback] that should be invoked to notify AdaptyUI 39 | * that the purchase is finished successfully or not, or the purchase is canceled. 40 | * 41 | * From Kotlin: 42 | * 43 | * ```Kotlin 44 | * onFinishPurchase() 45 | * ``` 46 | * 47 | * From Java: 48 | * 49 | * ```Java 50 | * onFinishPurchase.invoke() 51 | * ``` 52 | */ 53 | public fun onPurchaseInitiated( 54 | product: AdaptyPaywallProduct, 55 | paywall: AdaptyPaywall, 56 | context: Context, 57 | onStartPurchase: PurchaseStartCallback, 58 | onFinishPurchase: PurchaseFinishCallback, 59 | ) 60 | 61 | public fun interface PurchaseStartCallback { 62 | /** 63 | * This method should be called to notify AdaptyUI that the purchase is started. 64 | */ 65 | public operator fun invoke() 66 | } 67 | 68 | public fun interface PurchaseFinishCallback { 69 | /** 70 | * This method should be called to notify AdaptyUI that the purchase is finished successfully or not, 71 | * or the purchase is canceled. 72 | */ 73 | public operator fun invoke() 74 | } 75 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/PriceConverter.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.text 2 | 3 | import com.adapty.models.AdaptyPaywallProduct 4 | import com.adapty.models.AdaptyPeriodUnit 5 | import com.adapty.models.AdaptyPeriodUnit.MONTH 6 | import com.adapty.models.AdaptyPeriodUnit.YEAR 7 | import java.math.BigDecimal 8 | import java.math.RoundingMode 9 | 10 | internal class PriceConverter { 11 | 12 | fun toYearly( 13 | price: AdaptyPaywallProduct.Price, 14 | unit: AdaptyPeriodUnit, 15 | numberOfUnits: Int, 16 | ): BigDecimal { 17 | val unitsInYear = when (unit) { 18 | YEAR -> 1 19 | MONTH -> 12 20 | else -> 52 21 | } 22 | val divisor = numberOfUnits.toBigDecimal() 23 | val multiplier = unitsInYear.toBigDecimal() 24 | return price.amount.divide(divisor, 4, RoundingMode.CEILING) * multiplier 25 | } 26 | 27 | fun toMonthly( 28 | price: AdaptyPaywallProduct.Price, 29 | unit: AdaptyPeriodUnit, 30 | numberOfUnits: Int, 31 | ): BigDecimal { 32 | val divisor: BigDecimal 33 | val multiplier: BigDecimal 34 | when (unit) { 35 | YEAR -> { 36 | divisor = (12 * numberOfUnits).toBigDecimal() 37 | multiplier = BigDecimal.ONE 38 | } 39 | MONTH -> { 40 | divisor = numberOfUnits.toBigDecimal() 41 | multiplier = BigDecimal.ONE 42 | } 43 | else -> { 44 | divisor = numberOfUnits.toBigDecimal() 45 | multiplier = 4.toBigDecimal() 46 | } 47 | } 48 | return price.amount.divide(divisor, 4, RoundingMode.CEILING) * multiplier 49 | } 50 | 51 | fun toWeekly( 52 | price: AdaptyPaywallProduct.Price, 53 | unit: AdaptyPeriodUnit, 54 | numberOfUnits: Int, 55 | ): BigDecimal { 56 | val weeksInUnit = when (unit) { 57 | YEAR -> 52 58 | MONTH -> 4 59 | else -> 1 60 | } 61 | val divisor = (weeksInUnit * numberOfUnits).toBigDecimal() 62 | return price.amount.divide(divisor, 4, RoundingMode.CEILING) 63 | } 64 | 65 | fun toDaily( 66 | price: AdaptyPaywallProduct.Price, 67 | unit: AdaptyPeriodUnit, 68 | numberOfUnits: Int, 69 | ): BigDecimal { 70 | val daysInUnit = when (unit) { 71 | YEAR -> 365 72 | MONTH -> 30 73 | else -> 7 74 | } 75 | val divisor = (daysInUnit * numberOfUnits).toBigDecimal() 76 | return price.amount.divide(divisor, 4, RoundingMode.CEILING) 77 | } 78 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/Shapes.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.geometry.Rect 5 | import androidx.compose.ui.geometry.Size 6 | import androidx.compose.ui.graphics.Outline 7 | import androidx.compose.ui.graphics.Path 8 | import androidx.compose.ui.graphics.Shape 9 | import androidx.compose.ui.unit.Density 10 | import androidx.compose.ui.unit.LayoutDirection 11 | import kotlin.math.pow 12 | 13 | internal class RectWithArcShape(private val arcHeight: Float, private val segments: Int = 50) : Shape { 14 | override fun createOutline( 15 | size: Size, 16 | layoutDirection: LayoutDirection, 17 | density: Density, 18 | ): Outline { 19 | val path = Path() 20 | val bounds = Rect(0f, 0f, size.width, size.height) 21 | path.moveTo(bounds.left, bounds.bottom) 22 | when { 23 | arcHeight > 0f -> { 24 | val yOffset = bounds.top + arcHeight 25 | path.lineTo(bounds.left, yOffset) 26 | addParabola(path, bounds, yOffset, bounds.top, segments) 27 | } 28 | arcHeight < 0f -> { 29 | path.lineTo(bounds.left, bounds.top) 30 | addParabola(path, bounds, bounds.top, bounds.top - arcHeight, segments) 31 | } 32 | else -> { 33 | path.lineTo(bounds.left, bounds.top) 34 | path.lineTo(bounds.right, bounds.top) 35 | } 36 | } 37 | 38 | path.lineTo(bounds.right, bounds.bottom) 39 | path.close() 40 | 41 | return Outline.Generic(path) 42 | } 43 | 44 | private fun addParabola(path: Path, bounds: Rect, startY: Float, peakY: Float, segments: Int) { 45 | val a = (startY - peakY) * 4 / bounds.width.pow(2) 46 | for (i in 0..segments) { 47 | val x = bounds.left + bounds.width * i / segments 48 | val y = a * (x - bounds.center.x).pow(2) + peakY 49 | path.lineTo(x, y) 50 | } 51 | } 52 | } 53 | 54 | internal object CircleShape : Shape { 55 | override fun createOutline( 56 | size: Size, 57 | layoutDirection: LayoutDirection, 58 | density: Density, 59 | ): Outline { 60 | val radius = minOf(size.width, size.height) / 2f 61 | val center = Offset(size.width / 2f, size.height / 2f) 62 | 63 | return Outline.Generic( 64 | Path().apply { 65 | addOval( 66 | Rect( 67 | center.x - radius, 68 | center.y - radius, 69 | center.x + radius, 70 | center.y + radius 71 | ) 72 | ) 73 | } 74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/listeners/AdaptyUiDefaultEventListener.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.listeners 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import com.adapty.errors.AdaptyError 8 | import com.adapty.models.AdaptyPaywallProduct 9 | import com.adapty.models.AdaptyProfile 10 | import com.adapty.models.AdaptyPurchasedInfo 11 | import com.adapty.models.AdaptySubscriptionUpdateParameters 12 | import com.adapty.ui.AdaptyUI 13 | import com.adapty.ui.internal.utils.LOG_PREFIX_ERROR 14 | import com.adapty.ui.internal.utils.log 15 | import com.adapty.utils.AdaptyLogLevel.Companion.ERROR 16 | 17 | public open class AdaptyUiDefaultEventListener : AdaptyUiEventListener { 18 | 19 | override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { 20 | when (action) { 21 | AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() 22 | is AdaptyUI.Action.OpenUrl -> { 23 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url)) 24 | try { 25 | context.startActivity(Intent.createChooser(intent, "")) 26 | } catch (e: Throwable) { 27 | log(ERROR) { "$LOG_PREFIX_ERROR couldn't find an app that can process this url" } 28 | } 29 | } 30 | is AdaptyUI.Action.Custom -> Unit 31 | } 32 | } 33 | 34 | override fun onAwaitingSubscriptionUpdateParams( 35 | product: AdaptyPaywallProduct, 36 | context: Context, 37 | ): AdaptySubscriptionUpdateParameters? { 38 | return null 39 | } 40 | 41 | public override fun onLoadingProductsFailure( 42 | error: AdaptyError, 43 | context: Context, 44 | ): Boolean = false 45 | 46 | override fun onProductSelected( 47 | product: AdaptyPaywallProduct, 48 | context: Context, 49 | ) {} 50 | 51 | public override fun onPurchaseCanceled( 52 | product: AdaptyPaywallProduct, 53 | context: Context, 54 | ) {} 55 | 56 | public override fun onPurchaseFailure( 57 | error: AdaptyError, 58 | product: AdaptyPaywallProduct, 59 | context: Context, 60 | ) {} 61 | 62 | override fun onPurchaseStarted( 63 | product: AdaptyPaywallProduct, 64 | context: Context, 65 | ) {} 66 | 67 | public override fun onPurchaseSuccess( 68 | purchasedInfo: AdaptyPurchasedInfo?, 69 | product: AdaptyPaywallProduct, 70 | context: Context, 71 | ) { 72 | (context as? Activity)?.onBackPressed() 73 | } 74 | 75 | public override fun onRenderingError( 76 | error: AdaptyError, 77 | context: Context, 78 | ) {} 79 | 80 | public override fun onRestoreFailure( 81 | error: AdaptyError, 82 | context: Context, 83 | ) {} 84 | 85 | override fun onRestoreStarted(context: Context) {} 86 | 87 | public override fun onRestoreSuccess( 88 | profile: AdaptyProfile, 89 | context: Context, 90 | ) {} 91 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/cache/SingleMediaHandler.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.cache 4 | 5 | import androidx.annotation.RestrictTo 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.unlockQuietly 8 | import com.adapty.ui.internal.utils.LOG_PREFIX 9 | import com.adapty.ui.internal.utils.log 10 | import com.adapty.utils.AdaptyLogLevel.Companion.VERBOSE 11 | import java.io.File 12 | import java.util.concurrent.ExecutorService 13 | import java.util.concurrent.locks.ReentrantLock 14 | 15 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 16 | internal class SingleMediaHandler( 17 | private val mediaDownloader: MediaDownloader, 18 | private val mediaSaver: MediaSaver, 19 | private val cacheFileManager: CacheFileManager, 20 | private val cacheCleanupService: CacheCleanupService, 21 | private val executor: ExecutorService, 22 | private val url: String, 23 | ) { 24 | 25 | private val lock = ReentrantLock() 26 | 27 | fun loadMedia(onResult: ((Result) -> Unit)? = null, onCachedSkipped: ((Boolean) -> Unit)? = null) { 28 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# requesting media \"...${url.takeLast(10)}\"" } 29 | executor.execute { 30 | try { 31 | val cachedFile = cacheFileManager.getFile(url) 32 | if (cachedFile.exists() && cacheFileManager.getSize(cachedFile) > 0L) { 33 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# media \"...${url.takeLast(10)}\" retrieved from cache" } 34 | onCachedSkipped?.invoke(false) 35 | onResult?.invoke(Result.success(cachedFile)) 36 | return@execute 37 | } else { 38 | onCachedSkipped?.invoke(true) 39 | lock.lock() 40 | if (cachedFile.exists() && cacheFileManager.getSize(cachedFile) > 0L) { 41 | lock.unlockQuietly() 42 | log(VERBOSE) { "$LOG_PREFIX #AdaptyMediaCache# media \"...${url.takeLast(10)}\" retrieved from cache" } 43 | onCachedSkipped?.invoke(false) 44 | onResult?.invoke(Result.success(cachedFile)) 45 | return@execute 46 | } 47 | mediaDownloader.download(url) 48 | .mapCatching { connection -> 49 | mediaSaver.save(url, connection) 50 | } 51 | .onSuccess { result -> 52 | lock.unlockQuietly() 53 | onResult?.invoke(result) 54 | cacheCleanupService.clearExpired() 55 | } 56 | .onFailure { e -> 57 | lock.unlockQuietly() 58 | onResult?.invoke(Result.failure(e)) 59 | } 60 | } 61 | } catch (e: Throwable) { 62 | lock.unlockQuietly() 63 | onResult?.invoke(Result.failure(e)) 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/ToggleElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material3.Switch 6 | import androidx.compose.material3.SwitchDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset 12 | import com.adapty.ui.internal.mapping.element.Assets 13 | import com.adapty.ui.internal.text.StringId 14 | import com.adapty.ui.internal.text.StringWrapper 15 | import com.adapty.ui.internal.ui.attributes.Shape 16 | import com.adapty.ui.internal.ui.attributes.toComposeFill 17 | import com.adapty.ui.internal.utils.EventCallback 18 | import com.adapty.ui.internal.utils.getProductGroupKey 19 | 20 | @InternalAdaptyApi 21 | public class ToggleElement internal constructor( 22 | internal val onActions: List, 23 | internal val offActions: List, 24 | internal val onCondition: Condition, 25 | internal val color: Shape.Fill?, 26 | override val baseProps: BaseProps, 27 | ) : UIElement { 28 | 29 | override fun toComposable( 30 | resolveAssets: () -> Assets, 31 | resolveText: @Composable (StringId) -> StringWrapper?, 32 | resolveState: () -> Map, 33 | eventCallback: EventCallback, 34 | modifier: Modifier, 35 | ): @Composable () -> Unit = { 36 | val state = resolveState() 37 | val fill = color?.assetId?.let { assetId -> resolveAssets()[assetId] } 38 | val colors = if (fill is Asset.Color) 39 | SwitchDefaults.colors(checkedTrackColor = fill .toComposeFill().color) 40 | else 41 | SwitchDefaults.colors() 42 | 43 | Box( 44 | Modifier 45 | .fillMaxWidth(), 46 | contentAlignment = Alignment.CenterEnd, 47 | ) { 48 | val onActionsResolved = onActions.mapNotNull { action -> action.resolve(resolveText) } 49 | val offActionsResolved = offActions.mapNotNull { action -> action.resolve(resolveText) } 50 | 51 | Switch( 52 | checked = when (onCondition) { 53 | is Condition.SelectedSection -> { 54 | val sectionKey = SectionElement.getKey(onCondition.sectionId) 55 | state[sectionKey] as? Int == onCondition.index 56 | } 57 | is Condition.SelectedProduct -> { 58 | val productGroupKey = getProductGroupKey(onCondition.groupId) 59 | state[productGroupKey] as? String == onCondition.productId 60 | } 61 | else -> false 62 | }, 63 | onCheckedChange = { checked -> 64 | eventCallback.onActions(if (checked) onActionsResolved else offActionsResolved) 65 | }, 66 | colors = colors, 67 | modifier = modifier, 68 | ) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/ImageElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.BoxWithConstraints 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.asImageBitmap 12 | import com.adapty.internal.utils.InternalAdaptyApi 13 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset 14 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset.Image.ScaleType 15 | import com.adapty.ui.internal.mapping.element.Assets 16 | import com.adapty.ui.internal.text.StringId 17 | import com.adapty.ui.internal.text.StringWrapper 18 | import com.adapty.ui.internal.ui.attributes.AspectRatio 19 | import com.adapty.ui.internal.ui.attributes.Shape 20 | import com.adapty.ui.internal.ui.attributes.evaluateComposeImageAlignment 21 | import com.adapty.ui.internal.ui.attributes.toComposeContentScale 22 | import com.adapty.ui.internal.ui.attributes.toComposeFill 23 | import com.adapty.ui.internal.utils.EventCallback 24 | import com.adapty.ui.internal.utils.getBitmap 25 | 26 | @InternalAdaptyApi 27 | public class ImageElement internal constructor( 28 | internal val assetId: String, 29 | internal val aspectRatio: AspectRatio, 30 | internal val tint: Shape.Fill?, 31 | override val baseProps: BaseProps, 32 | ) : UIElement { 33 | 34 | override fun toComposable( 35 | resolveAssets: () -> Assets, 36 | resolveText: @Composable (StringId) -> StringWrapper?, 37 | resolveState: () -> Map, 38 | eventCallback: EventCallback, 39 | modifier: Modifier, 40 | ): @Composable () -> Unit = { 41 | val tint = tint?.assetId?.let { assetId -> resolveAssets()[assetId] } 42 | val colorFilter = remember { 43 | (tint as? Asset.Color)?.toComposeFill()?.color?.let { color -> 44 | ColorFilter.tint(color) 45 | } 46 | } 47 | val image = resolveAssets()[assetId] as? Asset.Image 48 | BoxWithConstraints { 49 | val imageBitmap = remember(constraints.maxWidth, constraints.maxHeight, image?.source?.javaClass) { 50 | image?.let { 51 | getBitmap(image, constraints.maxWidth, constraints.maxHeight, if (aspectRatio == AspectRatio.FIT) ScaleType.FIT_MIN else ScaleType.FIT_MAX) 52 | ?.asImageBitmap() 53 | } 54 | } 55 | 56 | if (imageBitmap == null) return@BoxWithConstraints 57 | 58 | Image( 59 | bitmap = imageBitmap, 60 | contentDescription = null, 61 | alignment = aspectRatio.evaluateComposeImageAlignment(), 62 | contentScale = aspectRatio.toComposeContentScale(), 63 | colorFilter = colorFilter, 64 | modifier = modifier 65 | .fillMaxWidth() 66 | .fillMaxHeight(), 67 | ) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/bitmap.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.utils 4 | 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.util.Base64 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset 10 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset.Image.Dimension 11 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset.Image.ScaleType 12 | 13 | internal fun getBitmap(image: Asset.Image, boundsW: Int, boundsH: Int, scaleType: ScaleType): Bitmap? { 14 | val dim: Dimension 15 | val reqDim: Int 16 | val coef = when (scaleType) { 17 | ScaleType.FIT_MAX -> 1 18 | ScaleType.FIT_MIN -> -1 19 | } 20 | if ((boundsW - boundsH) * coef > 0) { 21 | dim = Dimension.WIDTH 22 | reqDim = boundsW 23 | } else { 24 | dim = Dimension.HEIGHT 25 | reqDim = boundsH 26 | } 27 | return getBitmap(image, reqDim, dim) 28 | } 29 | 30 | internal fun getBitmap(image: Asset.Image, reqDim: Int = 0, dim: Dimension = Dimension.WIDTH) : Bitmap? { 31 | return when (val source = image.source) { 32 | is Asset.Image.Source.Base64Str -> getBitmap(source, reqDim, dim) 33 | is Asset.Image.Source.File -> getBitmap(source, reqDim, dim) 34 | } 35 | } 36 | 37 | private fun getBitmap(source: Asset.Image.Source.Base64Str, reqDim: Int, dim: Dimension) : Bitmap? { 38 | if (source.imageBase64 == null) return null 39 | val byteArray = Base64.decode(source.imageBase64, Base64.DEFAULT) 40 | if (reqDim <= 0) { 41 | return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) 42 | } 43 | val options = BitmapFactory.Options().apply { 44 | inJustDecodeBounds = true 45 | } 46 | BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options) 47 | options.updateInSampleSize(reqDim, dim) 48 | return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options) 49 | } 50 | 51 | private fun getBitmap(source: Asset.Image.Source.File, reqDim: Int, dim: Dimension) : Bitmap? { 52 | if (reqDim <= 0) { 53 | return BitmapFactory.decodeFile(source.file.absolutePath) 54 | } 55 | val options = BitmapFactory.Options().apply { 56 | inJustDecodeBounds = true 57 | } 58 | BitmapFactory.decodeFile(source.file.absolutePath, options) 59 | options.updateInSampleSize(reqDim, dim) 60 | return BitmapFactory.decodeFile(source.file.absolutePath, options) 61 | } 62 | 63 | private fun BitmapFactory.Options.updateInSampleSize(reqDim: Int, dim: Dimension) { 64 | inSampleSize = calculateInSampleSize( 65 | when (dim) { 66 | Dimension.WIDTH -> this.outWidth 67 | Dimension.HEIGHT -> this.outHeight 68 | }, 69 | reqDim, 70 | ) 71 | inJustDecodeBounds = false 72 | } 73 | 74 | private fun calculateInSampleSize(initialDimValue: Int, reqDimValue: Int): Int { 75 | var inSampleSize = 1 76 | if (initialDimValue > reqDimValue) { 77 | val half: Int = initialDimValue / 2 78 | 79 | while (half / inSampleSize >= reqDimValue) { 80 | inSampleSize *= 2 81 | } 82 | } 83 | return inSampleSize 84 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/ToggleElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.mapping.attributes.InteractiveAttributeMapper 10 | import com.adapty.ui.internal.ui.attributes.Shape 11 | import com.adapty.ui.internal.ui.element.Action 12 | import com.adapty.ui.internal.ui.element.Condition 13 | import com.adapty.ui.internal.ui.element.ToggleElement 14 | import com.adapty.ui.internal.ui.element.UIElement 15 | 16 | internal class ToggleElementMapper( 17 | private val interactiveAttributeMapper: InteractiveAttributeMapper, 18 | commonAttributeMapper: CommonAttributeMapper, 19 | ) : BaseUIElementMapper("toggle", commonAttributeMapper), UIPlainElementMapper { 20 | override fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement { 21 | val onActions: List 22 | val offActions: List 23 | val onCondition: Condition 24 | val sectionId = config["section_id"] as? String 25 | if (sectionId != null) { 26 | if (sectionId.isEmpty()) 27 | throw adaptyError( 28 | message = "section_id in Toggle must not be empty", 29 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 30 | ) 31 | val onIndex = (config["on_index"] as? Number)?.toInt() ?: 0 32 | val offIndex = (config["off_index"] as? Number)?.toInt() ?: -1 33 | onCondition = Condition.SelectedSection(sectionId, onIndex) 34 | onActions = listOf(Action.SwitchSection(sectionId, onIndex)) 35 | offActions = listOf(Action.SwitchSection(sectionId, offIndex)) 36 | } else { 37 | onCondition = (config["on_condition"] as? Map<*, *>)?.let(interactiveAttributeMapper::mapCondition) 38 | ?: throw adaptyError( 39 | message = "on_condition in Toggle must not be null", 40 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 41 | ) 42 | onActions = config["on_action"].asActions() 43 | offActions = config["off_action"].asActions() 44 | } 45 | 46 | return ToggleElement( 47 | onActions, 48 | offActions, 49 | onCondition, 50 | (config["color"] as? String)?.let { assetId -> Shape.Fill(assetId) }, 51 | config.extractBaseProps(), 52 | ) 53 | .also { element -> 54 | addToReferenceTargetsIfNeeded(config, element, refBundles) 55 | } 56 | } 57 | 58 | private fun Any?.asActions() = 59 | (this as? Iterable<*>)?.mapNotNull { item -> (item as? Map<*, *>)?.let(interactiveAttributeMapper::mapAction) } 60 | ?: (this as? Map<*, *>)?.let(interactiveAttributeMapper::mapAction)?.let { action -> listOf(action) } 61 | .orEmpty() 62 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/ButtonElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.clip 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.internal.mapping.element.Assets 12 | import com.adapty.ui.internal.text.StringId 13 | import com.adapty.ui.internal.text.StringWrapper 14 | import com.adapty.ui.internal.ui.attributes.toComposeShape 15 | import com.adapty.ui.internal.ui.clickIndication 16 | import com.adapty.ui.internal.ui.fillWithBaseParams 17 | import com.adapty.ui.internal.utils.EventCallback 18 | import com.adapty.ui.internal.utils.getProductGroupKey 19 | 20 | @InternalAdaptyApi 21 | public class ButtonElement internal constructor( 22 | internal val actions: List, 23 | internal val normal: UIElement, 24 | internal val selected: UIElement?, 25 | internal val selectedCondition: Condition?, 26 | override val baseProps: BaseProps, 27 | ) : UIElement { 28 | 29 | override fun toComposable( 30 | resolveAssets: () -> Assets, 31 | resolveText: @Composable (StringId) -> StringWrapper?, 32 | resolveState: () -> Map, 33 | eventCallback: EventCallback, 34 | modifier: Modifier, 35 | ): @Composable () -> Unit = { 36 | val state = resolveState() 37 | val item = when { 38 | selected == null -> normal 39 | selectedCondition is Condition.SelectedSection -> { 40 | val sectionKey = SectionElement.getKey(selectedCondition.sectionId) 41 | if (state[sectionKey] as? Int == selectedCondition.index) 42 | selected 43 | else 44 | normal 45 | } 46 | selectedCondition is Condition.SelectedProduct -> { 47 | val productGroupKey = getProductGroupKey(selectedCondition.groupId) 48 | if (state[productGroupKey] as? String == selectedCondition.productId) 49 | selected 50 | else 51 | normal 52 | } 53 | else -> normal 54 | } 55 | val shape = item.baseProps.shape?.type?.toComposeShape() 56 | val actionsResolved = actions.mapNotNull { action -> action.resolve(resolveText) } 57 | Box( 58 | modifier 59 | .run { 60 | if (shape != null) 61 | clip(shape) 62 | else 63 | this 64 | } 65 | .clickable( 66 | indication = shape?.let { clickIndication() }, 67 | interactionSource = remember { MutableInteractionSource() }, 68 | ) { 69 | eventCallback.onActions(actionsResolved) 70 | } 71 | ) { 72 | item.toComposable( 73 | resolveAssets, 74 | resolveText, 75 | resolveState, 76 | eventCallback, 77 | Modifier.fillWithBaseParams(item, resolveAssets), 78 | ).invoke() 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/utils/InsetWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.utils 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.foundation.layout.safeContent 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.staticCompositionLocalOf 7 | import androidx.compose.ui.unit.Density 8 | import androidx.compose.ui.unit.LayoutDirection 9 | import com.adapty.ui.AdaptyPaywallInsets 10 | 11 | internal sealed class InsetWrapper { 12 | abstract fun getTop(density: Density): Int 13 | abstract fun getBottom(density: Density): Int 14 | abstract fun getLeft(density: Density, layoutDirection: LayoutDirection): Int 15 | abstract fun getRight(density: Density, layoutDirection: LayoutDirection): Int 16 | 17 | val isCustom get() = this is Custom 18 | 19 | class System(internal val insets: WindowInsets): InsetWrapper() { 20 | override fun getBottom(density: Density): Int { 21 | return insets.getBottom(density) 22 | } 23 | 24 | override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int { 25 | return insets.getLeft(density, layoutDirection) 26 | } 27 | 28 | override fun getRight(density: Density, layoutDirection: LayoutDirection): Int { 29 | return insets.getRight(density, layoutDirection) 30 | } 31 | 32 | override fun getTop(density: Density): Int { 33 | return insets.getTop(density) 34 | } 35 | 36 | override fun equals(other: Any?): Boolean { 37 | if (this === other) return true 38 | if (javaClass != other?.javaClass) return false 39 | 40 | other as System 41 | 42 | return insets == other.insets 43 | } 44 | 45 | override fun hashCode(): Int { 46 | return insets.hashCode() 47 | } 48 | } 49 | 50 | class Custom(internal val insets: AdaptyPaywallInsets): InsetWrapper() { 51 | override fun getBottom(density: Density): Int { 52 | return insets.bottom 53 | } 54 | 55 | override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int { 56 | return if (layoutDirection == LayoutDirection.Rtl) insets.end else insets.start 57 | } 58 | 59 | override fun getRight(density: Density, layoutDirection: LayoutDirection): Int { 60 | return if (layoutDirection == LayoutDirection.Rtl) insets.start else insets.end 61 | } 62 | 63 | override fun getTop(density: Density): Int { 64 | return insets.top 65 | } 66 | 67 | override fun equals(other: Any?): Boolean { 68 | if (this === other) return true 69 | if (javaClass != other?.javaClass) return false 70 | 71 | other as Custom 72 | 73 | return insets == other.insets 74 | } 75 | 76 | override fun hashCode(): Int { 77 | return insets.hashCode() 78 | } 79 | } 80 | } 81 | 82 | internal fun WindowInsets.wrap() = InsetWrapper.System(this) 83 | 84 | internal fun AdaptyPaywallInsets.wrap() = InsetWrapper.Custom(this) 85 | 86 | internal val LocalCustomInsets = staticCompositionLocalOf { 87 | AdaptyPaywallInsets.UNSPECIFIED.wrap() 88 | } 89 | 90 | @Composable 91 | internal fun getInsets() = LocalCustomInsets.current.takeIf { it.insets != AdaptyPaywallInsets.UNSPECIFIED } 92 | ?: WindowInsets.safeContent.wrap() -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/StringWrapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.text 4 | 5 | import androidx.compose.foundation.text.InlineTextContent 6 | import androidx.compose.foundation.text.appendInlineContent 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.AnnotatedString 9 | import androidx.compose.ui.text.SpanStyle 10 | import androidx.compose.ui.text.buildAnnotatedString 11 | import androidx.compose.ui.text.withStyle 12 | import androidx.compose.ui.unit.TextUnit 13 | import androidx.compose.ui.unit.sp 14 | import com.adapty.internal.utils.InternalAdaptyApi 15 | 16 | @InternalAdaptyApi 17 | public sealed class StringWrapper { 18 | internal sealed class Single(val value: String, val attrs: ComposeTextAttrs?): StringWrapper() 19 | internal class Str internal constructor(value: String, attrs: ComposeTextAttrs? = null): Single(value, attrs) 20 | internal class TimerSegmentStr internal constructor( 21 | value: String, 22 | val timerSegment: TimerSegment, 23 | attrs: ComposeTextAttrs? = null, 24 | ) : Single(value, attrs) 25 | 26 | internal class ComplexStr internal constructor(val parts: List): StringWrapper() { 27 | sealed class ComplexStrPart { 28 | class Text(val str: Single): ComplexStrPart() 29 | class Image(val id: String, val inlineContent: InlineTextContent): ComplexStrPart() 30 | } 31 | 32 | fun resolve(): AnnotatedStr { 33 | val inlineContent = mutableMapOf() 34 | val annotatedString = buildAnnotatedString { 35 | parts.forEach { part -> 36 | when (part) { 37 | is ComplexStrPart.Text -> { 38 | append(part.str) 39 | } 40 | is ComplexStrPart.Image -> { 41 | appendInlineContent(part.id, " ") 42 | inlineContent[part.id] = part.inlineContent 43 | } 44 | } 45 | } 46 | } 47 | return AnnotatedStr(annotatedString, inlineContent) 48 | } 49 | } 50 | 51 | internal companion object { 52 | val EMPTY = Str("") 53 | val PRODUCT_NOT_FOUND = Str("") 54 | val CUSTOM_TAG_NOT_FOUND = Str("") 55 | } 56 | } 57 | 58 | internal fun StringWrapper.toPlainString() = 59 | when (this) { 60 | is StringWrapper.Single -> value 61 | is StringWrapper.ComplexStr -> resolve().value.text 62 | } 63 | 64 | internal class AnnotatedStr(val value: AnnotatedString, val inlineContent: Map) 65 | 66 | private fun AnnotatedString.Builder.append(processedItem: StringWrapper.Single) { 67 | if (processedItem.attrs == null) { 68 | append(processedItem.value) 69 | } else { 70 | withStyle(createSpanStyle(processedItem.attrs)) { 71 | append(processedItem.value) 72 | } 73 | } 74 | } 75 | 76 | private fun createSpanStyle(attrs: ComposeTextAttrs): SpanStyle { 77 | return SpanStyle( 78 | color = attrs.textColor ?: Color.Unspecified, 79 | fontSize = attrs.fontSize?.sp ?: TextUnit.Unspecified, 80 | fontFamily = attrs.fontFamily, 81 | background = attrs.backgroundColor ?: Color.Unspecified, 82 | textDecoration = attrs.textDecoration, 83 | ) 84 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/UIElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.internal.utils.InternalAdaptyApi 6 | import com.adapty.models.AdaptyPaywallProduct 7 | import com.adapty.ui.AdaptyUI 8 | import com.adapty.ui.internal.ui.element.Container 9 | import com.adapty.ui.internal.ui.element.UIElement 10 | import com.adapty.ui.internal.ui.element.UnknownElement 11 | import com.adapty.ui.internal.utils.NO_SHRINK 12 | 13 | internal typealias Assets = Map 14 | internal typealias Texts = Map 15 | internal typealias Products = Map 16 | internal typealias StateMap = MutableMap 17 | internal typealias ChildMapper = (item: Map<*, *>) -> UIElement? 18 | internal typealias ChildMapperShrinkable = (item: Map<*, *>, nextInheritShrink: Int) -> UIElement? 19 | 20 | internal interface UIElementMapper { 21 | fun canMap(config: Map<*, *>): Boolean 22 | } 23 | 24 | internal interface UIPlainElementMapper: UIElementMapper { 25 | fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles): UIElement 26 | } 27 | 28 | internal interface UIComplexElementMapper: UIElementMapper { 29 | fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles, stateMap: MutableMap, inheritShrink: Int, childMapper: ChildMapper): UIElement 30 | } 31 | 32 | internal interface UIComplexShrinkableElementMapper: UIElementMapper { 33 | fun map(config: Map<*, *>, assets: Assets, refBundles: ReferenceBundles, stateMap: MutableMap, inheritShrink: Int, childMapper: ChildMapperShrinkable): UIElement 34 | } 35 | 36 | internal class UIElementFactory(private val mappers: List) { 37 | fun createElementTree( 38 | config: Map<*, *>, 39 | assets: Assets, 40 | stateMap: StateMap, 41 | refBundles: ReferenceBundles, 42 | ): UIElement { 43 | return createElement(config, assets, stateMap, refBundles, NO_SHRINK) 44 | } 45 | 46 | private fun createElement( 47 | config: Map<*, *>, 48 | assets: Assets, 49 | stateMap: StateMap, 50 | refBundles: ReferenceBundles, 51 | inheritShrink: Int, 52 | ): UIElement { 53 | val mapper = mappers.find { it.canMap(config) } 54 | return when (mapper) { 55 | is UIPlainElementMapper -> { 56 | mapper.map(config, assets, refBundles) 57 | } 58 | is UIComplexElementMapper -> { 59 | mapper.map(config, assets, refBundles, stateMap, inheritShrink) { childConfig -> 60 | createElement(childConfig, assets, stateMap, refBundles, inheritShrink) 61 | } 62 | } 63 | is UIComplexShrinkableElementMapper -> { 64 | mapper.map(config, assets, refBundles, stateMap, inheritShrink) { childConfig, nextInheritShrink -> 65 | createElement(childConfig, assets, stateMap, refBundles, nextInheritShrink) 66 | } 67 | } 68 | else -> UnknownElement 69 | } 70 | } 71 | } 72 | 73 | internal class ReferenceBundles( 74 | val targetElements: MutableMap, 75 | val awaitingElements: MutableMap>>, 76 | ) { 77 | companion object { 78 | fun create() = ReferenceBundles(mutableMapOf(), mutableMapOf()) 79 | } 80 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/AdaptyPaywallInsets.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui 2 | 3 | /** 4 | * @property[start] Additional left margin in pixels for LTR and right margin for RTL layouts 5 | * @property[top] Additional top margin in pixels. Useful when the status bar overlaps the paywall screen 6 | * @property[end] Additional right margin in pixels for LTR and left margin for RTL layouts 7 | * @property[bottom] Additional bottom margin in pixels. Useful when the navigation bar overlaps the paywall screen 8 | */ 9 | public class AdaptyPaywallInsets private constructor( 10 | public val start: Int, 11 | public val top: Int, 12 | public val end: Int, 13 | public val bottom: Int, 14 | ) { 15 | public companion object { 16 | /** 17 | * @param[start] Additional left margin in pixels for LTR and right margin for RTL layouts 18 | * @param[top] Additional top margin in pixels. Useful when the status bar overlaps the paywall screen 19 | * @param[end] Additional right margin in pixels for LTR and left margin for RTL layouts 20 | * @param[bottom] Additional bottom margin in pixels. Useful when the navigation bar overlaps the paywall screen 21 | */ 22 | @JvmStatic 23 | public fun of(start: Int, top: Int, end: Int, bottom: Int): AdaptyPaywallInsets = 24 | AdaptyPaywallInsets(start, top, end, bottom) 25 | 26 | /** 27 | * @param[top] Additional top margin in pixels. Useful when the status bar overlaps the paywall screen 28 | * @param[bottom] Additional bottom margin in pixels. Useful when the navigation bar overlaps the paywall screen 29 | */ 30 | @JvmStatic 31 | public fun vertical(top: Int, bottom: Int): AdaptyPaywallInsets = 32 | AdaptyPaywallInsets(0, top, 0, bottom) 33 | 34 | /** 35 | * @param[start] Additional left margin in pixels for LTR and right margin for RTL layouts 36 | * @param[end] Additional right margin in pixels for LTR and left margin for RTL layouts 37 | */ 38 | @JvmStatic 39 | public fun horizontal(start: Int, end: Int): AdaptyPaywallInsets = 40 | AdaptyPaywallInsets(start, 0, end, 0) 41 | 42 | /** 43 | * @param[all] Additional margins in pixels 44 | */ 45 | @JvmStatic 46 | public fun of(all: Int): AdaptyPaywallInsets = AdaptyPaywallInsets(all, all, all, all) 47 | 48 | /** 49 | * You can use this field when none of the system bars overlap the paywall screen 50 | */ 51 | @JvmField 52 | public val NONE: AdaptyPaywallInsets = of(0) 53 | 54 | /** 55 | * You can use this field in case the paywall screen is edge-to-edge 56 | * and actual window insets should be applied 57 | */ 58 | @JvmField 59 | public val UNSPECIFIED: AdaptyPaywallInsets = of(-1) 60 | } 61 | 62 | override fun equals(other: Any?): Boolean { 63 | if (this === other) return true 64 | if (other !is AdaptyPaywallInsets) return false 65 | 66 | if (start != other.start) return false 67 | if (top != other.top) return false 68 | if (end != other.end) return false 69 | if (bottom != other.bottom) return false 70 | 71 | return true 72 | } 73 | 74 | override fun hashCode(): Int { 75 | var result = start 76 | result = 31 * result + top 77 | result = 31 * result + end 78 | result = 31 * result + bottom 79 | return result 80 | } 81 | 82 | override fun toString(): String { 83 | return "AdaptyPaywallInsets(start=$start, top=$top, end=$end, bottom=$bottom)" 84 | } 85 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/attributes/PagerAttributeMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.attributes 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.ui.attributes.EdgeEntities 9 | import com.adapty.ui.internal.ui.attributes.InteractionBehavior 10 | import com.adapty.ui.internal.ui.attributes.PageSize 11 | import com.adapty.ui.internal.ui.attributes.PagerAnimation 12 | import com.adapty.ui.internal.ui.attributes.PagerIndicator 13 | import com.adapty.ui.internal.ui.attributes.Shape 14 | import com.adapty.ui.internal.ui.attributes.Transition 15 | import com.adapty.ui.internal.ui.attributes.VerticalAlign 16 | 17 | internal class PagerAttributeMapper( 18 | private val commonAttributeMapper: CommonAttributeMapper, 19 | ) { 20 | 21 | fun mapPagerAnimation(item: Map<*, *>): PagerAnimation { 22 | val startDelay = (item["start_delay"] as? Number)?.toLong() ?: 0L 23 | val afterInteractionDelay = (item["after_interaction_delay"] as? Number)?.toLong() ?: 3000L 24 | val pageTransition = 25 | ((item["page_transition"] as? Map<*, *>)?.let(commonAttributeMapper::mapTransition) as? Transition.Slide) 26 | ?: throw adaptyError( 27 | message = "page_transition is invalid (${item["page_transition"]})", 28 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 29 | ) 30 | val repeatTransition = 31 | (item["repeat_transition"] as? Map<*, *>)?.let(commonAttributeMapper::mapTransition) as? Transition.Slide 32 | 33 | return PagerAnimation(startDelay, afterInteractionDelay, pageTransition, repeatTransition) 34 | } 35 | 36 | fun mapPagerIndicator(item: Map<*, *>): PagerIndicator { 37 | val layout = (item["layout"] as? String).let { layout -> 38 | when (layout) { 39 | "overlaid" -> PagerIndicator.Layout.OVERLAID 40 | else -> PagerIndicator.Layout.STACKED 41 | } 42 | } 43 | val vAlign = commonAttributeMapper.mapVerticalAlign(item["v_align"], default = VerticalAlign.BOTTOM) 44 | val dotSize = (item["dot_size"] as? Number)?.toFloat() ?: 6f 45 | val spacing = (item["spacing"] as? Number)?.toFloat() ?: 6f 46 | val padding = item["padding"]?.let(commonAttributeMapper::mapEdgeEntities) ?: EdgeEntities(6f) 47 | val color = (item["color"] as? String)?.let { assetId -> Shape.Fill(assetId) } 48 | val selectedColor = (item["selected_color"] as? String)?.let { assetId -> Shape.Fill(assetId) } 49 | 50 | return PagerIndicator(layout, vAlign, padding, dotSize, spacing, color, selectedColor) 51 | } 52 | 53 | fun mapInteractionBehavior(item: Any?): InteractionBehavior { 54 | return when(item) { 55 | "none" -> InteractionBehavior.NONE 56 | "cancel_animation" -> InteractionBehavior.CANCEL_ANIMATION 57 | else -> InteractionBehavior.PAUSE_ANIMATION 58 | } 59 | } 60 | 61 | fun mapPageSize(item: Any?): PageSize { 62 | when (item) { 63 | is Map<*, *> -> { 64 | val parent = item["parent"] as? Number 65 | if (parent != null) 66 | return PageSize.PageFraction(parent.toFloat()) 67 | return PageSize.Unit(commonAttributeMapper.mapDimUnit(item)) 68 | } 69 | else -> return item?.let(commonAttributeMapper::mapDimUnit)?.let { PageSize.Unit(it) } 70 | ?: PageSize.PageFraction(1f) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ This repository is deprecated because AdaptyUI is now part of [AdaptySDK](https://github.com/adaptyteam/AdaptySDK-Android). Please consider updating. ⚠️ 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |
Adapty UI 10 |

11 | 12 |

13 | 14 | 15 | 16 |

17 | 18 | **AdaptyUI** is an open-source framework that is an extension to the Adapty SDK that allows you to easily add purchase screens to your application. It’s 100% open-source, native, and lightweight. 19 | 20 | ### [1. Fetching Paywalls & ViewConfiguration](https://docs.adapty.io/docs/paywall-builder-fetching) 21 | 22 | Paywall can be obtained in the way you are already familiar with: 23 | 24 | ```kotlin 25 | Adapty.getPaywall("YOUR_PAYWALL_ID") { result -> 26 | when (result) { 27 | is AdaptyResult.Success -> { 28 | val paywall = result.value 29 | // the requested paywall 30 | } 31 | is AdaptyResult.Error -> { 32 | val error = result.error 33 | // handle the error 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | After fetching the paywall call the `Adapty.getViewConfiguration(paywall, locale)` method to load the view configuration: 40 | 41 | ```kotlin 42 | Adapty.getViewConfiguration(paywall, locale) { result -> 43 | when(result) { 44 | is AdaptyResult.Success -> { 45 | val viewConfiguration = result.value 46 | // use loaded configuration 47 | } 48 | is AdaptyResult.Error -> { 49 | val error = result.error 50 | // handle the error 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | ### [2. Presenting Visual Paywalls](https://docs.adapty.io/docs/paywall-builder-presenting-android) 57 | 58 | In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: 59 | 60 | ```kotlin 61 | val paywallView = AdaptyUI.getPaywallView( 62 | activity, 63 | paywall, 64 | products, 65 | viewConfiguration, 66 | AdaptyPaywallInsets.of(topInset, bottomInset), 67 | eventListener, 68 | ) 69 | 70 | //======= OR ======= 71 | 72 | val paywallView = 73 | AdaptyPaywallView(activity) // or retrieve it from xml 74 | ... 75 | with(paywallView) { 76 | setEventListener(eventListener) 77 | showPaywall( 78 | paywall, 79 | products, 80 | viewConfiguration, 81 | AdaptyPaywallInsets.of(topInset, bottomInset), 82 | ) 83 | } 84 | 85 | ``` 86 | 87 | After the object has been successfully created, you can add it to the view hierarchy and display on the screen of the device. 88 | 89 | ### 3. Full Documentation and Next Steps 90 | 91 | We recommend that you read the [full documentation](https://docs.adapty.io/docs/paywall-builder-getting-started). If you are not familiar with Adapty, then start [here](https://docs.adapty.io/docs). 92 | 93 | ## Contributing 94 | 95 | - Feel free to open an issue, we check all of them or drop us an email at [support@adapty.io](mailto:support@adapty.io) and tell us everything you want. 96 | - Want to suggest a feature? Just contact us or open an issue in the repo. 97 | 98 | ## Like AdaptyUI? 99 | 100 | So do we! Feel free to star the repo ⭐️⭐️⭐️ and make our developers happy! 101 | 102 | ## License 103 | 104 | AdaptyUI is available under the MIT license. [Click here](https://github.com/adaptyteam/AdaptyUI-Android/blob/main/LICENSE) for details. 105 | 106 | --- 107 | -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/element/BaseUIElementMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.element 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.mapping.attributes.CommonAttributeMapper 9 | import com.adapty.ui.internal.ui.attributes.DimSpec 10 | import com.adapty.ui.internal.ui.element.BaseProps 11 | import com.adapty.ui.internal.ui.element.Container 12 | import com.adapty.ui.internal.ui.element.MultiContainer 13 | import com.adapty.ui.internal.ui.element.ReferenceElement 14 | import com.adapty.ui.internal.ui.element.SingleContainer 15 | import com.adapty.ui.internal.ui.element.UIElement 16 | 17 | internal abstract class BaseUIElementMapper( 18 | private val elementTypeStr: String, 19 | protected val commonAttributeMapper: CommonAttributeMapper, 20 | ): UIElementMapper { 21 | 22 | override fun canMap(config: Map<*, *>) = config["type"] == elementTypeStr 23 | 24 | protected fun Map<*, *>.extractBaseProps(): BaseProps { 25 | return BaseProps( 26 | this["width"]?.let { item -> commonAttributeMapper.mapDimSpec(item, DimSpec.Axis.X) }, 27 | this["height"]?.let { item -> commonAttributeMapper.mapDimSpec(item, DimSpec.Axis.Y) }, 28 | this["weight"]?.toFloatOrNull(), 29 | this["decorator"]?.let(commonAttributeMapper::mapShape), 30 | this["padding"]?.let(commonAttributeMapper::mapEdgeEntities), 31 | this["offset"]?.let(commonAttributeMapper::mapOffset), 32 | (this["visibility"] as? Boolean) ?: true, 33 | (this["transition_in"] as? List<*>)?.mapNotNull { item -> 34 | (item as? Map<*, *>)?.let(commonAttributeMapper::mapTransition) 35 | }?.takeIf { it.isNotEmpty() } 36 | ?: (this["transition_in"] as? Map<*, *>)?.let(commonAttributeMapper::mapTransition)?.let { listOf(it) } 37 | ) 38 | } 39 | 40 | protected fun Any.toFloatOrNull() = (this as? Number)?.toFloat() 41 | 42 | protected fun Map<*, *>.extractSpacingOrNull(): Float? = 43 | this["spacing"]?.toFloatOrNull()?.takeIf { it > 0f } 44 | 45 | protected fun addToReferenceTargetsIfNeeded( 46 | rawElement: Map<*, *>, 47 | element: UIElement, 48 | refBundles: ReferenceBundles, 49 | ) { 50 | (rawElement["element_id"] as? String)?.let { elementId -> 51 | refBundles.targetElements[elementId] = element 52 | 53 | processThoseAwaitingReferences(elementId, element, refBundles.awaitingElements) 54 | } 55 | } 56 | 57 | private fun processThoseAwaitingReferences( 58 | elementId: String, 59 | actualElement: UIElement, 60 | referenceAwaitingMap: MutableMap>>, 61 | ) { 62 | referenceAwaitingMap.remove(elementId)?.forEach { container -> 63 | when (container) { 64 | is SingleContainer -> { 65 | container.content = actualElement 66 | } 67 | is MultiContainer -> { 68 | container.content = container.content.map { item -> 69 | if (item is ReferenceElement && item.id == elementId) 70 | actualElement 71 | else 72 | item 73 | } 74 | } 75 | else -> Unit 76 | } 77 | } 78 | } 79 | 80 | protected fun addToAwaitingReferencesIfNeeded( 81 | referenceIds: Iterable, 82 | container: Container<*>, 83 | referenceAwaitingMap: MutableMap>>, 84 | ) { 85 | referenceIds.forEach { id -> 86 | referenceAwaitingMap.getOrPut(id) { mutableListOf() }.add(container) 87 | } 88 | } 89 | 90 | protected fun checkAsset(assetId: String, assets: Assets) { 91 | if (assets[assetId] == null) 92 | throw adaptyError( 93 | message = "asset_id ($assetId) does not exist", 94 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 95 | ) 96 | } 97 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/AdaptyPaywallScreen.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.lifecycle.viewmodel.compose.viewModel 7 | import com.adapty.models.AdaptyPaywallProduct 8 | import com.adapty.ui.internal.ui.AdaptyPaywallInternal 9 | import com.adapty.ui.internal.ui.PaywallViewModel 10 | import com.adapty.ui.internal.ui.PaywallViewModelArgs 11 | import com.adapty.ui.internal.ui.PaywallViewModelFactory 12 | import com.adapty.ui.internal.ui.UserArgs 13 | import com.adapty.ui.internal.utils.ProductLoadingFailureCallback 14 | import com.adapty.ui.internal.utils.getCurrentLocale 15 | import com.adapty.ui.listeners.AdaptyUiDefaultEventListener 16 | import com.adapty.ui.listeners.AdaptyUiEventListener 17 | import com.adapty.ui.listeners.AdaptyUiObserverModeHandler 18 | import com.adapty.ui.listeners.AdaptyUiPersonalizedOfferResolver 19 | import com.adapty.ui.listeners.AdaptyUiTagResolver 20 | import com.adapty.ui.listeners.AdaptyUiTimerResolver 21 | import java.util.UUID 22 | 23 | /** 24 | * Paywall screen composable representation 25 | * 26 | * @param[viewConfiguration] An [AdaptyUI.LocalizedViewConfiguration] object containing information 27 | * about the visual part of the paywall. To load it, use the [AdaptyUI.getViewConfiguration] method. 28 | * 29 | * @param[products] Optional [AdaptyPaywallProduct] list. Pass this value in order to optimize 30 | * the display time of the products on the screen. If you pass `null`, `AdaptyUI` will 31 | * automatically fetch the required products. 32 | * 33 | * @param[eventListener] An object that implements the [AdaptyUiEventListener] interface. 34 | * Use it to respond to different events happening inside the purchase screen. 35 | * Also you can extend [AdaptyUiDefaultEventListener] so you don't need to override all the methods. 36 | * 37 | * @param[insets] You can override the default window inset handling by specifying the [AdaptyPaywallInsets]. 38 | * 39 | * @param[personalizedOfferResolver] In case you want to indicate whether the price is personalized ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price)), 40 | * you can implement [AdaptyUiPersonalizedOfferResolver] and pass your own logic 41 | * that maps [AdaptyPaywallProduct] to `true`, if the price of the product is personalized, otherwise `false`. 42 | * 43 | * @param[tagResolver] If you are going to use custom tags functionality, pass the resolver function here. 44 | * 45 | * @param[timerResolver] If you are going to use custom timer functionality, pass the resolver function here. 46 | * 47 | * @param[observerModeHandler] If you use Adapty in [Observer mode](https://adapty.io/docs/observer-vs-full-mode), 48 | * pass the [AdaptyUiObserverModeHandler] implementation to handle purchases on your own. 49 | */ 50 | @Composable 51 | public fun AdaptyPaywallScreen( 52 | viewConfiguration: AdaptyUI.LocalizedViewConfiguration, 53 | products: List?, 54 | eventListener: AdaptyUiEventListener, 55 | insets: AdaptyPaywallInsets = AdaptyPaywallInsets.UNSPECIFIED, 56 | personalizedOfferResolver: AdaptyUiPersonalizedOfferResolver = AdaptyUiPersonalizedOfferResolver.DEFAULT, 57 | tagResolver: AdaptyUiTagResolver = AdaptyUiTagResolver.DEFAULT, 58 | timerResolver: AdaptyUiTimerResolver = AdaptyUiTimerResolver.DEFAULT, 59 | observerModeHandler: AdaptyUiObserverModeHandler? = null, 60 | ) { 61 | val context = LocalContext.current 62 | val vmArgs = remember { 63 | val userArgs = UserArgs.create( 64 | viewConfiguration, 65 | eventListener, 66 | insets, 67 | personalizedOfferResolver, 68 | tagResolver, 69 | timerResolver, 70 | observerModeHandler, 71 | products, 72 | ProductLoadingFailureCallback { error -> eventListener.onLoadingProductsFailure(error, context) }, 73 | ) 74 | PaywallViewModelArgs.create( 75 | "${UUID.randomUUID().toString().hashCode()}", 76 | userArgs, 77 | context.getCurrentLocale(), 78 | ) 79 | } ?: return 80 | 81 | val viewModel: PaywallViewModel = viewModel( 82 | factory = PaywallViewModelFactory(vmArgs) 83 | ) 84 | AdaptyPaywallInternal(viewModel) 85 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/GridItem.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import com.adapty.internal.utils.InternalAdaptyApi 12 | import com.adapty.ui.internal.mapping.element.Assets 13 | import com.adapty.ui.internal.text.StringId 14 | import com.adapty.ui.internal.text.StringWrapper 15 | import com.adapty.ui.internal.ui.attributes.Align 16 | import com.adapty.ui.internal.ui.attributes.DimSpec 17 | import com.adapty.ui.internal.ui.attributes.toComposeAlignment 18 | import com.adapty.ui.internal.ui.fillWithBaseParams 19 | import com.adapty.ui.internal.utils.EventCallback 20 | 21 | @InternalAdaptyApi 22 | public class GridItem internal constructor( 23 | dimAxis: DimSpec.Axis, 24 | sideSpec: DimSpec?, 25 | override var content: UIElement, 26 | internal val align: Align, 27 | baseProps: BaseProps, 28 | ) : UIElement, SingleContainer { 29 | 30 | override val baseProps: BaseProps = 31 | if (dimAxis == DimSpec.Axis.X) 32 | baseProps.copy(widthSpec = sideSpec) 33 | else 34 | baseProps.copy(heightSpec = sideSpec) 35 | 36 | override fun toComposable( 37 | resolveAssets: () -> Assets, 38 | resolveText: @Composable (StringId) -> StringWrapper?, 39 | resolveState: () -> Map, 40 | eventCallback: EventCallback, 41 | modifier: Modifier, 42 | ): @Composable () -> Unit = { 43 | Box( 44 | contentAlignment = align.toComposeAlignment(), 45 | modifier = modifier.fillMaxSize(), 46 | ) { 47 | content.toComposable( 48 | resolveAssets, 49 | resolveText, 50 | resolveState, 51 | eventCallback, 52 | Modifier.fillWithBaseParams(content, resolveAssets), 53 | ).invoke() 54 | } 55 | } 56 | 57 | override fun ColumnScope.toComposableInColumn( 58 | resolveAssets: () -> Assets, 59 | resolveText: @Composable (StringId) -> StringWrapper?, 60 | resolveState: () -> Map, 61 | eventCallback: EventCallback, 62 | modifier: Modifier, 63 | ): @Composable () -> Unit = { 64 | Box( 65 | contentAlignment = align.toComposeAlignment(), 66 | modifier = modifier.fillMaxWidth(), 67 | ) { 68 | content.run { 69 | this@toComposableInColumn.toComposableInColumn( 70 | resolveAssets, 71 | resolveText, 72 | resolveState, 73 | eventCallback, 74 | this@toComposableInColumn.fillModifierWithScopedParams( 75 | content, 76 | Modifier.fillWithBaseParams(content, resolveAssets) 77 | ), 78 | ).invoke() 79 | } 80 | } 81 | } 82 | 83 | override fun RowScope.toComposableInRow( 84 | resolveAssets: () -> Assets, 85 | resolveText: @Composable (StringId) -> StringWrapper?, 86 | resolveState: () -> Map, 87 | eventCallback: EventCallback, 88 | modifier: Modifier, 89 | ): @Composable () -> Unit = { 90 | Box( 91 | contentAlignment = align.toComposeAlignment(), 92 | modifier = modifier.fillMaxHeight(), 93 | ) { 94 | content.run { 95 | this@toComposableInRow.toComposableInRow( 96 | resolveAssets, 97 | resolveText, 98 | resolveState, 99 | eventCallback, 100 | this@toComposableInRow.fillModifierWithScopedParams( 101 | content, 102 | Modifier.fillWithBaseParams(content, resolveAssets) 103 | ), 104 | ).invoke() 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/Align.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import androidx.compose.ui.AbsoluteAlignment 6 | import androidx.compose.ui.Alignment 7 | import com.adapty.errors.AdaptyErrorCode 8 | import com.adapty.internal.utils.InternalAdaptyApi 9 | import com.adapty.internal.utils.adaptyError 10 | 11 | internal enum class VerticalAlign(internal val intValue: Int) { 12 | CENTER(0b0), 13 | TOP(0b1), 14 | BOTTOM(0b10), 15 | } 16 | 17 | internal enum class HorizontalAlign(internal val intValue: Int) { 18 | CENTER(0b100), 19 | START(0b1000), 20 | LEFT(0b1100), 21 | END(0b10000), 22 | RIGHT(0b10100), 23 | } 24 | 25 | internal enum class Align(internal val intValue: Int) { 26 | TOP_START(VerticalAlign.TOP.intValue or HorizontalAlign.START.intValue), 27 | TOP_CENTER(VerticalAlign.TOP.intValue or HorizontalAlign.CENTER.intValue), 28 | TOP_END(VerticalAlign.TOP.intValue or HorizontalAlign.END.intValue), 29 | CENTER_START(VerticalAlign.CENTER.intValue or HorizontalAlign.START.intValue), 30 | CENTER(VerticalAlign.CENTER.intValue or HorizontalAlign.CENTER.intValue), 31 | CENTER_END(VerticalAlign.CENTER.intValue or HorizontalAlign.END.intValue), 32 | BOTTOM_START(VerticalAlign.BOTTOM.intValue or HorizontalAlign.START.intValue), 33 | BOTTOM_CENTER(VerticalAlign.BOTTOM.intValue or HorizontalAlign.CENTER.intValue), 34 | BOTTOM_END(VerticalAlign.BOTTOM.intValue or HorizontalAlign.END.intValue), 35 | TOP_LEFT(VerticalAlign.TOP.intValue or HorizontalAlign.LEFT.intValue), 36 | TOP_RIGHT(VerticalAlign.TOP.intValue or HorizontalAlign.RIGHT.intValue), 37 | CENTER_LEFT(VerticalAlign.CENTER.intValue or HorizontalAlign.LEFT.intValue), 38 | CENTER_RIGHT(VerticalAlign.CENTER.intValue or HorizontalAlign.RIGHT.intValue), 39 | BOTTOM_LEFT(VerticalAlign.BOTTOM.intValue or HorizontalAlign.LEFT.intValue), 40 | BOTTOM_RIGHT(VerticalAlign.BOTTOM.intValue or HorizontalAlign.RIGHT.intValue); 41 | 42 | companion object { 43 | fun getOrNull(intValue: Int) = values().firstOrNull { it.intValue == intValue } 44 | } 45 | } 46 | 47 | internal operator fun HorizontalAlign.plus(other: VerticalAlign): Align { 48 | return Align.getOrNull(this.intValue or other.intValue) 49 | ?: throw adaptyError( 50 | message = "Can't find composite alignment from ${this.name} (${this.intValue}) and ${other.name} (${other.intValue})", 51 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 52 | ) 53 | } 54 | 55 | internal operator fun VerticalAlign.plus(other: HorizontalAlign): Align { 56 | return Align.getOrNull(this.intValue or other.intValue) 57 | ?: throw adaptyError( 58 | message = "Can't find composite alignment from ${this.name} (${this.intValue}) and ${other.name} (${other.intValue})", 59 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 60 | ) 61 | } 62 | 63 | internal fun VerticalAlign.toComposeAlignment(): Alignment.Vertical { 64 | return when(this) { 65 | VerticalAlign.CENTER -> Alignment.CenterVertically 66 | VerticalAlign.TOP -> Alignment.Top 67 | VerticalAlign.BOTTOM -> Alignment.Bottom 68 | } 69 | } 70 | 71 | internal fun HorizontalAlign.toComposeAlignment(): Alignment.Horizontal { 72 | return when(this) { 73 | HorizontalAlign.CENTER -> Alignment.CenterHorizontally 74 | HorizontalAlign.START -> Alignment.Start 75 | HorizontalAlign.END -> Alignment.End 76 | HorizontalAlign.LEFT -> AbsoluteAlignment.Left 77 | HorizontalAlign.RIGHT -> AbsoluteAlignment.Right 78 | } 79 | } 80 | 81 | internal fun Align.toComposeAlignment(): Alignment { 82 | return when(this) { 83 | Align.CENTER -> Alignment.Center 84 | Align.CENTER_START -> Alignment.CenterStart 85 | Align.CENTER_END -> Alignment.CenterEnd 86 | Align.TOP_START -> Alignment.TopStart 87 | Align.TOP_CENTER -> Alignment.TopCenter 88 | Align.TOP_END -> Alignment.TopEnd 89 | Align.BOTTOM_START -> Alignment.BottomStart 90 | Align.BOTTOM_CENTER -> Alignment.BottomCenter 91 | Align.BOTTOM_END -> Alignment.BottomEnd 92 | Align.CENTER_LEFT -> AbsoluteAlignment.CenterLeft 93 | Align.CENTER_RIGHT -> AbsoluteAlignment.CenterRight 94 | Align.TOP_LEFT -> AbsoluteAlignment.TopLeft 95 | Align.TOP_RIGHT -> AbsoluteAlignment.TopRight 96 | Align.BOTTOM_LEFT -> AbsoluteAlignment.BottomLeft 97 | Align.BOTTOM_RIGHT -> AbsoluteAlignment.BottomRight 98 | } 99 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/element/SectionElement.kt: -------------------------------------------------------------------------------- 1 | package com.adapty.ui.internal.ui.element 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | import com.adapty.internal.utils.InternalAdaptyApi 11 | import com.adapty.ui.internal.mapping.element.Assets 12 | import com.adapty.ui.internal.text.StringId 13 | import com.adapty.ui.internal.text.StringWrapper 14 | import com.adapty.ui.internal.ui.fillWithBaseParams 15 | import com.adapty.ui.internal.utils.EventCallback 16 | 17 | @InternalAdaptyApi 18 | public class SectionElement internal constructor( 19 | internal val id: String, 20 | internal val index: Int, 21 | override var content: List, 22 | ): UIElement, MultiContainer { 23 | override val baseProps: BaseProps = BaseProps.EMPTY 24 | 25 | internal val key get() = getKey(id) 26 | 27 | internal companion object { 28 | fun getKey(sectionId: String) = "section_$sectionId" 29 | } 30 | 31 | override fun toComposable( 32 | resolveAssets: () -> Assets, 33 | resolveText: @Composable (StringId) -> StringWrapper?, 34 | resolveState: () -> Map, 35 | eventCallback: EventCallback, 36 | modifier: Modifier, 37 | ): @Composable () -> Unit { 38 | return { 39 | renderSection(resolveState) { currentIndex -> 40 | content[currentIndex].run { 41 | toComposable( 42 | resolveAssets, 43 | resolveText, 44 | resolveState, 45 | eventCallback, 46 | Modifier.fillWithBaseParams(this, resolveAssets) 47 | ).invoke() 48 | } 49 | } 50 | } 51 | } 52 | 53 | override fun ColumnScope.toComposableInColumn( 54 | resolveAssets: () -> Assets, 55 | resolveText: @Composable (StringId) -> StringWrapper?, 56 | resolveState: () -> Map, 57 | eventCallback: EventCallback, 58 | modifier: Modifier 59 | ): @Composable () -> Unit { 60 | return { 61 | renderSection(resolveState) { currentIndex -> 62 | content[currentIndex].run { 63 | this@toComposableInColumn.toComposableInColumn( 64 | resolveAssets, 65 | resolveText, 66 | resolveState, 67 | eventCallback, 68 | fillModifierWithScopedParams( 69 | this, 70 | Modifier.fillWithBaseParams(this, resolveAssets), 71 | ), 72 | ).invoke() 73 | } 74 | } 75 | } 76 | } 77 | 78 | override fun RowScope.toComposableInRow( 79 | resolveAssets: () -> Assets, 80 | resolveText: @Composable (StringId) -> StringWrapper?, 81 | resolveState: () -> Map, 82 | eventCallback: EventCallback, 83 | modifier: Modifier, 84 | ): @Composable () -> Unit { 85 | return { 86 | renderSection(resolveState) { currentIndex -> 87 | content[currentIndex].run { 88 | this@toComposableInRow.toComposableInRow( 89 | resolveAssets, 90 | resolveText, 91 | resolveState, 92 | eventCallback, 93 | fillModifierWithScopedParams( 94 | this, 95 | Modifier.fillWithBaseParams(this, resolveAssets), 96 | ), 97 | ).invoke() 98 | } 99 | } 100 | } 101 | } 102 | 103 | @Composable 104 | private fun renderSection( 105 | resolveState: () -> Map, 106 | renderChild: @Composable (currentIndex: Int) -> Unit, 107 | ) { 108 | val state = resolveState() 109 | val currentIndex by remember { 110 | derivedStateOf { (state[key] as? Int) ?: index } 111 | } 112 | if (currentIndex in content.indices) 113 | renderChild(currentIndex) 114 | } 115 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/text/ComposeTextAttrs.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.text 4 | 5 | import android.content.Context 6 | import androidx.annotation.ColorInt 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.text.font.FontFamily 11 | import androidx.compose.ui.text.style.TextDecoration 12 | import com.adapty.internal.utils.InternalAdaptyApi 13 | import com.adapty.ui.AdaptyUI 14 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.RichText 15 | import com.adapty.ui.internal.mapping.element.Assets 16 | import com.adapty.ui.internal.ui.element.BaseTextElement 17 | 18 | internal class ComposeTextAttrs( 19 | val textColor: Color?, 20 | val backgroundColor: Color?, 21 | val fontSize: Float?, 22 | val textDecoration: TextDecoration?, 23 | val fontFamily: FontFamily?, 24 | ) { 25 | companion object { 26 | @Composable 27 | fun from(attrs: RichText.Attributes, assets: Assets): ComposeTextAttrs { 28 | return from( 29 | textColorAssetId = attrs.textColorAssetId, 30 | backgroundColorAssetId = attrs.backgroundAssetId, 31 | fontAssetId = attrs.fontAssetId, 32 | fontSize = attrs.size, 33 | underline = attrs.underline, 34 | strikethrough = attrs.strikethrough, 35 | assets = assets, 36 | ) 37 | } 38 | 39 | @Composable 40 | fun from(elementAttrs: BaseTextElement.Attributes, assets: Assets): ComposeTextAttrs { 41 | return from( 42 | textColorAssetId = elementAttrs.textColor?.assetId, 43 | backgroundColorAssetId = elementAttrs.background?.assetId, 44 | fontAssetId = elementAttrs.fontId, 45 | fontSize = elementAttrs.fontSize, 46 | underline = elementAttrs.underline, 47 | strikethrough = elementAttrs.strikethrough, 48 | assets = assets, 49 | ) 50 | } 51 | 52 | @Composable 53 | private fun from( 54 | textColorAssetId: String?, 55 | backgroundColorAssetId: String?, 56 | fontAssetId: String?, 57 | fontSize: Float?, 58 | underline: Boolean, 59 | strikethrough: Boolean, 60 | assets: Assets, 61 | ): ComposeTextAttrs { 62 | val context = LocalContext.current 63 | val fontAsset = resolveFontAsset(fontAssetId, assets) 64 | return ComposeTextAttrs( 65 | resolveColorAsset(textColorAssetId, assets) ?: resolveColor(fontAsset?.color), 66 | resolveColorAsset(backgroundColorAssetId, assets), 67 | fontSize ?: fontAsset?.size, 68 | resolveTextDecoration(underline, strikethrough), 69 | resolveFontFamily(fontAsset, context), 70 | ) 71 | } 72 | 73 | private fun resolveColorAsset(assetId: String?, assets: Assets): Color? { 74 | return assetId 75 | ?.let { assets[assetId] as? AdaptyUI.LocalizedViewConfiguration.Asset.Color } 76 | ?.let { asset -> Color(asset.value) } 77 | } 78 | 79 | private fun resolveColor(@ColorInt color: Int?): Color? { 80 | return color?.let { Color(color) } 81 | } 82 | 83 | private fun resolveTextDecoration(underline: Boolean, strikethrough: Boolean): TextDecoration? { 84 | val textDecorations = listOfNotNull( 85 | underline.takeIf { it }?.let { TextDecoration.Underline }, 86 | strikethrough.takeIf { it }?.let { TextDecoration.LineThrough }, 87 | ) 88 | return when(textDecorations.size) { 89 | 0 -> null 90 | 1 -> textDecorations.first() 91 | else -> TextDecoration.combine(textDecorations) 92 | } 93 | } 94 | 95 | private fun resolveFontAsset(assetId: String?, assets: Assets): AdaptyUI.LocalizedViewConfiguration.Asset.Font? { 96 | return assetId 97 | ?.let { assets[assetId] as? AdaptyUI.LocalizedViewConfiguration.Asset.Font } 98 | } 99 | 100 | private fun resolveFontFamily(font: AdaptyUI.LocalizedViewConfiguration.Asset.Font?, context: Context): FontFamily? { 101 | return font?.let { FontFamily(TypefaceHolder.getOrPut(context, font)) } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/ui/attributes/Shape.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.ui.attributes 4 | 5 | import android.graphics.Bitmap 6 | import android.graphics.Matrix 7 | import android.graphics.Paint 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.geometry.Size 12 | import androidx.compose.ui.graphics.Brush 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.RectangleShape 15 | import androidx.compose.ui.platform.LocalDensity 16 | import androidx.compose.ui.unit.dp 17 | import com.adapty.internal.utils.InternalAdaptyApi 18 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.Asset 19 | import com.adapty.ui.internal.ui.CircleShape 20 | import com.adapty.ui.internal.ui.RectWithArcShape 21 | import com.adapty.ui.internal.utils.getBitmap 22 | import kotlin.math.roundToInt 23 | 24 | @InternalAdaptyApi 25 | public class Shape internal constructor( 26 | internal val fill: Fill?, 27 | internal val type: Type, 28 | internal val border: Border?, 29 | ) { 30 | internal class Fill(val assetId: String) 31 | 32 | public sealed class Type { 33 | public class Rectangle internal constructor(internal val cornerRadius: CornerRadius?): Type() 34 | public object Circle: Type() 35 | public class RectWithArc internal constructor(internal val arcHeight: Float): Type() { 36 | internal companion object { 37 | const val ABS_ARC_HEIGHT = 32f 38 | } 39 | } 40 | } 41 | 42 | internal class CornerRadius( 43 | topLeft: Float, 44 | topRight: Float, 45 | bottomRight: Float, 46 | bottomLeft: Float, 47 | ) { 48 | val topLeft = topLeft * MULT 49 | val topRight = topRight * MULT 50 | val bottomRight = bottomRight * MULT 51 | val bottomLeft = bottomLeft * MULT 52 | 53 | constructor(value: Float): this(value, value, value, value) 54 | 55 | private companion object { 56 | const val MULT = 2 57 | } 58 | } 59 | 60 | internal class Border( 61 | val color: String, 62 | val shapeType: Type, 63 | val thickness: Float, 64 | ) 65 | 66 | internal companion object { 67 | fun plain(assetId: String) = 68 | Shape(fill = Fill(assetId), type = Type.Rectangle(null), border = null) 69 | } 70 | } 71 | 72 | internal sealed class ComposeFill { 73 | class Color(val color: androidx.compose.ui.graphics.Color): ComposeFill() 74 | class Gradient(val shader: Brush): ComposeFill() 75 | class Image(val image: Bitmap, val matrix: Matrix, val paint: Paint): ComposeFill() 76 | } 77 | 78 | internal fun Asset.Color.toComposeFill(): ComposeFill.Color { 79 | return ComposeFill.Color(Color(this.value)) 80 | } 81 | 82 | internal fun Asset.Gradient.toComposeFill(): ComposeFill.Gradient { 83 | val colorStops = this.values.map { (point, color) -> point to Color(color.value) }.toTypedArray() 84 | val (x0, y0, x1, y1) = this.points 85 | val shader = when (this.type) { 86 | Asset.Gradient.Type.LINEAR -> Brush.linearGradient( 87 | colorStops = *colorStops, 88 | start = Offset(x = x0, y = y0), 89 | end = Offset(x = x1, y = y1), 90 | ) 91 | Asset.Gradient.Type.RADIAL -> Brush.radialGradient(*colorStops) 92 | Asset.Gradient.Type.CONIC -> Brush.sweepGradient(*colorStops) 93 | } 94 | return ComposeFill.Gradient(shader) 95 | } 96 | 97 | internal fun Asset.Image.toComposeFill(size: Size): ComposeFill.Image? { 98 | if (!(size.width > 0 && size.height > 0)) 99 | return null 100 | 101 | val image = getBitmap(this, size.width.roundToInt(), size.height.roundToInt(), Asset.Image.ScaleType.FIT_MAX) 102 | ?: return null 103 | 104 | if (!(image.width > 0 && image.height > 0)) 105 | return null 106 | 107 | val paint = Paint() 108 | val matrix = Matrix() 109 | 110 | val scale = kotlin.math.max( 111 | size.width / image.width, 112 | size.height / image.height 113 | ) 114 | 115 | matrix.reset() 116 | matrix.setScale(scale, scale) 117 | matrix.postTranslate( 118 | (size.width - image.width * scale) / 2f, 119 | 0f, 120 | ) 121 | 122 | return ComposeFill.Image(image, matrix, paint) 123 | } 124 | 125 | @Composable 126 | internal fun Shape.Type.toComposeShape(): androidx.compose.ui.graphics.Shape { 127 | return when (this) { 128 | is Shape.Type.Circle -> CircleShape 129 | is Shape.Type.RectWithArc -> RectWithArcShape(with(LocalDensity.current) { arcHeight.dp.toPx() }) 130 | is Shape.Type.Rectangle -> { 131 | val radius = cornerRadius 132 | if (radius != null) 133 | RoundedCornerShape(radius.topLeft, radius.topRight, radius.bottomRight, radius.bottomLeft) 134 | else 135 | RectangleShape 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/viewconfig/ViewConfigurationTextMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.viewconfig 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.RichText 9 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration.TextItem 10 | import com.adapty.ui.internal.utils.getAs 11 | 12 | internal class ViewConfigurationTextMapper { 13 | 14 | private companion object { 15 | const val LOCALIZATIONS = "localizations" 16 | const val ID = "id" 17 | const val VALUE = "value" 18 | const val TEXT = "text" 19 | const val TAG = "tag" 20 | const val IMAGE = "image" 21 | const val ATTRS = "attributes" 22 | const val FONT = "font" 23 | const val SIZE = "size" 24 | const val STRIKE = "strike" 25 | const val UNDERLINE = "underline" 26 | const val COLOR = "color" 27 | const val BACKGROUND = "background" 28 | const val TINT = "tint" 29 | const val STRINGS = "strings" 30 | const val FALLBACK = "fallback" 31 | } 32 | 33 | fun map(config: JsonObject, localesOrderedDesc: Set): Map { 34 | val rawTextItems = mutableMapOf() 35 | localesOrderedDesc.forEach { locale -> 36 | config.getAs(LOCALIZATIONS) 37 | ?.firstOrNull { it.getAs(ID) == locale } 38 | ?.getAs(STRINGS) 39 | ?.mapNotNull { rawTextItem -> 40 | rawTextItem.getAs(ID) 41 | ?.let { id -> id to rawTextItem } 42 | } 43 | ?.toMap() 44 | ?.let { localizedRawTextItems -> 45 | rawTextItems.putAll(localizedRawTextItems) 46 | } 47 | } 48 | 49 | return rawTextItems.mapNotNull { (id, rawTextItem) -> 50 | id to mapTextItem(rawTextItem) 51 | }.toMap() 52 | } 53 | 54 | private fun mapTextItem(rawTextItem: JsonObject): TextItem { 55 | val textItemId = rawTextItem.getAs(ID) 56 | val textItemValue = rawTextItem.getAsRichText(VALUE) 57 | if (textItemId == null || textItemValue == null) { 58 | throw adaptyError( 59 | message = "id and value in strings in Localization should not be null", 60 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 61 | ) 62 | } 63 | return TextItem( 64 | textItemValue, 65 | rawTextItem.getAsRichText(FALLBACK), 66 | ) 67 | } 68 | 69 | private fun JsonObject.getAsRichText(key: String) = 70 | getAs>(key)?.let(::mapRichText) 71 | ?: getAs(key)?.let(::mapRichText) 72 | ?: getAs(key)?.let { item -> RichText(mapRichTextItem(item)) } 73 | 74 | private fun mapRichText(rawRichText: Iterable<*>): RichText? { 75 | val richTextItems = rawRichText.mapNotNull { item -> 76 | when(item) { 77 | is String -> mapRichTextItem(item) 78 | is Map<*, *> -> mapRichTextItem(item) 79 | else -> null 80 | } 81 | } 82 | .takeIf { it.isNotEmpty() } 83 | ?: return null 84 | return RichText(richTextItems) 85 | } 86 | 87 | private fun mapRichText(rawRichText: JsonObject): RichText? { 88 | val richTextItem = mapRichTextItem(rawRichText) ?: return null 89 | return RichText(richTextItem) 90 | } 91 | 92 | private fun mapRichTextItem(rawRichTextItem: String): RichText.Item { 93 | return RichText.Item.Text(rawRichTextItem, null) 94 | } 95 | 96 | private fun mapRichTextItem(rawRichTextItem: Map<*, *>): RichText.Item? { 97 | val attrs = rawRichTextItem.getAs(ATTRS)?.let(::mapRichTextAttrs) 98 | 99 | val image = rawRichTextItem.getAs(IMAGE) 100 | if (image != null) 101 | return RichText.Item.Image(image, attrs) 102 | 103 | val tag = rawRichTextItem.getAs(TAG) 104 | if (tag != null) 105 | return RichText.Item.Tag(tag, attrs) 106 | 107 | val text = rawRichTextItem.getAs(TEXT) 108 | if (text != null) 109 | return RichText.Item.Text(text, attrs) 110 | 111 | return null 112 | } 113 | 114 | private fun mapRichTextAttrs(rawRichTextAttrs: JsonObject): RichText.Attributes { 115 | return RichText.Attributes( 116 | rawRichTextAttrs.getAs(FONT), 117 | rawRichTextAttrs.getAs(SIZE)?.toFloat(), 118 | rawRichTextAttrs.getAs(STRIKE) ?: false, 119 | rawRichTextAttrs.getAs(UNDERLINE) ?: false, 120 | rawRichTextAttrs.getAs(COLOR), 121 | rawRichTextAttrs.getAs(BACKGROUND), 122 | rawRichTextAttrs.getAs(TINT), 123 | ) 124 | } 125 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/attributes/InteractiveAttributeMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.attributes 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.ui.internal.ui.element.Action 9 | import com.adapty.ui.internal.ui.element.Condition 10 | import com.adapty.ui.internal.utils.DEFAULT_PRODUCT_GROUP 11 | 12 | internal class InteractiveAttributeMapper { 13 | fun mapAction(item: Map<*, *>): Action { 14 | return when(item["type"]) { 15 | "open_url" -> { 16 | val url = (item["url"] as? String) 17 | ?: throw adaptyError( 18 | message = "Couldn't find 'url' for an 'open_url' action", 19 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 20 | ) 21 | Action.OpenUrl(url) 22 | } 23 | "custom" -> { 24 | val customId = (item["custom_id"] as? String) 25 | ?: throw adaptyError( 26 | message = "Couldn't find 'custom_id' for a 'custom' action", 27 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 28 | ) 29 | Action.Custom(customId) 30 | } 31 | "select_product" -> { 32 | val productId = (item["product_id"] as? String) 33 | ?: throw adaptyError( 34 | message = "Couldn't find 'product_id' for a 'select_product' action", 35 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 36 | ) 37 | val groupId = (item["group_id"] as? String) ?: DEFAULT_PRODUCT_GROUP 38 | Action.SelectProduct(productId, groupId) 39 | } 40 | "unselect_product" -> { 41 | val groupId = (item["group_id"] as? String) ?: DEFAULT_PRODUCT_GROUP 42 | Action.UnselectProduct(groupId) 43 | } 44 | "purchase_product" -> { 45 | val productId = (item["product_id"] as? String) 46 | ?: throw adaptyError( 47 | message = "Couldn't find 'product_id' for a 'purchase_product' action", 48 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 49 | ) 50 | Action.PurchaseProduct(productId) 51 | } 52 | "purchase_selected_product" -> { 53 | val groupId = (item["group_id"] as? String) ?: DEFAULT_PRODUCT_GROUP 54 | Action.PurchaseSelectedProduct(groupId) 55 | } 56 | "restore" -> Action.RestorePurchases 57 | "open_screen" -> { 58 | val screenId = (item["screen_id"] as? String) 59 | ?: throw adaptyError( 60 | message = "Couldn't find 'screen_id' for a 'open_screen' action", 61 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 62 | ) 63 | Action.OpenScreen(screenId) 64 | } 65 | "close_screen" -> Action.CloseCurrentScreen 66 | "switch" -> { 67 | val sectionId = (item["section_id"] as? String) 68 | ?: throw adaptyError( 69 | message = "Couldn't find 'section_id' for a 'switch' action", 70 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 71 | ) 72 | val index = (item["index"] as? Number)?.toInt() 73 | ?: throw adaptyError( 74 | message = "Couldn't find 'index' for a 'switch' action", 75 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 76 | ) 77 | Action.SwitchSection(sectionId, index) 78 | } 79 | "close" -> Action.ClosePaywall 80 | else -> Action.Unknown 81 | } 82 | } 83 | 84 | fun mapCondition(item: Map<*, *>): Condition { 85 | return when(item["type"]) { 86 | "selected_section" -> { 87 | val sectionId = (item["section_id"] as? String) 88 | ?: throw adaptyError( 89 | message = "Couldn't find 'section_id' for a 'selected_section' condition", 90 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 91 | ) 92 | val index = (item["index"] as? Number)?.toInt() 93 | ?: throw adaptyError( 94 | message = "Couldn't find 'index' for a 'selected_section' condition", 95 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 96 | ) 97 | Condition.SelectedSection(sectionId, index) 98 | } 99 | "selected_product" -> { 100 | val productId = (item["product_id"] as? String) 101 | ?: throw adaptyError( 102 | message = "Couldn't find 'product_id' for a 'selected_product' condition", 103 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 104 | ) 105 | val groupId = (item["group_id"] as? String) ?: DEFAULT_PRODUCT_GROUP 106 | Condition.SelectedProduct(productId, groupId) 107 | } 108 | else -> Condition.Unknown 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /adapty-ui/src/main/java/com/adapty/ui/internal/mapping/viewconfig/ViewConfigurationMapper.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalAdaptyApi::class) 2 | 3 | package com.adapty.ui.internal.mapping.viewconfig 4 | 5 | import com.adapty.errors.AdaptyErrorCode 6 | import com.adapty.internal.utils.InternalAdaptyApi 7 | import com.adapty.internal.utils.adaptyError 8 | import com.adapty.models.AdaptyPaywall 9 | import com.adapty.ui.AdaptyUI.LocalizedViewConfiguration 10 | import com.adapty.ui.internal.utils.getAs 11 | import com.adapty.ui.internal.utils.getProductGroupKey 12 | 13 | internal typealias JsonObject = Map 14 | internal typealias JsonArray = Iterable 15 | 16 | internal class ViewConfigurationMapper( 17 | private val assetMapper: ViewConfigurationAssetMapper, 18 | private val textMapper: ViewConfigurationTextMapper, 19 | private val screenMapper: ViewConfigurationScreenMapper, 20 | ) { 21 | 22 | private companion object { 23 | const val DATA = "data" 24 | const val PAYWALL_BUILDER_ID = "paywall_builder_id" 25 | const val PAYWALL_BUILDER_CONFIG = "paywall_builder_config" 26 | const val IS_HARD_PAYWALL = "is_hard_paywall" 27 | const val TEMPLATE_ID = "template_id" 28 | const val LANG = "lang" 29 | const val DEFAULT_LOCALIZATION = "default_localization" 30 | const val ASSETS = "assets" 31 | const val LOCALIZATIONS = "localizations" 32 | const val ID = "id" 33 | const val TYPE = "type" 34 | const val URL = "url" 35 | const val STYLES = "styles" 36 | } 37 | 38 | fun map(data: JsonObject, paywall: AdaptyPaywall): LocalizedViewConfiguration { 39 | val data = data.getAs(DATA) ?: data 40 | val id = data.getAs(PAYWALL_BUILDER_ID) ?: throw adaptyError( 41 | message = "id in ViewConfiguration should not be null", 42 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 43 | ) 44 | val config = data.getAs(PAYWALL_BUILDER_CONFIG) ?: throw adaptyError( 45 | message = "config in ViewConfiguration should not be null", 46 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 47 | ) 48 | 49 | val template = Template.from(config.getAs(TEMPLATE_ID)) 50 | 51 | val localesOrderedDesc = setOfNotNull( 52 | config.getAs(DEFAULT_LOCALIZATION), 53 | data.getAs(LANG), 54 | ) 55 | 56 | val screenStateMap = mutableMapOf() 57 | 58 | config.getAs("products")?.getAs("selected")?.forEach { (k, v) -> 59 | if (v == null) 60 | throw adaptyError( 61 | message = "styles in ViewConfiguration should not be null", 62 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 63 | ) 64 | screenStateMap[getProductGroupKey(k)] = v 65 | } 66 | 67 | val assets = assetMapper.map(config, localesOrderedDesc) 68 | 69 | return LocalizedViewConfiguration( 70 | id = id, 71 | paywall = paywall, 72 | isHard = config.getAs(IS_HARD_PAYWALL) ?: false, 73 | isRtl = config.getAs(LOCALIZATIONS)?.let { localizations -> 74 | localizations 75 | .firstOrNull { it.getAs(ID) == localesOrderedDesc.lastOrNull() } 76 | ?.getAs("is_right_to_left") 77 | } ?: false, 78 | assets = assets, 79 | texts = textMapper.map(config, localesOrderedDesc), 80 | screens = config.getAs(STYLES)?.let { 81 | screenMapper.map(it, template, assets, screenStateMap) 82 | } ?: throw adaptyError( 83 | message = "styles in ViewConfiguration should not be null", 84 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 85 | ), 86 | ) 87 | } 88 | 89 | fun mapToMediaUrls(data: JsonObject): Pair> { 90 | val id = data.getAs(PAYWALL_BUILDER_ID) ?: throw adaptyError( 91 | message = "id in ViewConfiguration should not be null", 92 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 93 | ) 94 | val config = data.getAs(PAYWALL_BUILDER_CONFIG) ?: return id to emptySet() 95 | val mediaUrls = mutableSetOf() 96 | config.getAs(ASSETS)?.let { assets -> 97 | mediaUrls += findMediaUrls(assets) 98 | } 99 | config.getAs(LOCALIZATIONS)?.forEach { localization -> 100 | localization.getAs(ASSETS)?.let { assets -> 101 | mediaUrls += findMediaUrls(assets) 102 | } 103 | } 104 | return id to mediaUrls 105 | } 106 | 107 | private fun findMediaUrls(assets: JsonArray): Set { 108 | val mediaUrls = mutableSetOf() 109 | assets.forEach { asset -> 110 | if (asset.getAs(TYPE) == "image") { 111 | asset.getAs(URL)?.let { url -> mediaUrls.add(url) } 112 | } 113 | } 114 | return mediaUrls 115 | } 116 | } 117 | 118 | internal enum class Template { 119 | BASIC, FLAT, TRANSPARENT; 120 | 121 | companion object { 122 | fun from(templateId: String?) = when (templateId) { 123 | "basic" -> BASIC 124 | "flat" -> FLAT 125 | "transparent" -> TRANSPARENT 126 | else -> throw adaptyError( 127 | message = "Unsupported templateId: $templateId", 128 | adaptyErrorCode = AdaptyErrorCode.DECODING_FAILED 129 | ) 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------