diff --git a/WebViewLocalStateStorageTests.swift b/WebViewLocalStateStorageTests.swift
new file mode 100644
index 000000000..26b0b44c6
--- /dev/null
+++ b/WebViewLocalStateStorageTests.swift
@@ -0,0 +1,279 @@
+//
+// WebViewLocalStateStorageTests.swift
+// MindboxTests
+//
+// Created by Sergei Semko on 3/11/26.
+// Copyright © 2026 Mindbox. All rights reserved.
+//
+
+import Testing
+@testable import Mindbox
+
+@Suite("WebViewLocalStateStorage", .tags(.webView))
+struct WebViewLocalStateStorageTests {
+
+ private let testSuiteName = "cloud.Mindbox.test.webview.localState"
+ private let keyPrefix = Constants.WebViewLocalState.keyPrefix
+
+ private func makeSUT() -> (sut: WebViewLocalStateStorage, defaults: UserDefaults, persistence: MockPersistenceStorage) {
+ let persistence = MockPersistenceStorage()
+ let defaults = UserDefaults(suiteName: testSuiteName)!
+ defaults.removePersistentDomain(forName: testSuiteName)
+ let sut = WebViewLocalStateStorage(dataDefaults: defaults, persistenceStorage: persistence)
+ return (sut, defaults, persistence)
+ }
+
+ // MARK: - get
+
+ @Test("get returns default version and empty data when storage is empty")
+ func getEmptyStorage() {
+ let (sut, _, _) = makeSUT()
+
+ let state = sut.get(keys: [])
+
+ #expect(state.version == Constants.WebViewLocalState.defaultVersion)
+ #expect(state.data.isEmpty)
+ }
+
+ @Test("get returns all stored keys when keys array is empty")
+ func getAllKeys() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("value1", forKey: "\(keyPrefix)key1")
+ defaults.set("value2", forKey: "\(keyPrefix)key2")
+
+ let state = sut.get(keys: [])
+
+ #expect(state.data.count == 2)
+ #expect(state.data["key1"] == "value1")
+ #expect(state.data["key2"] == "value2")
+ }
+
+ @Test("get returns only requested keys")
+ func getSpecificKeys() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("value1", forKey: "\(keyPrefix)key1")
+ defaults.set("value2", forKey: "\(keyPrefix)key2")
+ defaults.set("value3", forKey: "\(keyPrefix)key3")
+
+ let state = sut.get(keys: ["key1", "key3"])
+
+ #expect(state.data.count == 2)
+ #expect(state.data["key1"] == "value1")
+ #expect(state.data["key3"] == "value3")
+ }
+
+ @Test("get omits missing keys from data")
+ func getMissingKeys() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("value1", forKey: "\(keyPrefix)key1")
+
+ let state = sut.get(keys: ["key1", "missing"])
+
+ #expect(state.data.count == 1)
+ #expect(state.data["key1"] == "value1")
+ #expect(state.data["missing"] == nil)
+ }
+
+ @Test("get returns current version from persistence")
+ func getCurrentVersion() {
+ let (sut, _, persistence) = makeSUT()
+ persistence.webViewLocalStateVersion = 5
+
+ let state = sut.get(keys: [])
+
+ #expect(state.version == 5)
+ }
+
+ @Test("get returns default version when persistence version is nil")
+ func getDefaultVersion() {
+ let (sut, _, persistence) = makeSUT()
+ persistence.webViewLocalStateVersion = nil
+
+ let state = sut.get(keys: [])
+
+ #expect(state.version == Constants.WebViewLocalState.defaultVersion)
+ }
+
+ // MARK: - set
+
+ @Test("set stores values in UserDefaults")
+ func setStoresValues() {
+ let (sut, defaults, _) = makeSUT()
+
+ _ = sut.set(data: ["key1": "value1", "key2": "value2"])
+
+ #expect(defaults.string(forKey: "\(keyPrefix)key1") == "value1")
+ #expect(defaults.string(forKey: "\(keyPrefix)key2") == "value2")
+ }
+
+ @Test("set removes key when value is nil")
+ func setRemovesNilKey() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("value1", forKey: "\(keyPrefix)key1")
+
+ _ = sut.set(data: ["key1": nil])
+
+ #expect(defaults.string(forKey: "\(keyPrefix)key1") == nil)
+ }
+
+ @Test("set updates existing values")
+ func setUpdatesValues() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("old", forKey: "\(keyPrefix)key1")
+
+ let state = sut.set(data: ["key1": "new"])
+
+ #expect(defaults.string(forKey: "\(keyPrefix)key1") == "new")
+ #expect(state.data["key1"] == "new")
+ }
+
+ @Test("set returns only affected keys")
+ func setReturnsAffectedKeys() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("existing", forKey: "\(keyPrefix)existing")
+
+ let state = sut.set(data: ["key1": "value1"])
+
+ #expect(state.data.count == 1)
+ #expect(state.data["key1"] == "value1")
+ #expect(state.data["existing"] == nil)
+ }
+
+ @Test("set does not change version")
+ func setPreservesVersion() {
+ let (sut, _, persistence) = makeSUT()
+ persistence.webViewLocalStateVersion = 3
+
+ let state = sut.set(data: ["key1": "value1"])
+
+ #expect(state.version == 3)
+ #expect(persistence.webViewLocalStateVersion == 3)
+ }
+
+ @Test("set stores each key as separate UserDefaults entry")
+ func setSeparateEntries() {
+ let (sut, defaults, _) = makeSUT()
+
+ _ = sut.set(data: ["firstKey": "firstValue", "secondKey": "secondValue"])
+
+ #expect(defaults.string(forKey: "\(keyPrefix)firstKey") == "firstValue")
+ #expect(defaults.string(forKey: "\(keyPrefix)secondKey") == "secondValue")
+ }
+
+ // MARK: - initialize
+
+ @Test("initialize stores version in PersistenceStorage")
+ func initStoresVersion() {
+ let (sut, _, persistence) = makeSUT()
+
+ _ = sut.initialize(version: 7, data: ["key": "value"])
+
+ #expect(persistence.webViewLocalStateVersion == 7)
+ }
+
+ @Test("initialize stores data and returns it")
+ func initStoresAndReturnsData() throws {
+ let (sut, defaults, _) = makeSUT()
+
+ let state = try #require(sut.initialize(version: 2, data: ["key1": "value1", "key2": "value2"]))
+
+ #expect(state.version == 2)
+ #expect(state.data["key1"] == "value1")
+ #expect(state.data["key2"] == "value2")
+ #expect(defaults.string(forKey: "\(keyPrefix)key1") == "value1")
+ #expect(defaults.string(forKey: "\(keyPrefix)key2") == "value2")
+ }
+
+ @Test("initialize rejects zero version")
+ func initRejectsZero() {
+ let (sut, _, _) = makeSUT()
+
+ #expect(sut.initialize(version: 0, data: ["key": "value"]) == nil)
+ }
+
+ @Test("initialize rejects negative version")
+ func initRejectsNegative() {
+ let (sut, _, _) = makeSUT()
+
+ #expect(sut.initialize(version: -1, data: ["key": "value"]) == nil)
+ }
+
+ @Test("initialize removes keys with nil values")
+ func initRemovesNilKeys() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("value1", forKey: "\(keyPrefix)key1")
+
+ let state = sut.initialize(version: 2, data: ["key1": nil])
+
+ #expect(state != nil)
+ #expect(defaults.string(forKey: "\(keyPrefix)key1") == nil)
+ }
+
+ @Test("initialize merges with existing data")
+ func initMergesData() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("existing", forKey: "\(keyPrefix)old")
+
+ let state = sut.initialize(version: 3, data: ["new": "value"])
+
+ #expect(state != nil)
+ #expect(defaults.string(forKey: "\(keyPrefix)old") == "existing")
+ #expect(defaults.string(forKey: "\(keyPrefix)new") == "value")
+ }
+
+ @Test("initialize does not store version on rejection")
+ func initPreservesVersionOnReject() {
+ let (sut, _, persistence) = makeSUT()
+ persistence.webViewLocalStateVersion = 5
+
+ _ = sut.initialize(version: 0, data: ["key": "value"])
+
+ #expect(persistence.webViewLocalStateVersion == 5)
+ }
+
+ // MARK: - Integration
+
+ @Test("full flow: init → set → get")
+ func fullFlow() throws {
+ let (sut, _, _) = makeSUT()
+
+ let initState = try #require(sut.initialize(version: 2, data: ["key1": "value1", "key2": "value2"]))
+ #expect(initState.version == 2)
+
+ let setState = sut.set(data: ["key1": "updated", "key2": nil, "key3": "value3"])
+ #expect(setState.version == 2)
+
+ let getState = sut.get(keys: [])
+ #expect(getState.version == 2)
+ #expect(getState.data["key1"] == "updated")
+ #expect(getState.data["key2"] == nil)
+ #expect(getState.data["key3"] == "value3")
+ }
+
+ @Test("get after set with null returns empty for deleted key")
+ func setNullThenGet() {
+ let (sut, _, _) = makeSUT()
+
+ _ = sut.set(data: ["key1": "value1"])
+ _ = sut.set(data: ["key1": nil])
+
+ let state = sut.get(keys: ["key1"])
+ #expect(state.data.isEmpty)
+ }
+
+ @Test("prefix isolation: non-prefixed keys and Apple system keys are filtered out")
+ func prefixIsolation() {
+ let (sut, defaults, _) = makeSUT()
+ defaults.set("foreign", forKey: "foreignKey")
+ defaults.set("value", forKey: "\(keyPrefix)myKey")
+
+ let state = sut.get(keys: [])
+
+ #expect(state.data.count == 1)
+ #expect(state.data["myKey"] == "value")
+ #expect(state.data["foreignKey"] == nil)
+ #expect(state.data["AKLastLocale"] == nil)
+ #expect(state.data["AppleLocale"] == nil)
+ #expect(state.data["NSInterfaceStyle"] == nil)
+ }
+}
diff --git a/example/app/build.gradle b/example/app/build.gradle
index 85e4b4269..1fbb71f6d 100644
--- a/example/app/build.gradle
+++ b/example/app/build.gradle
@@ -92,7 +92,7 @@ dependencies {
implementation 'com.google.code.gson:gson:2.11.0'
//Mindbox
- implementation 'cloud.mindbox:mobile-sdk:2.14.5'
+ implementation 'cloud.mindbox:mobile-sdk:2.15.0-rc'
implementation 'cloud.mindbox:mindbox-firebase'
implementation 'cloud.mindbox:mindbox-huawei'
implementation 'cloud.mindbox:mindbox-rustore'
diff --git a/gradle.properties b/gradle.properties
index d74a443de..c9ffc8477 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,7 +20,7 @@ android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# SDK version property
-SDK_VERSION_NAME=2.14.5
+SDK_VERSION_NAME=2.15.0-rc
USE_LOCAL_MINDBOX_COMMON=true
android.nonTransitiveRClass=false
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml
index 822fd693c..e50bafe2f 100644
--- a/sdk/src/main/AndroidManifest.xml
+++ b/sdk/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt
index 9780be6a3..ca2530714 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt
@@ -22,6 +22,7 @@ import cloud.mindbox.mobile_sdk.Mindbox.logE
import cloud.mindbox.mobile_sdk.Mindbox.logW
import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
+import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer
import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_BUTTON_KEY
import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_KEY
@@ -299,3 +300,19 @@ internal fun List.sortByPriority(): List {
internal inline fun Queue.pollIf(predicate: (T) -> Boolean): T? {
return peek()?.takeIf(predicate)?.let { poll() }
}
+
+internal fun InAppType.getImageUrl(): String? {
+ return when (this) {
+ is InAppType.WebView -> this.layers
+ is InAppType.ModalWindow -> this.layers
+ is InAppType.Snackbar -> this.layers
+ }
+ .filterIsInstance()
+ .firstOrNull()
+ ?.source
+ ?.let { source ->
+ when (source) {
+ is Layer.ImageLayer.Source.UrlSource -> source.url
+ }
+ }
+}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
index 95e4bf48a..9ad7a86e2 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
@@ -94,6 +94,7 @@ public object Mindbox : MindboxLog {
private lateinit var lifecycleManager: LifecycleManager
private val userVisitManager: UserVisitManager by mindboxInject { userVisitManager }
+ private val timeProvider by mindboxInject { timeProvider }
internal var pushServiceHandlers: List = listOf()
@@ -1244,6 +1245,11 @@ public object Mindbox : MindboxLog {
MindboxPreferences.isNotificationEnabled = isNotificationEnabled
MindboxPreferences.instanceId = instanceId
+ if (MindboxPreferences.firstInitializationTime == null) {
+ MindboxPreferences.firstInitializationTime = timeProvider.currentTimestamp()
+ .convertToIso8601String()
+ }
+
MindboxEventManager.appInstalled(context, initData, configuration.shouldCreateCustomer)
deliverDeviceUuid(deviceUuid)
@@ -1358,7 +1364,9 @@ public object Mindbox : MindboxLog {
requestUrl = requestUrl,
sdkVersionNumeric = Constants.SDK_VERSION_NUMERIC
)
-
+ if (source != null || requestUrl != null) {
+ sessionStorageManager.lastTrackVisitData = trackVisitData
+ }
MindboxEventManager.appStarted(applicationContext, trackVisitData)
}
}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DataModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DataModule.kt
index 65e570ff2..6695ffa47 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DataModule.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DataModule.kt
@@ -1,5 +1,6 @@
package cloud.mindbox.mobile_sdk.di.modules
+import cloud.mindbox.mobile_sdk.annotations.InternalMindboxApi
import cloud.mindbox.mobile_sdk.inapp.data.checkers.MaxInappsPerDayLimitChecker
import cloud.mindbox.mobile_sdk.inapp.data.checkers.MaxInappsPerSessionLimitChecker
import cloud.mindbox.mobile_sdk.inapp.data.checkers.MinIntervalBetweenShowsLimitChecker
@@ -17,7 +18,9 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppImageLoader
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppImageSizeStorage
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.PermissionManager
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.checkers.Checker
+import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.FeatureToggleManager
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.GeoSerializationManager
+import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppFailureTracker
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppSerializationManager
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.MobileConfigSerializationManager
import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.*
@@ -25,6 +28,7 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.validators.InAppValidato
import cloud.mindbox.mobile_sdk.inapp.presentation.InAppMessageDelayedManager
import cloud.mindbox.mobile_sdk.inapp.presentation.MindboxNotificationManager
import cloud.mindbox.mobile_sdk.inapp.presentation.MindboxNotificationManagerImpl
+import cloud.mindbox.mobile_sdk.inapp.presentation.view.BridgeMessage
import cloud.mindbox.mobile_sdk.managers.*
import cloud.mindbox.mobile_sdk.managers.MobileConfigSettingsManagerImpl
import cloud.mindbox.mobile_sdk.managers.RequestPermissionManager
@@ -37,6 +41,7 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.Dispatchers
+@OptIn(InternalMindboxApi::class)
internal fun DataModule(
appContextModule: AppContextModule,
apiModule: ApiModule
@@ -56,11 +61,14 @@ internal fun DataModule(
override val modalWindowValidator: ModalWindowValidator by lazy {
ModalWindowValidator(
imageLayerValidator = imageLayerValidator,
+ webViewLayerValidator = webViewLayerValidator,
elementValidator = modalElementValidator
)
}
override val imageLayerValidator: ImageLayerValidator
get() = ImageLayerValidator()
+ override val webViewLayerValidator: WebViewLayerValidator
+ get() = WebViewLayerValidator()
override val modalElementValidator: ModalElementValidator by lazy {
ModalElementValidator(
@@ -154,7 +162,8 @@ internal fun DataModule(
timeSpanPositiveValidator = slidingExpirationParametersValidator,
mobileConfigSettingsManager = mobileConfigSettingsManager,
integerPositiveValidator = integerPositiveValidator,
- inappSettingsManager = inappSettingsManager
+ inappSettingsManager = inappSettingsManager,
+ featureToggleManager = featureToggleManager
)
}
@@ -197,6 +206,14 @@ internal fun DataModule(
override val inAppSerializationManager: InAppSerializationManager
get() = InAppSerializationManagerImpl(gson = gson)
+ override val inAppFailureTracker: InAppFailureTracker by lazy {
+ InAppFailureTrackerImpl(
+ timeProvider = timeProvider,
+ inAppRepository = inAppRepository,
+ featureToggleManager = featureToggleManager
+ )
+ }
+
override val inAppSegmentationRepository: InAppSegmentationRepository by lazy {
InAppSegmentationRepositoryImpl(
inAppMapper = inAppMapper,
@@ -204,6 +221,11 @@ internal fun DataModule(
gatewayManager = gatewayManager,
)
}
+ override val inAppTargetingErrorRepository: InAppTargetingErrorRepository by lazy {
+ InAppTargetingErrorRepositoryImpl(
+ sessionStorageManager = sessionStorageManager
+ )
+ }
override val monitoringValidator: MonitoringValidator by lazy { MonitoringValidator() }
@@ -239,6 +261,7 @@ internal fun DataModule(
}
override val integerPositiveValidator: IntegerPositiveValidator by lazy { IntegerPositiveValidator() }
override val inappSettingsManager: InappSettingsManagerImpl by lazy { InappSettingsManagerImpl(sessionStorageManager) }
+ override val featureToggleManager: FeatureToggleManager by lazy { FeatureToggleManagerImpl() }
override val maxInappsPerSessionLimitChecker: Checker by lazy { MaxInappsPerSessionLimitChecker(sessionStorageManager) }
override val maxInappsPerDayLimitChecker: Checker by lazy { MaxInappsPerDayLimitChecker(inAppRepository, sessionStorageManager, timeProvider) }
override val minIntervalBetweenShowsLimitChecker: Checker by lazy { MinIntervalBetweenShowsLimitChecker(sessionStorageManager, inAppRepository, timeProvider) }
@@ -270,6 +293,23 @@ internal fun DataModule(
override val gson: Gson by lazy {
GsonBuilder()
+ .registerTypeAdapterFactory(
+ RuntimeTypeAdapterFactory
+ .of(
+ BridgeMessage::class.java,
+ BridgeMessage.TYPE_FIELD_NAME,
+ true
+ ).registerSubtype(
+ BridgeMessage.Request::class.java,
+ BridgeMessage.TYPE_REQUEST
+ ).registerSubtype(
+ BridgeMessage.Response::class.java,
+ BridgeMessage.TYPE_RESPONSE
+ ).registerSubtype(
+ BridgeMessage.Error::class.java,
+ BridgeMessage.TYPE_ERROR
+ )
+ )
.registerTypeAdapterFactory(
RuntimeTypeAdapterFactory
.of(
@@ -295,9 +335,6 @@ internal fun DataModule(
).registerSubtype(
PayloadBlankDto.SnackBarBlankDto::class.java,
PayloadDto.SnackbarDto.SNACKBAR_JSON_NAME
- ).registerSubtype(
- PayloadBlankDto.WebViewBlankDto::class.java,
- PayloadDto.WebViewDto.WEBVIEW_JSON_NAME
)
).registerTypeAdapterFactory(
RuntimeTypeAdapterFactory
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt
index ffd36eab9..a2cf4ba4f 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/DomainModule.kt
@@ -33,6 +33,7 @@ internal fun DomainModule(
maxInappsPerDayLimitChecker = maxInappsPerDayLimitChecker,
minIntervalBetweenShowsLimitChecker = minIntervalBetweenShowsLimitChecker,
timeProvider = timeProvider,
+ sessionStorageManager = sessionStorageManager,
)
}
override val callbackInteractor: CallbackInteractor by lazy {
@@ -43,8 +44,10 @@ internal fun DomainModule(
InAppProcessingManagerImpl(
inAppGeoRepository = inAppGeoRepository,
inAppSegmentationRepository = inAppSegmentationRepository,
+ inAppTargetingErrorRepository = inAppTargetingErrorRepository,
inAppContentFetcher = inAppContentFetcher,
- inAppRepository = inAppRepository
+ inAppRepository = inAppRepository,
+ inAppFailureTracker = inAppFailureTracker
)
}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt
index 8c2796e29..4d5f84995 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/MindboxModule.kt
@@ -69,7 +69,9 @@ internal interface DataModule : MindboxModule {
val callbackRepository: CallbackRepository
val geoSerializationManager: GeoSerializationManager
val inAppSerializationManager: InAppSerializationManager
+ val inAppFailureTracker: InAppFailureTracker
val inAppSegmentationRepository: InAppSegmentationRepository
+ val inAppTargetingErrorRepository: InAppTargetingErrorRepository
val inAppValidator: InAppValidator
val inAppMapper: InAppMapper
val gson: Gson
@@ -88,6 +90,7 @@ internal interface DataModule : MindboxModule {
val modalElementDtoDataFiller: ModalElementDtoDataFiller
val modalWindowValidator: ModalWindowValidator
val imageLayerValidator: ImageLayerValidator
+ val webViewLayerValidator: WebViewLayerValidator
val modalElementValidator: ModalElementValidator
val snackbarValidator: SnackbarValidator
val closeButtonModalElementValidator: CloseButtonModalElementValidator
@@ -114,6 +117,7 @@ internal interface DataModule : MindboxModule {
val mobileConfigSettingsManager: MobileConfigSettingsManager
val integerPositiveValidator: IntegerPositiveValidator
val inappSettingsManager: InappSettingsManager
+ val featureToggleManager: FeatureToggleManager
val maxInappsPerSessionLimitChecker: Checker
val maxInappsPerDayLimitChecker: Checker
val minIntervalBetweenShowsLimitChecker: Checker
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt
index ec8be65c0..de83c9b68 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt
@@ -28,7 +28,8 @@ internal fun PresentationModule(
monitoringInteractor = monitoringInteractor,
sessionStorageManager = sessionStorageManager,
userVisitManager = userVisitManager,
- inAppMessageDelayedManager = inAppMessageDelayedManager
+ inAppMessageDelayedManager = inAppMessageDelayedManager,
+ timeProvider = timeProvider
)
}
override val clipboardManager: ClipboardManager by lazy {
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/BackgroundDto.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/BackgroundDto.kt
index 6898ffa48..81fde851b 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/BackgroundDto.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/BackgroundDto.kt
@@ -1,5 +1,7 @@
package cloud.mindbox.mobile_sdk.inapp.data.dto
+import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.WebViewParamsDeserializer
+import com.google.gson.annotations.JsonAdapter
import com.google.gson.annotations.SerializedName
internal data class BackgroundDto(
@@ -68,6 +70,7 @@ internal data class BackgroundDto(
@SerializedName("${"$"}type")
val type: String?,
@SerializedName("params")
+ @JsonAdapter(WebViewParamsDeserializer::class)
val params: Map?,
) : LayerDto() {
internal companion object {
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/FormBlankDto.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/FormBlankDto.kt
index e92b31539..71fddb49f 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/FormBlankDto.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/FormBlankDto.kt
@@ -67,20 +67,6 @@ internal sealed class PayloadBlankDto {
val elements: List?
)
}
-
- data class WebViewBlankDto(
- @SerializedName("content")
- val content: ContentBlankDto?,
- @SerializedName("${"$"}type")
- val type: String?
- ) : PayloadBlankDto() {
- internal data class ContentBlankDto(
- @SerializedName("background")
- val background: BackgroundBlankDto?,
- @SerializedName("elements")
- val elements: List?
- )
- }
}
internal data class BackgroundBlankDto(
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/PayloadDto.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/PayloadDto.kt
index dee31da95..c8b7e5b11 100644
--- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/PayloadDto.kt
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/PayloadDto.kt
@@ -1,6 +1,5 @@
package cloud.mindbox.mobile_sdk.inapp.data.dto
-import cloud.mindbox.mobile_sdk.inapp.data.dto.PayloadDto.SnackbarDto.ContentDto
import cloud.mindbox.mobile_sdk.isInRange
import com.google.gson.annotations.SerializedName
@@ -8,18 +7,6 @@ import com.google.gson.annotations.SerializedName
* In-app types
**/
internal sealed class PayloadDto {
-
- data class WebViewDto(
- @SerializedName("${"$"}type")
- val type: String?,
- @SerializedName("content")
- val content: ModalWindowDto.ContentDto?,
- ) : PayloadDto() {
- internal companion object {
- const val WEBVIEW_JSON_NAME = "webview"
- }
- }
-
data class SnackbarDto(
@SerializedName("content")
val content: ContentDto?,
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializer.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializer.kt
new file mode 100644
index 000000000..7da4678a8
--- /dev/null
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializer.kt
@@ -0,0 +1,34 @@
+package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers
+
+import cloud.mindbox.mobile_sdk.logger.mindboxLogW
+import cloud.mindbox.mobile_sdk.models.operation.response.SettingsDtoBlank
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import java.lang.reflect.Type
+
+private typealias FeatureTogglesDtoBlank = SettingsDtoBlank.FeatureTogglesDtoBlank
+
+internal class FeatureTogglesDtoBlankDeserializer : JsonDeserializer {
+ override fun deserialize(
+ json: JsonElement,
+ typeOfT: Type,
+ context: JsonDeserializationContext
+ ): FeatureTogglesDtoBlank {
+ val jsonObject = json.asJsonObject
+ val result = mutableMapOf()
+ jsonObject.entrySet().forEach { (key, value) ->
+ val booleanValue = when {
+ value?.isJsonPrimitive == true && value.asJsonPrimitive.isBoolean ->
+ value.asJsonPrimitive.asBoolean
+ else -> {
+ mindboxLogW("Feature toggle value is not boolean. key=$key, value=$value")
+ null
+ }
+ }
+
+ result[key] = booleanValue
+ }
+ return FeatureTogglesDtoBlank(toggles = result)
+ }
+}
diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt
new file mode 100644
index 000000000..415e6e2f1
--- /dev/null
+++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt
@@ -0,0 +1,29 @@
+package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import java.lang.reflect.Type
+
+internal class InAppTagsDeserializer : JsonDeserializer