Skip to content

Commit

Permalink
Loading remote config with context key (#562)
Browse files Browse the repository at this point in the history
* Loading remote config with context key

* CR fixes

* Testing fixes

* Detekt fixes
  • Loading branch information
SpertsyanKM authored Mar 8, 2024
1 parent 1cb4918 commit 3c657fc
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 47 deletions.
3 changes: 2 additions & 1 deletion config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<ID>ComplexMethod:ScreenPresenter.kt$ScreenPresenter$override fun shouldOverrideUrlLoading(url: String?): Boolean</ID>
<ID>ComplexMethod:errors.kt$internal fun BillingError.toQonversionError(): QonversionError</ID>
<ID>ConstructorParameterNaming:Environment.kt$Environment$@Json(name = "app_version") val app_version: String</ID>
<ID>ConstructorParameterNaming:QRemoteConfig.kt$QRemoteConfig$@Json(name = "source") internal val _source: QRemoteConfigurationSource?</ID>
<ID>EmptyCatchBlock:AdvertisingProvider.kt$AdvertisingProvider.AdvertisingConnection${ }</ID>
<ID>EmptyFunctionBlock:AdvertisingProvider.kt$AdvertisingProvider.AdvertisingConnection${}</ID>
<ID>EmptyFunctionBlock:QAutomationsManagerTest.kt$QAutomationsManagerTest.&lt;no name provided&gt;${}</ID>
Expand Down Expand Up @@ -113,6 +112,7 @@
<ID>MaxLineLength:QonversionBillingService.kt$QonversionBillingService${ error -&gt; logger.release("Failed to fetch product type for purchase $productId - " + error.message) }</ID>
<ID>MaxLineLength:QonversionConfig.kt$QonversionConfig.Builder$*</ID>
<ID>MaxLineLength:QonversionError.kt$QonversionErrorCode$*</ID>
<ID>MaxLineLength:QonversionError.kt$QonversionErrorCode$RemoteConfigurationNotAvailable : QonversionErrorCode</ID>
<ID>MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"""HTTP status code=400, data={"message":"Invalid access token received","code":10003,"status":400,"extra":[]}. """</ID>
<ID>MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lcbfeigohklhpdgmpildjabg.AO-J1OyV-EE2bKGqDcRCvqjZ2NI1uHDRuvonRn5RorP6LNsyK7yHK8FaFlXp6bjTEX3-4JvZKtbY_bpquKBfux09Mfkx05M9YGZsfsr5BJk74r719m77Oyo"</ID>
<ID>MaxLineLength:QonversionRepositoryIntegrationTest.kt$QonversionRepositoryIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY"</ID>
Expand Down Expand Up @@ -140,6 +140,7 @@
<ID>MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:135</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:142</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.automations.mvp.ScreenPresenterTest.kt:159</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.dto.QonversionError.kt:45</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.dto.products.QProductStoreDetails.kt:130</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:213</ID>
<ID>MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:370</ID>
Expand Down
9 changes: 8 additions & 1 deletion sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.squareup.moshi.JsonClass
data class QRemoteConfig internal constructor(
@Json(name = "payload") val payload: Map<String, Any>,
@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!!
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,84 +13,96 @@ internal class QRemoteConfigManager @Inject constructor(
private val remoteConfigService: QRemoteConfigService,
private val internalConfig: InternalConfig
) {
internal class LoadingState(
var loadedConfig: QRemoteConfig? = null,
val callbacks: MutableList<QonversionRemoteConfigCallback> = mutableListOf(),
var isInProgress: Boolean = false
)

lateinit var userStateProvider: UserStateProvider
private var currentRemoteConfig: QRemoteConfig? = null
private var remoteConfigCallbacks = mutableListOf<QonversionRemoteConfigCallback>()
private var isRequestInProgress: Boolean = false
private var loadingStates = mutableMapOf<String?, LoadingState>()

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)
}

fun attachUserToRemoteConfiguration(
remoteConfigurationId: String,
callback: QonversionRemoteConfigurationAttachCallback
) {
currentRemoteConfig = null
loadingStates[null]?.loadedConfig = null
remoteConfigService.attachUserToRemoteConfiguration(remoteConfigurationId, internalConfig.uid, callback)
}

fun detachUserFromRemoteConfiguration(
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() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,19 @@ 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()}")
val body = it.body()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 3c657fc

Please sign in to comment.