├── 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 |
--------------------------------------------------------------------------------