From 3c657fcaa1a5d5acdbdfbcc512e4cfb793f0378c Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Fri, 8 Mar 2024 13:54:03 +0300 Subject: [PATCH 1/2] Loading remote config with context key (#562) * Loading remote config with context key * CR fixes * Testing fixes * Detekt fixes --- config/detekt/baseline.xml | 3 +- .../com/qonversion/android/sdk/Qonversion.kt | 9 ++- .../android/sdk/dto/QRemoteConfig.kt | 4 +- .../sdk/dto/QRemoteConfigurationSource.kt | 5 +- .../android/sdk/dto/QonversionError.kt | 2 +- .../sdk/internal/QRemoteConfigManager.kt | 74 +++++++++++-------- .../sdk/internal/QonversionInternal.kt | 10 ++- .../internal/repository/DefaultRepository.kt | 9 ++- .../sdk/internal/repository/QRepository.kt | 2 +- .../repository/RepositoryWithRateLimits.kt | 6 +- .../internal/services/QRemoteConfigService.kt | 8 +- 11 files changed, 85 insertions(+), 47 deletions(-) diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 466ca274..b8f6af31 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -6,7 +6,6 @@ ComplexMethod:ScreenPresenter.kt$ScreenPresenter$override fun shouldOverrideUrlLoading(url: String?): Boolean ComplexMethod:errors.kt$internal fun BillingError.toQonversionError(): QonversionError ConstructorParameterNaming:Environment.kt$Environment$@Json(name = "app_version") val app_version: String - ConstructorParameterNaming:QRemoteConfig.kt$QRemoteConfig$@Json(name = "source") internal val _source: QRemoteConfigurationSource? EmptyCatchBlock:AdvertisingProvider.kt$AdvertisingProvider.AdvertisingConnection${ } EmptyFunctionBlock:AdvertisingProvider.kt$AdvertisingProvider.AdvertisingConnection${} EmptyFunctionBlock:QAutomationsManagerTest.kt$QAutomationsManagerTest.<no name provided>${} @@ -113,6 +112,7 @@ MaxLineLength:QonversionBillingService.kt$QonversionBillingService${ error -> logger.release("Failed to fetch product type for purchase $productId - " + error.message) } MaxLineLength:QonversionConfig.kt$QonversionConfig.Builder$* MaxLineLength:QonversionError.kt$QonversionErrorCode$* + MaxLineLength:QonversionError.kt$QonversionErrorCode$RemoteConfigurationNotAvailable : QonversionErrorCode MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"""HTTP status code=400, data={"message":"Invalid access token received","code":10003,"status":400,"extra":[]}. """ MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lcbfeigohklhpdgmpildjabg.AO-J1OyV-EE2bKGqDcRCvqjZ2NI1uHDRuvonRn5RorP6LNsyK7yHK8FaFlXp6bjTEX3-4JvZKtbY_bpquKBfux09Mfkx05M9YGZsfsr5BJk74r719m77Oyo" MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" @@ -140,6 +140,7 @@ MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:135 MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:142 MaximumLineLength:com.qonversion.android.sdk.automations.mvp.ScreenPresenterTest.kt:159 + MaximumLineLength:com.qonversion.android.sdk.dto.QonversionError.kt:45 MaximumLineLength:com.qonversion.android.sdk.dto.products.QProductStoreDetails.kt:130 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:213 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:370 diff --git a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt index 2ff93d21..3fce05a9 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt @@ -116,12 +116,19 @@ interface Qonversion { fun offerings(callback: QonversionOfferingsCallback) /** - * Returns Qonversion remote config object + * Returns default Qonversion remote config object * Use this function to get the remote config with specific payload and experiment info. * @param callback - callback that will be called when response is received */ fun remoteConfig(callback: QonversionRemoteConfigCallback) + /** + * Returns Qonversion remote config object by [contextKey]. + * Use this function to get the remote config with specific payload and experiment info. + * @param callback - callback that will be called when response is received + */ + fun remoteConfig(contextKey: String, callback: QonversionRemoteConfigCallback) + /** * This function should be used for the test purposes only. Do not forget to delete the usage of this function before the release. * Use this function to attach the user to the experiment. diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfig.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfig.kt index 457d64e6..d6d4257b 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfig.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfig.kt @@ -8,7 +8,7 @@ import com.squareup.moshi.JsonClass data class QRemoteConfig internal constructor( @Json(name = "payload") val payload: Map, @Json(name = "experiment") val experiment: QExperiment?, - @Json(name = "source") internal val _source: QRemoteConfigurationSource? + @Json(name = "source") internal val sourceApi: QRemoteConfigurationSource? ) { - val source: QRemoteConfigurationSource get() = _source!! + val source: QRemoteConfigurationSource get() = sourceApi!! } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigurationSource.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigurationSource.kt index 1badb515..c52d4c67 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigurationSource.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigurationSource.kt @@ -9,4 +9,7 @@ data class QRemoteConfigurationSource( @Json(name = "name") val name: String, @Json(name = "assignment_type") val assignmentType: QRemoteConfigurationAssignmentType, @Json(name = "type") val type: QRemoteConfigurationSourceType, -) + @Json(name = "context_key") internal val contextKeyApi: String? +) { + val contextKey: String? = contextKeyApi?.takeIf { it.isNotEmpty() } +} diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt index b9639e6e..0a41eca1 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt @@ -42,6 +42,6 @@ enum class QonversionErrorCode(val specification: String) { FraudPurchase("Fraud purchase was detected"), ProjectConfigError("The project is not configured or configured incorrectly in the Qonversion Dashboard"), InvalidStoreCredentials("This account does not have access to the requested application"), - RemoteConfigurationNotAvailable("Remote configuration is not available for the current user"), + RemoteConfigurationNotAvailable("Remote configuration is not available for the current user or for the provided context key"), ApiRateLimitExceeded("API requests rate limit exceeded"), } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt index b15616f7..4ea01f0d 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt @@ -13,62 +13,71 @@ internal class QRemoteConfigManager @Inject constructor( private val remoteConfigService: QRemoteConfigService, private val internalConfig: InternalConfig ) { + internal class LoadingState( + var loadedConfig: QRemoteConfig? = null, + val callbacks: MutableList = mutableListOf(), + var isInProgress: Boolean = false + ) + lateinit var userStateProvider: UserStateProvider - private var currentRemoteConfig: QRemoteConfig? = null - private var remoteConfigCallbacks = mutableListOf() - private var isRequestInProgress: Boolean = false + private var loadingStates = mutableMapOf() fun handlePendingRequests() { - if (remoteConfigCallbacks.isNotEmpty()) { - loadRemoteConfig(null) - } + loadingStates.filter { it.value.callbacks.isNotEmpty() } + .keys.forEach { contextKey -> loadRemoteConfig(contextKey, null) } } fun userChangingRequestFailedWithError(error: QonversionError) { - fireToCallbacks { onError(error) } + loadingStates.keys.forEach { + fireToCallbacks(it) { onError(error) } + } } fun onUserUpdate() { - currentRemoteConfig = null + loadingStates = mutableMapOf() } - fun loadRemoteConfig(callback: QonversionRemoteConfigCallback?) { - currentRemoteConfig?.takeIf { userStateProvider.isUserStable }?.let { - callback?.onSuccess(it) - return - } + fun loadRemoteConfig(contextKey: String?, callback: QonversionRemoteConfigCallback?) { + loadingStates[contextKey] + ?.loadedConfig + ?.takeIf { userStateProvider.isUserStable } + ?.let { + callback?.onSuccess(it) + return + } + + val loadingState = loadingStates[contextKey] ?: LoadingState() + loadingStates[contextKey] = loadingState callback?.let { - remoteConfigCallbacks.add(it) + loadingState.callbacks.add(it) } - if (!userStateProvider.isUserStable || isRequestInProgress) { + if (!userStateProvider.isUserStable || loadingState.isInProgress) { return } - isRequestInProgress = true - currentRemoteConfig = null - remoteConfigService.loadRemoteConfig(internalConfig.uid, object : QonversionRemoteConfigCallback { + loadingState.isInProgress = true + loadingState.loadedConfig = null + remoteConfigService.loadRemoteConfig(internalConfig.uid, contextKey, object : QonversionRemoteConfigCallback { override fun onSuccess(remoteConfig: QRemoteConfig) { - isRequestInProgress = false - currentRemoteConfig = remoteConfig - fireToCallbacks { onSuccess(remoteConfig) } + loadingState.loadedConfig = remoteConfig + fireToCallbacks(contextKey) { onSuccess(remoteConfig) } } override fun onError(error: QonversionError) { - isRequestInProgress = false - fireToCallbacks { onError(error) } + fireToCallbacks(contextKey) { onError(error) } } }) } fun attachUserToExperiment(experimentId: String, groupId: String, callback: QonversionExperimentAttachCallback) { - currentRemoteConfig = null + loadingStates[null]?.loadedConfig = null remoteConfigService.attachUserToExperiment(experimentId, groupId, internalConfig.uid, callback) } fun detachUserFromExperiment(experimentId: String, callback: QonversionExperimentAttachCallback) { - currentRemoteConfig = null + loadingStates[null]?.loadedConfig = null remoteConfigService.detachUserFromExperiment(experimentId, internalConfig.uid, callback) } @@ -76,7 +85,7 @@ internal class QRemoteConfigManager @Inject constructor( remoteConfigurationId: String, callback: QonversionRemoteConfigurationAttachCallback ) { - currentRemoteConfig = null + loadingStates[null]?.loadedConfig = null remoteConfigService.attachUserToRemoteConfiguration(remoteConfigurationId, internalConfig.uid, callback) } @@ -84,13 +93,16 @@ internal class QRemoteConfigManager @Inject constructor( remoteConfigurationId: String, callback: QonversionRemoteConfigurationAttachCallback ) { - currentRemoteConfig = null + loadingStates[null]?.loadedConfig = null remoteConfigService.detachUserFromRemoteConfiguration(remoteConfigurationId, internalConfig.uid, callback) } - private fun fireToCallbacks(action: QonversionRemoteConfigCallback.() -> Unit) { - val callbacks = remoteConfigCallbacks.toList() - callbacks.forEach { it.action() } - remoteConfigCallbacks.clear() + private fun fireToCallbacks(contextKey: String?, action: QonversionRemoteConfigCallback.() -> Unit) { + loadingStates[contextKey]?.let { loadingState -> + loadingState.isInProgress = false + val callbacks = loadingState.callbacks.toList() + loadingState.callbacks.clear() + callbacks.forEach { it.action() } + } } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt index f6574166..13b81d8e 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt @@ -194,7 +194,15 @@ internal class QonversionInternal( } override fun remoteConfig(callback: QonversionRemoteConfigCallback) { - remoteConfigManager?.loadRemoteConfig(object : QonversionRemoteConfigCallback { + loadRemoteConfig(null, callback) + } + + override fun remoteConfig(contextKey: String, callback: QonversionRemoteConfigCallback) { + loadRemoteConfig(contextKey, callback) + } + + private fun loadRemoteConfig(contextKey: String?, callback: QonversionRemoteConfigCallback) { + remoteConfigManager?.loadRemoteConfig(contextKey, object : QonversionRemoteConfigCallback { override fun onSuccess(remoteConfig: QRemoteConfig) { postToMainThread { callback.onSuccess(remoteConfig) } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt index 72f513db..cb330e30 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt @@ -82,8 +82,11 @@ internal class DefaultRepository internal constructor( initRequest(initRequestData.purchases, initRequestData.callback) } - override fun remoteConfig(userID: String, callback: QonversionRemoteConfigCallback) { - val queryParams = mapOf("user_id" to userID) + override fun remoteConfig(userID: String, contextKey: String?, callback: QonversionRemoteConfigCallback) { + val queryParams = mapOf("user_id" to userID, "context_key" to contextKey) + .filterValues { it != null } + .mapValues { it.value!! } + api.remoteConfig(queryParams).enqueue { onResponse = { logger.debug("remoteConfigRequest - ${it.getLogMessage()}") @@ -91,7 +94,7 @@ internal class DefaultRepository internal constructor( if (body == null) { callback.onError(errorMapper.getErrorFromResponse(it)) } else { - if (body.payload.isEmpty() && body._source == null) { + if (body.payload.isEmpty() && body.sourceApi == null) { callback.onError(QonversionError(QonversionErrorCode.RemoteConfigurationNotAvailable)) } else { callback.onSuccess(body) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/QRepository.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/QRepository.kt index 55f2c0ac..3952957f 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/QRepository.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/QRepository.kt @@ -19,7 +19,7 @@ internal interface QRepository { fun init(initRequestData: InitRequestData) - fun remoteConfig(userID: String, callback: QonversionRemoteConfigCallback) + fun remoteConfig(userID: String, contextKey: String?, callback: QonversionRemoteConfigCallback) fun attachUserToExperiment( experimentId: String, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt index a1e608b2..bdbd08c9 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt @@ -32,13 +32,13 @@ internal class RepositoryWithRateLimits( } } - override fun remoteConfig(userID: String, callback: QonversionRemoteConfigCallback) { + override fun remoteConfig(userID: String, contextKey: String?, callback: QonversionRemoteConfigCallback) { withRateLimitCheck( RequestType.RemoteConfig, - userID.hashCode(), + (userID + contextKey).hashCode(), { error -> callback.onError(error) } ) { - repository.remoteConfig(userID, callback) + repository.remoteConfig(userID, contextKey, callback) } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QRemoteConfigService.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QRemoteConfigService.kt index e7ff5657..4ff9b453 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QRemoteConfigService.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QRemoteConfigService.kt @@ -9,8 +9,12 @@ import javax.inject.Inject internal class QRemoteConfigService @Inject constructor( private val repository: QRepository ) { - fun loadRemoteConfig(userId: String, callback: QonversionRemoteConfigCallback) { - repository.remoteConfig(userId, callback) + fun loadRemoteConfig( + userId: String, + contextKey: String?, + callback: QonversionRemoteConfigCallback + ) { + repository.remoteConfig(userId, contextKey, callback) } fun attachUserToExperiment( From daa0477a7178d9181afee5b4831ea665c4c5b7dd Mon Sep 17 00:00:00 2001 From: SpertsyanKM Date: Fri, 8 Mar 2024 11:05:11 +0000 Subject: [PATCH 2/2] [create-pull-request] automated change --- build.gradle | 2 +- fastlane/report.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e0174107..0aff0fa2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask buildscript { ext { release = [ - versionName: "7.1.0", + versionName: "7.2.0", versionCode: 1 ] } diff --git a/fastlane/report.xml b/fastlane/report.xml index 2ffb37cd..1c012a86 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,7 +5,7 @@ - +