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 41f7ec0de..6ec059ae4 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 @@ -18,6 +18,7 @@ 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.InAppSerializationManager import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.MobileConfigSerializationManager @@ -157,7 +158,8 @@ internal fun DataModule( timeSpanPositiveValidator = slidingExpirationParametersValidator, mobileConfigSettingsManager = mobileConfigSettingsManager, integerPositiveValidator = integerPositiveValidator, - inappSettingsManager = inappSettingsManager + inappSettingsManager = inappSettingsManager, + featureToggleManager = featureToggleManager ) } @@ -242,6 +244,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) } 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..729ac3d96 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 @@ -114,6 +114,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..3fb51bd31 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 @@ -17,7 +17,7 @@ internal fun PresentationModule( AppContextModule by appContextModule { override val inAppMessageViewDisplayer by lazy { - InAppMessageViewDisplayerImpl(inAppImageSizeStorage) + InAppMessageViewDisplayerImpl(inAppImageSizeStorage, featureToggleManager) } override val inAppMessageManager by lazy { 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..6b904cad1 --- /dev/null +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializer.kt @@ -0,0 +1,28 @@ +package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers + +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) -> + result[key] = value?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isBoolean } + ?.asJsonPrimitive + ?.asBoolean + } + + return FeatureTogglesDtoBlank(toggles = result) + } +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/JsonElementExtensions.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/JsonElementExtensions.kt index 27f6cdbae..e8198e416 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/JsonElementExtensions.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/JsonElementExtensions.kt @@ -39,3 +39,9 @@ internal fun JsonElement.getString(): String? { else -> null } } + +internal fun JsonObject.getAsBooleanOrNull(key: String): Boolean? { + return get(key)?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isBoolean } + ?.asJsonPrimitive + ?.asBoolean +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImpl.kt new file mode 100644 index 000000000..efe64102d --- /dev/null +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImpl.kt @@ -0,0 +1,25 @@ +package cloud.mindbox.mobile_sdk.inapp.data.managers + +import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.FeatureToggleManager +import cloud.mindbox.mobile_sdk.models.operation.response.InAppConfigResponse +import java.util.concurrent.ConcurrentHashMap + +internal const val SEND_INAPP_SHOW_ERROR_FEATURE = "shouldSendInAppShowError" + +internal class FeatureToggleManagerImpl : FeatureToggleManager { + + private val toggles = ConcurrentHashMap() + + override fun applyToggles(config: InAppConfigResponse?) { + toggles.clear() + config?.settings?.featureToggles?.forEach { (key, value) -> + value?.let { + toggles[key] = value + } + } + } + + override fun isEnabled(key: String): Boolean { + return toggles[key] ?: false + } +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImpl.kt index b9d9bfc3b..8d7ce6c5e 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/MobileConfigSerializationManagerImpl.kt @@ -110,12 +110,18 @@ internal class MobileConfigSerializationManagerImpl(private val gson: Gson) : } val inappSettings = runCatching { - gson.fromJson(json.asJsonObject.get("inapp"), SettingsDtoBlank.InappSettingsDtoBlank::class.java)?.copy() + gson.fromJson(json.asJsonObject.get("inapp"), InappSettingsDtoBlank::class.java)?.copy() }.getOrNull { mindboxLogE("Failed to parse inapp block in settings section ") } - SettingsDtoBlank(operations, ttl, slidingExpiration, inappSettings) + val featureToggles = runCatching { + gson.fromJson(json.asJsonObject.get("featureToggles"), FeatureTogglesDtoBlank::class.java)?.copy() + }.getOrNull { + mindboxLogE("Failed to parse featureToggles block in settings section") + } + + SettingsDtoBlank(operations, ttl, slidingExpiration, inappSettings, featureToggles) } }.getOrNull { mindboxLogE("Failed to parse settings block", it) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImpl.kt index 34a864701..097abc998 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImpl.kt @@ -7,6 +7,7 @@ import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager import cloud.mindbox.mobile_sdk.inapp.data.managers.data_filler.DataManager import cloud.mindbox.mobile_sdk.inapp.data.mapper.InAppMapper import cloud.mindbox.mobile_sdk.inapp.data.validators.* +import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.FeatureToggleManager import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.MobileConfigSerializationManager import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfigRepository import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.validators.InAppValidator @@ -49,7 +50,8 @@ internal class MobileConfigRepositoryImpl( private val timeSpanPositiveValidator: TimeSpanPositiveValidator, private val mobileConfigSettingsManager: MobileConfigSettingsManager, private val integerPositiveValidator: IntegerPositiveValidator, - private val inappSettingsManager: InappSettingsManager + private val inappSettingsManager: InappSettingsManager, + private val featureToggleManager: FeatureToggleManager ) : MobileConfigRepository { private val mutex = Mutex() @@ -100,6 +102,7 @@ internal class MobileConfigRepositoryImpl( mobileConfigSettingsManager.saveSessionTime(config = filteredConfig) mobileConfigSettingsManager.checkPushTokenKeepalive(config = filteredConfig) inappSettingsManager.applySettings(config = filteredConfig) + featureToggleManager.applyToggles(config = filteredConfig) configState.value = updatedInAppConfig mindboxLogI(message = "Providing config: $updatedInAppConfig") } @@ -182,7 +185,12 @@ internal class MobileConfigRepositoryImpl( val inappSettings = runCatching { getInappSettings(configBlank) }.getOrNull { mindboxLogW("Unable to get inapp settings $it") } - return SettingsDto(operations, ttl, slidingExpiration, inappSettings) + + val featureToggles = runCatching { getFeatureToggles(configBlank) }.getOrNull { + mindboxLogW("Unable to get featureToggles settings $it") + } + + return SettingsDto(operations, ttl, slidingExpiration, inappSettings, featureToggles) } private fun getInAppTtl(configBlank: InAppConfigResponseBlank?): TtlDto? = @@ -241,6 +249,9 @@ internal class MobileConfigRepositoryImpl( null } + private fun getFeatureToggles(configBlank: InAppConfigResponseBlank?): Map? = + configBlank?.settings?.featureToggles?.toggles + private fun getABTests(configBlank: InAppConfigResponseBlank?): List { return try { if (configBlank?.abtests == null) return listOf() diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/FeatureToggleManager.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/FeatureToggleManager.kt new file mode 100644 index 000000000..244e6a8ad --- /dev/null +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/FeatureToggleManager.kt @@ -0,0 +1,10 @@ +package cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers + +import cloud.mindbox.mobile_sdk.models.operation.response.InAppConfigResponse + +internal interface FeatureToggleManager { + + fun applyToggles(config: InAppConfigResponse?) + + fun isEnabled(key: String): Boolean +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt index 02e2f808a..d38a009fc 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt @@ -8,8 +8,10 @@ import cloud.mindbox.mobile_sdk.di.mindboxInject import cloud.mindbox.mobile_sdk.fromJson import cloud.mindbox.mobile_sdk.inapp.data.dto.BackgroundDto import cloud.mindbox.mobile_sdk.inapp.data.dto.PayloadDto +import cloud.mindbox.mobile_sdk.inapp.data.managers.SEND_INAPP_SHOW_ERROR_FEATURE import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppActionCallbacks import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppImageSizeStorage +import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.FeatureToggleManager import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppTypeWrapper import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer @@ -34,7 +36,10 @@ internal interface MindboxView { fun requestPermission() } -internal class InAppMessageViewDisplayerImpl(private val inAppImageSizeStorage: InAppImageSizeStorage) : +internal class InAppMessageViewDisplayerImpl( + private val inAppImageSizeStorage: InAppImageSizeStorage, + private val featureToggleManager: FeatureToggleManager +) : InAppMessageViewDisplayer { companion object { @@ -191,6 +196,11 @@ internal class InAppMessageViewDisplayerImpl(private val inAppImageSizeStorage: wrapper: InAppTypeWrapper, isRestored: Boolean = false, ) { + when (featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) { + true -> mindboxLogI("InApp.ShowFailure sending enabled") + false -> mindboxLogI("InApp.ShowFailure sending disabled") + } + if (!isRestored) isActionExecuted = false if (isRestored && tryReattachRestoredInApp(wrapper.inAppType.inAppId)) return diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt index 5125ed618..f9be86f54 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt @@ -1,6 +1,7 @@ package cloud.mindbox.mobile_sdk.models.operation.response import cloud.mindbox.mobile_sdk.inapp.data.dto.PayloadDto +import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.FeatureTogglesDtoBlankDeserializer import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.InAppIsPriorityDeserializer import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.models.TimeSpan @@ -31,7 +32,9 @@ internal data class SettingsDtoBlank( @SerializedName("slidingExpiration") val slidingExpiration: SlidingExpirationDtoBlank?, @SerializedName("inapp") - val inappSettings: InappSettingsDtoBlank? + val inappSettings: InappSettingsDtoBlank?, + @SerializedName("featureToggles") + val featureToggles: FeatureTogglesDtoBlank? ) { internal data class OperationDtoBlank( @SerializedName("systemName") @@ -60,6 +63,11 @@ internal data class SettingsDtoBlank( @SerializedName(InappSettingsDtoBlankDeserializer.MIN_INTERVAL_BETWEEN_SHOWS) val minIntervalBetweenShows: TimeSpan?, ) + + @JsonAdapter(FeatureTogglesDtoBlankDeserializer::class) + internal data class FeatureTogglesDtoBlank( + val toggles: Map + ) } internal data class SettingsDto( @@ -70,7 +78,9 @@ internal data class SettingsDto( @SerializedName("slidingExpiration") val slidingExpiration: SlidingExpirationDto?, @SerializedName("inapp") - val inapp: InappSettingsDto? + val inapp: InappSettingsDto?, + @SerializedName("featureToggles") + val featureToggles: Map? ) internal data class OperationDto( diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializerTest.kt new file mode 100644 index 000000000..eb7ee8e93 --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/FeatureTogglesDtoBlankDeserializerTest.kt @@ -0,0 +1,160 @@ +package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers + +import cloud.mindbox.mobile_sdk.inapp.data.managers.SEND_INAPP_SHOW_ERROR_FEATURE +import cloud.mindbox.mobile_sdk.models.operation.response.SettingsDtoBlank +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class FeatureTogglesDtoBlankDeserializerTest { + private lateinit var gson: Gson + + @Before + fun setup() { + gson = GsonBuilder() + .create() + } + + @Test + fun `deserialize valid true value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", true) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertEquals(true, result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize valid false value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", false) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertEquals(false, result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize multiple keys`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", true) + addProperty("anotherToggle", false) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertEquals(true, result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + assertEquals(false, result.toggles["anotherToggle"]) + } + + @Test + fun `deserialize string true value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", "true") + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize string false value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", "false") + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize number 1 value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", 1) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize invalid string value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", "invalid") + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize object value`() { + val json = JsonObject().apply { + add("shouldSendInAppShowError", JsonObject().apply { + addProperty("value", true) + }) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize array value`() { + val json = JsonObject().apply { + add("shouldSendInAppShowError", JsonArray().apply { + add(true) + }) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize empty string value`() { + val json = JsonObject().apply { + addProperty("shouldSendInAppShowError", "") + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } + + @Test + fun `deserialize missing key`() { + val json = JsonObject() + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertTrue(result.toggles.isEmpty()) + } + + @Test + fun `deserialize null value`() { + val json = JsonObject().apply { + add("shouldSendInAppShowError", JsonNull.INSTANCE) + } + + val result = gson.fromJson(json, SettingsDtoBlank.FeatureTogglesDtoBlank::class.java) + + assertNull(result.toggles[SEND_INAPP_SHOW_ERROR_FEATURE]) + } +} diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImplTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImplTest.kt new file mode 100644 index 000000000..0a3dde008 --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/FeatureToggleManagerImplTest.kt @@ -0,0 +1,277 @@ +package cloud.mindbox.mobile_sdk.inapp.data.managers + +import cloud.mindbox.mobile_sdk.models.operation.response.InAppConfigResponse +import cloud.mindbox.mobile_sdk.models.operation.response.SettingsDto +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class FeatureToggleManagerImplTest { + + private lateinit var featureToggleManager: FeatureToggleManagerImpl + + @Before + fun onTestStart() { + featureToggleManager = FeatureToggleManagerImpl() + } + + @Test + fun `applyToggles sets shouldSendInAppShowError to true when featureToggles contains true`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to true) + ), + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles sets shouldSendInAppShowError to false when featureToggles contains false`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to false) + ), + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles handles multiple toggles`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf( + "shouldSendInAppShowError" to true, + "anotherToggle" to false + ) + ), + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + assertEquals(false, featureToggleManager.isEnabled("anotherToggle")) + } + + @Test + fun `applyToggles ignores null values in featureToggles map`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf( + "shouldSendInAppShowError" to true, + "invalidToggle" to null + ) + ), + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + assertEquals(false, featureToggleManager.isEnabled("invalidToggle")) + } + + @Test + fun `applyToggles returns false when featureToggles is null`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = null + ), + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles returns false when settings is null`() { + val config = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = null, + abtests = null + ) + + featureToggleManager.applyToggles(config) + + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles returns false when config is null`() { + featureToggleManager.applyToggles(null) + + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `isEnabled returns false by default`() { + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles can change value from true to false`() { + val configTrue = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to true) + ), + abtests = null + ) + featureToggleManager.applyToggles(configTrue) + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + + val configFalse = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to false) + ), + abtests = null + ) + featureToggleManager.applyToggles(configFalse) + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles can change value from false to true`() { + val configFalse = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to false) + ), + abtests = null + ) + featureToggleManager.applyToggles(configFalse) + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + + val configTrue = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to true) + ), + abtests = null + ) + featureToggleManager.applyToggles(configTrue) + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun `applyToggles clears previous toggles when null config is applied`() { + val configTrue = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("shouldSendInAppShowError" to true) + ), + abtests = null + ) + featureToggleManager.applyToggles(configTrue) + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + + featureToggleManager.applyToggles(null) + assertEquals(false, featureToggleManager.isEnabled("shouldSendInAppShowError")) + } + + @Test + fun `applyToggles clears previous toggles when new config is applied`() { + val config1 = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf( + "shouldSendInAppShowError" to true, + "toggle1" to true + ) + ), + abtests = null + ) + featureToggleManager.applyToggles(config1) + assertEquals(true, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + assertEquals(true, featureToggleManager.isEnabled("toggle1")) + + val config2 = InAppConfigResponse( + inApps = null, + monitoring = null, + settings = SettingsDto( + operations = null, + ttl = null, + slidingExpiration = null, + inapp = null, + featureToggles = mapOf("toggle2" to true) + ), + abtests = null + ) + featureToggleManager.applyToggles(config2) + assertEquals(false, featureToggleManager.isEnabled(SEND_INAPP_SHOW_ERROR_FEATURE)) + assertEquals(false, featureToggleManager.isEnabled("toggle1")) + assertEquals(true, featureToggleManager.isEnabled("toggle2")) + } +} diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/SettingsMobileConfigSerializationManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/SettingsMobileConfigSerializationManagerTest.kt index 9e4623b30..48bb9f388 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/SettingsMobileConfigSerializationManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/SettingsMobileConfigSerializationManagerTest.kt @@ -4,6 +4,7 @@ import android.app.Application import cloud.mindbox.mobile_sdk.di.MindboxDI import cloud.mindbox.mobile_sdk.di.mindboxInject import cloud.mindbox.mobile_sdk.inapp.data.managers.MobileConfigSerializationManagerImpl +import cloud.mindbox.mobile_sdk.inapp.data.managers.SEND_INAPP_SHOW_ERROR_FEATURE import cloud.mindbox.mobile_sdk.models.operation.response.ABTestDto import cloud.mindbox.mobile_sdk.models.operation.response.SdkVersion import io.mockk.every @@ -100,6 +101,7 @@ class SettingsMobileConfigSerializationManagerTest { assertNotNull(config.settings.ttl?.inApps) assertNotNull(config.settings.slidingExpiration?.config) assertNotNull(config.settings.slidingExpiration?.pushTokenKeepalive) + assertNotNull(config.settings.featureToggles) assertNotNull(config.abtests) assertEquals(2, config.abtests!!.size) @@ -126,6 +128,9 @@ class SettingsMobileConfigSerializationManagerTest { assertNotNull(config.inappSettings?.maxInappsPerDay) assertNotNull(config.inappSettings?.maxInappsPerSession) assertNotNull(config.inappSettings?.minIntervalBetweenShows) + + assertNotNull(config.featureToggles) + assertEquals(true, config.featureToggles?.toggles?.get(SEND_INAPP_SHOW_ERROR_FEATURE)) } // MARK: - Operations @@ -633,4 +638,93 @@ class SettingsMobileConfigSerializationManagerTest { assertNull("maxInappsPerDay must be `null` if the value is not a number", config.inappSettings?.maxInappsPerDay) assertNull("minIntervalBetweenShows must be `null` if the value is not a string", config.inappSettings?.minIntervalBetweenShows) } + + // MARK: - FeatureToggles + + @Test + fun settings_config_withFeatureToggles_shouldParseSuccessfully() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesConfig.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("FeatureToggles must be successfully parsed", config.featureToggles) + assertEquals(true, config.featureToggles?.toggles?.get(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun settings_config_withFeatureTogglesError_shouldSetFeatureTogglesToNull() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesError.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("Operations must be successfully parsed", config.operations) + assertNotNull(config.operations?.get("viewProduct")) + assertNotNull(config.operations?.get("viewCategory")) + assertNotNull(config.operations?.get("setCart")) + + assertNotNull("TTL must be successfully parsed", config.ttl) + assertNotNull("TTL must be successfully parsed", config.ttl?.inApps) + + assertNull("FeatureToggles must be `null` if the key `featureToggles` is not found", config.featureToggles) + } + + @Test + fun settings_config_withFeatureTogglesTypeError_shouldSetFeatureTogglesToNull() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesTypeError.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("Operations must be successfully parsed", config.operations) + assertNotNull(config.operations?.get("viewProduct")) + assertNotNull(config.operations?.get("viewCategory")) + assertNotNull(config.operations?.get("setCart")) + + assertNotNull("TTL must be successfully parsed", config.ttl) + assertNotNull("TTL must be successfully parsed", config.ttl?.inApps) + + assertNull( + "FeatureToggles must be `null` if the type of `featureToggles` is not an object", + config.featureToggles + ) + } + + @Test + fun settings_config_withFeatureTogglesShouldSendInAppShowErrorMissing_shouldSetValueToNull() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorMissing.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("Operations must be successfully parsed", config.operations) + assertNotNull(config.operations?.get("viewProduct")) + assertNotNull(config.operations?.get("viewCategory")) + assertNotNull(config.operations?.get("setCart")) + + assertNotNull("TTL must be successfully parsed", config.ttl) + assertNotNull("TTL must be successfully parsed", config.ttl?.inApps) + + assertNotNull("FeatureToggles must be parsed if the object exists", config.featureToggles) + assertTrue("FeatureToggles should be empty if no valid values", config.featureToggles!!.toggles.isEmpty()) + } + + @Test + fun settings_config_withFeatureTogglesShouldSendInAppShowErrorTypeError_shouldSetValueToNull() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorTypeError.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("Operations must be successfully parsed", config.operations) + assertNotNull(config.operations?.get("viewProduct")) + assertNotNull(config.operations?.get("viewCategory")) + assertNotNull(config.operations?.get("setCart")) + + assertNotNull("TTL must be successfully parsed", config.ttl) + assertNotNull("TTL must be successfully parsed", config.ttl?.inApps) + + assertNotNull("FeatureToggles must be parsed if the object exists", config.featureToggles) + assertNull("shouldSendInAppShowError must be `null` if the value is not a boolean", config.featureToggles?.toggles?.get(SEND_INAPP_SHOW_ERROR_FEATURE)) + } + + @Test + fun settings_config_withFeatureTogglesFalse_shouldParseFalse() { + val json = getJson("ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesFalse.json") + val config = manager.deserializeSettings(json)!! + + assertNotNull("FeatureToggles must be successfully parsed", config.featureToggles) + assertEquals(false, config.featureToggles?.toggles?.get("shouldSendInAppShowError")) + } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImplTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImplTest.kt index aaaa78bf2..4d73a19fd 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImplTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/MobileConfigRepositoryImplTest.kt @@ -97,7 +97,8 @@ internal class MobileConfigRepositoryImplTest { sessionStorageManager = mockk(relaxed = true), mobileConfigSettingsManager = mockk(relaxed = true), integerPositiveValidator = mockk(relaxed = true), - inappSettingsManager = mockk(relaxed = true) + inappSettingsManager = mockk(relaxed = true), + featureToggleManager = mockk(relaxed = true) ) } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImplTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImplTest.kt index 57bd95ead..68d5445be 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImplTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImplTest.kt @@ -25,7 +25,7 @@ internal class InAppMessageViewDisplayerImplTest { every { MindboxDI.appModule } returns mockk { every { gson } returns Gson() } - displayer = InAppMessageViewDisplayerImpl(mockk()) + displayer = InAppMessageViewDisplayerImpl(mockk(), mockk()) } @After diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt index c28f1a36f..43d055ec1 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt @@ -148,7 +148,7 @@ class MobileConfigSettingsManagerImplTest { @Test fun `checkPushTokenKeepalive not sends when SlidingExpiration is null`() { every { MindboxPreferences.lastInfoUpdateTime } returns now - val config = InAppConfigResponse(null, null, SettingsDto(null, null, null, null), null) + val config = InAppConfigResponse(null, null, SettingsDto(null, null, null, null, null), null) mobileConfigSettingsManager.checkPushTokenKeepalive(config) verify(exactly = 0) { MindboxEventManager.appKeepalive(any(), any()) } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/SettingsStub.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/SettingsStub.kt index 58f912aee..4767fd3bf 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/SettingsStub.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/SettingsStub.kt @@ -21,7 +21,8 @@ internal class SettingsStub { config = config, pushTokenKeepalive = pushTokenKeepalive ), - inapp = null + inapp = null, + featureToggles = null ), abtests = null ) @@ -43,7 +44,8 @@ internal class SettingsStub { maxInappsPerSession = maxInappsPerSession, maxInappsPerDay = maxInappsPerDay, minIntervalBetweenShows = minIntervalBetweenShows - ) + ), + featureToggles = emptyMap() ), abtests = null ) diff --git a/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json b/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json index 4868abfbc..4234fccae 100644 --- a/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json +++ b/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json @@ -330,6 +330,9 @@ "slidingExpiration": { "config": "0.00:30:00", "pushTokenKeepalive": "0.00:40:00" + }, + "featureToggles": { + "shouldSendInAppShowError": true } }, "abtests": [ diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesConfig.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesConfig.json new file mode 100644 index 000000000..52f42bdab --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesConfig.json @@ -0,0 +1,28 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + }, + "slidingExpiration": { + "config": "0.00:30:00", + "pushTokenKeepalive": "0.00:40:00" + }, + "inapp": { + "maxInappsPerSession": "2147483647", + "maxInappsPerDay": "33", + "minIntervalBetweenShows": "00:30:00" + }, + "featureToggles": { + "shouldSendInAppShowError": true + } +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesError.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesError.json new file mode 100644 index 000000000..ea62a96c9 --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesError.json @@ -0,0 +1,16 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + } +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesFalse.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesFalse.json new file mode 100644 index 000000000..444d36ee4 --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesFalse.json @@ -0,0 +1,19 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + }, + "featureToggles": { + "shouldSendInAppShowError": false + } +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorMissing.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorMissing.json new file mode 100644 index 000000000..e269393a9 --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorMissing.json @@ -0,0 +1,17 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + }, + "featureToggles": {} +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorTypeError.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorTypeError.json new file mode 100644 index 000000000..45423cc62 --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesShouldSendInAppShowErrorTypeError.json @@ -0,0 +1,19 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + }, + "featureToggles": { + "shouldSendInAppShowError": "true" + } +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesTypeError.json b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesTypeError.json new file mode 100644 index 000000000..45cffd769 --- /dev/null +++ b/sdk/src/test/resources/ConfigParsing/Settings/FeatureTogglesErrors/FeatureTogglesTypeError.json @@ -0,0 +1,17 @@ +{ + "operations": { + "viewProduct": { + "systemName": "ViewProduct" + }, + "viewCategory": { + "systemName": "ViewCategory" + }, + "setCart": { + "systemName": "SetCart" + } + }, + "ttl": { + "inapps": "1.00:00:00" + }, + "featureToggles": "not an object" +} diff --git a/sdk/src/test/resources/ConfigParsing/Settings/SettingsConfig.json b/sdk/src/test/resources/ConfigParsing/Settings/SettingsConfig.json index 8deb8fe5d..52f42bdab 100644 --- a/sdk/src/test/resources/ConfigParsing/Settings/SettingsConfig.json +++ b/sdk/src/test/resources/ConfigParsing/Settings/SettingsConfig.json @@ -21,5 +21,8 @@ "maxInappsPerSession": "2147483647", "maxInappsPerDay": "33", "minIntervalBetweenShows": "00:30:00" + }, + "featureToggles": { + "shouldSendInAppShowError": true } }