Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<ID>EmptyFunctionBlock:QProductCenterManager.kt$QProductCenterManager.&lt;no name provided&gt;${}</ID>
<ID>Filename:com.qonversion.android.sdk.internal.storage.util.kt:1</ID>
<ID>FinalNewline:com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt:1</ID>
<ID>FinalNewline:com.qonversion.android.sdk.internal.InternalConfigTest.kt:1</ID>
<ID>FinalNewline:com.qonversion.android.sdk.internal.QHandledPurchasesCacheTest.kt:1</ID>
<ID>FinalNewline:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:1</ID>
<ID>FinalNewline:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:1</ID>
Expand All @@ -33,6 +32,7 @@
<ID>FinalNewline:com.qonversion.android.sdk.utils.kt:1</ID>
<ID>LargeClass:QProductCenterManager.kt$QProductCenterManager : PurchasesListenerUserStateProvider</ID>
<ID>LongMethod:NoCodesSkeletonView.kt$NoCodesSkeletonView$private fun createSkeletonElements()</ID>
<ID>LongMethod:ScreenPresenter.kt$ScreenPresenter$override fun onWebViewMessageReceived(message: String)</ID>
<ID>MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10002</ID>
<ID>MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10003</ID>
<ID>MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10004</ID>
Expand Down Expand Up @@ -95,7 +95,6 @@
<ID>NewLineAtEndOfFile:ApiHelperTest.kt$com.qonversion.android.sdk.internal.api.ApiHelperTest.kt</ID>
<ID>NewLineAtEndOfFile:AppRequestTest.kt$com.qonversion.android.sdk.internal.requests.AppRequestTest.kt</ID>
<ID>NewLineAtEndOfFile:IncrementalDelayCalculatorTest.kt$com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt</ID>
<ID>NewLineAtEndOfFile:InternalConfigTest.kt$com.qonversion.android.sdk.internal.InternalConfigTest.kt</ID>
<ID>NewLineAtEndOfFile:OsRequestTest.kt$com.qonversion.android.sdk.internal.requests.OsRequestTest.kt</ID>
<ID>NewLineAtEndOfFile:ProviderDataRequestTest.kt$com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt</ID>
<ID>NewLineAtEndOfFile:PurchasesCacheTest.kt$com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt</ID>
Expand All @@ -111,6 +110,8 @@
<ID>NoConsecutiveBlankLines:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:18</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.dto.products.QProduct.kt:3</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.AdvertisingProvider.kt:7</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.DeferredPurchasesListenerTest.kt:8</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.DeferredPurchasesListenerTest.kt:9</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.EnvironmentProvider.kt:11</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.IncrementalDelayCalculator.kt:3</ID>
<ID>NoWildcardImports:com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt:3</ID>
Expand Down Expand Up @@ -139,6 +140,7 @@
<ID>ReturnCount:QProductCenterManager.kt$QProductCenterManager$@Synchronized private fun executeProductsBlocks(loadStoreProductsError: QonversionError? = null)</ID>
<ID>ReturnCount:QProductCenterManager.kt$QProductCenterManager$fun identify(identityId: String, callback: QonversionUserCallback? = null)</ID>
<ID>ReturnCount:QProductCenterManager.kt$QProductCenterManager$private fun calculatePurchasePermissionsLocally( purchase: Purchase, callback: QonversionPurchaseCallback?, purchaseError: QonversionError )</ID>
<ID>ReturnCount:ScreenPresenter.kt$ScreenPresenter$private fun injectCustomVariables(completion: () -&gt; Unit)</ID>
<ID>SpacingAroundColon:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:45</ID>
<ID>SpacingAroundCurly:com.qonversion.android.sdk.internal.QAttributionManagerTest.kt:39</ID>
<ID>SpacingAroundCurly:com.qonversion.android.sdk.internal.QAttributionManagerTest.kt:54</ID>
Expand Down
10 changes: 10 additions & 0 deletions nocodes/src/main/java/io/qonversion/nocodes/NoCodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.qonversion.nocodes.interfaces.PurchaseDelegateWithCallbacks
import io.qonversion.nocodes.internal.NoCodesInternal
import io.qonversion.nocodes.internal.di.DependenciesAssembly
import io.qonversion.nocodes.internal.dto.config.InternalConfig
import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.interfaces.ScreenCustomizationDelegate

interface NoCodes {
Expand Down Expand Up @@ -96,6 +97,15 @@ interface NoCodes {
*/
fun setPurchaseDelegate(delegate: PurchaseDelegateWithCallbacks)

/**
* The delegate will be called each time a screen is about to be displayed
* to get custom variables that will be injected into the screen's JavaScript context.
* You can also provide it during the initialization via [NoCodesConfig.Builder.setCustomVariablesDelegate].
*
* @param delegate delegate responsible for providing custom variables.
*/
fun setCustomVariablesDelegate(delegate: CustomVariablesDelegate)

/**
* Show the screen using its context key.
* @param contextKey the context key of the screen which must be shown.
Expand Down
17 changes: 17 additions & 0 deletions nocodes/src/main/java/io/qonversion/nocodes/NoCodesConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.qonversion.nocodes.dto.NoCodesTheme
import io.qonversion.nocodes.interfaces.NoCodesDelegate
import io.qonversion.nocodes.interfaces.PurchaseDelegate
import io.qonversion.nocodes.interfaces.PurchaseDelegateWithCallbacks
import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.interfaces.ScreenCustomizationDelegate
import io.qonversion.nocodes.internal.dto.config.LoggerConfig
import io.qonversion.nocodes.internal.dto.config.NetworkConfig
Expand All @@ -32,6 +33,7 @@ class NoCodesConfig internal constructor(
internal val screenCustomizationDelegate: ScreenCustomizationDelegate?,
internal val purchaseDelegate: PurchaseDelegate?,
internal val purchaseDelegateWithCallbacks: PurchaseDelegateWithCallbacks?,
internal val customVariablesDelegate: CustomVariablesDelegate?,
internal val locale: String?,
internal val theme: NoCodesTheme,
) {
Expand All @@ -54,6 +56,7 @@ class NoCodesConfig internal constructor(
private var screenCustomizationDelegate: ScreenCustomizationDelegate? = null
private var purchaseDelegate: PurchaseDelegate? = null
private var purchaseDelegateWithCallbacks: PurchaseDelegateWithCallbacks? = null
private var customVariablesDelegate: CustomVariablesDelegate? = null
private var proxyUrl: String? = null
private var logLevel = LogLevel.Info
private var logTag = DEFAULT_LOG_TAG
Expand Down Expand Up @@ -109,6 +112,19 @@ class NoCodesConfig internal constructor(
this.purchaseDelegateWithCallbacks = purchaseDelegateWithCallbacks
}

/**
* Provide a delegate to get custom variables for each No-Codes screen.
* Custom variables are injected into the screen's JavaScript context
* and can be used to influence content displayed on the screen.
* You can also provide it later via [NoCodes.setCustomVariablesDelegate].
*
* @param customVariablesDelegate delegate responsible for providing custom variables.
* @return builder instance for chain calls.
*/
fun setCustomVariablesDelegate(customVariablesDelegate: CustomVariablesDelegate): Builder = apply {
this.customVariablesDelegate = customVariablesDelegate
}

/**
* Provide a URL to your proxy server which will redirect all the requests from the No-Codes
* SDK to our API. Please, contact us before using this feature.
Expand Down Expand Up @@ -211,6 +227,7 @@ class NoCodesConfig internal constructor(
screenCustomizationDelegate,
purchaseDelegate,
purchaseDelegateWithCallbacks,
customVariablesDelegate,
locale,
theme,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.qonversion.nocodes.interfaces

/**
* Delegate responsible for providing custom variables for No-Codes screens.
* Custom variables are injected into the screen's JavaScript context
* and can be used to influence content displayed on the screen.
*/
interface CustomVariablesDelegate {

/**
* Provide custom variables for a specific screen identified by context key.
* Called each time a screen is about to be displayed.
*
* @param contextKey the context key of the screen being loaded.
* @return a map of custom variables to inject into the screen.
*/
fun getCustomVariables(contextKey: String): Map<String, String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import io.qonversion.nocodes.dto.NoCodesTheme
import io.qonversion.nocodes.interfaces.NoCodesDelegate
import io.qonversion.nocodes.interfaces.PurchaseDelegate
import io.qonversion.nocodes.interfaces.PurchaseDelegateWithCallbacks
import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.interfaces.ScreenCustomizationDelegate
import io.qonversion.nocodes.internal.di.DependenciesAssembly
import io.qonversion.nocodes.internal.dto.config.InternalConfig
import io.qonversion.nocodes.internal.dto.config.NoCodesDelegateWrapper
import io.qonversion.nocodes.internal.dto.config.PurchaseDelegateWithCallbacksAdapter
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -55,12 +57,34 @@ internal class NoCodesInternal(
internalConfig.purchaseDelegate = PurchaseDelegateWithCallbacksAdapter(delegate)
}

override fun setCustomVariablesDelegate(delegate: CustomVariablesDelegate) {
internalConfig.customVariablesDelegate = delegate
}

override fun showScreen(contextKey: String) {
scope.launch {
suspendFlushPendingUserProperties()
screenController.showScreen(contextKey)
}
}

private suspend fun suspendFlushPendingUserProperties() {
kotlin.coroutines.suspendCoroutine<Unit> { continuation ->
try {
com.qonversion.android.sdk.Qonversion.shared.forceSendProperties(
object : com.qonversion.android.sdk.listeners.QonversionEmptyCallback {
override fun onComplete() {
continuation.resume(Unit)
}
}
)
} catch (e: Exception) {
logger.warn("NoCodesInternal -> Failed to flush pending user properties: ${e.message}")
continuation.resume(Unit)
}
}
}

override fun close() {
runBlocking {
screenEventsService.flushAndWait()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ internal class ControllersAssemblyImpl(
mappersAssembly.actionMapper(),
servicesAssembly.screenEventsService(),
{ miscAssembly.customLocale() },
{ miscAssembly.theme() }
{ miscAssembly.theme() },
{ internalConfig.customVariablesDelegate }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.qonversion.nocodes.NoCodesConfig
import io.qonversion.nocodes.dto.LogLevel
import io.qonversion.nocodes.dto.NoCodesTheme
import io.qonversion.nocodes.interfaces.NoCodesDelegate
import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.interfaces.PurchaseDelegate
import io.qonversion.nocodes.interfaces.ScreenCustomizationDelegate
import io.qonversion.nocodes.internal.provider.LocaleConfigProvider
Expand All @@ -21,6 +22,7 @@ internal class InternalConfig(
override var noCodesDelegate: NoCodesDelegate?,
var screenCustomizationDelegate: ScreenCustomizationDelegate?,
override var purchaseDelegate: PurchaseDelegate?,
var customVariablesDelegate: CustomVariablesDelegate? = null,
override var customLocale: String? = null,
override var theme: NoCodesTheme = NoCodesTheme.Auto,
) : PrimaryConfigProvider,
Expand All @@ -40,6 +42,7 @@ internal class InternalConfig(
// If PurchaseDelegate is provided, use it directly. Otherwise, wrap PurchaseDelegateWithCallbacks.
noCodesConfig.purchaseDelegate
?: noCodesConfig.purchaseDelegateWithCallbacks?.let { PurchaseDelegateWithCallbacksAdapter(it) },
noCodesConfig.customVariablesDelegate,
noCodesConfig.locale,
noCodesConfig.theme
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal class ScreenContract {
fun handleGetContext(variables: List<String>)

fun finishScreenPreparation()

fun setVariable(name: String, value: String, completion: () -> Unit = {})
}

internal interface Presenter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,23 +325,42 @@ class ScreenFragment : Fragment(), ScreenContract.View {
override fun handleGetContext(variables: List<String>) {
val productIds = extractProductIds(variables)

var activeIds: List<String> = emptyList()
var fetchedUserProperties: Map<String, String> = emptyMap()
var remainingLoads = 2
val lock = Any()

fun onLoadComplete() {
synchronized(lock) {
remainingLoads--
if (remainingLoads > 0) return
}
loadProductsAndSendContext(activeIds, productIds, fetchedUserProperties)
}

Qonversion.shared.checkEntitlements(object : QonversionEntitlementsCallback {
override fun onSuccess(entitlements: Map<String, QEntitlement>) {
val activeIds = entitlements.filter { it.value.isActive }.map { it.key }
if (productIds.isNotEmpty()) {
loadProductsAndSendContext(activeIds, productIds)
} else {
sendContextResponse(activeIds, org.json.JSONObject())
}
activeIds = entitlements.filter { it.value.isActive }.map { it.key }
onLoadComplete()
}

override fun onError(error: QonversionError) {
logger.error("Failed to load entitlements for context: ${error.description}")
if (productIds.isNotEmpty()) {
loadProductsAndSendContext(emptyList(), productIds)
} else {
sendContextResponse(emptyList(), org.json.JSONObject())
}
onLoadComplete()
}
})

Qonversion.shared.userProperties(object : com.qonversion.android.sdk.listeners.QonversionUserPropertiesCallback {
override fun onSuccess(userProperties: com.qonversion.android.sdk.dto.properties.QUserProperties) {
val map = mutableMapOf<String, String>()
userProperties.properties.forEach { map[it.key] = it.value }
fetchedUserProperties = map
onLoadComplete()
}

override fun onError(error: QonversionError) {
logger.error("Failed to load user properties for context: ${error.description}")
onLoadComplete()
}
})
}
Expand All @@ -355,16 +374,25 @@ class ScreenFragment : Fragment(), ScreenContract.View {
}.distinct()
}

private fun loadProductsAndSendContext(activeIds: List<String>, productIds: List<String>) {
private fun loadProductsAndSendContext(
activeIds: List<String>,
productIds: List<String>,
userProperties: Map<String, String> = emptyMap()
) {
if (productIds.isEmpty()) {
sendContextResponse(activeIds, org.json.JSONObject(), userProperties)
return
}

Qonversion.shared.products(object : QonversionProductsCallback {
override fun onSuccess(products: Map<String, QProduct>) {
val productsContext = buildProductsContextJSON(products, productIds)
sendContextResponse(activeIds, productsContext)
sendContextResponse(activeIds, productsContext, userProperties)
}

override fun onError(error: QonversionError) {
logger.error("Failed to load products for context: ${error.description}")
sendContextResponse(activeIds, org.json.JSONObject())
sendContextResponse(activeIds, org.json.JSONObject(), userProperties)
}
})
}
Expand Down Expand Up @@ -399,7 +427,11 @@ class ScreenFragment : Fragment(), ScreenContract.View {
return json
}

private fun sendContextResponse(activeEntitlementIds: List<String>, productsContext: org.json.JSONObject) {
private fun sendContextResponse(
activeEntitlementIds: List<String>,
productsContext: org.json.JSONObject,
userProperties: Map<String, String> = emptyMap()
) {
val deviceJson = org.json.JSONObject()
deviceJson.put("platform", "Android")
deviceJson.put("osVersion", Build.VERSION.RELEASE)
Expand Down Expand Up @@ -430,6 +462,9 @@ class ScreenFragment : Fragment(), ScreenContract.View {
userJson.put("daysSinceInstall", calculateDaysSinceInstall())
userJson.put("hasAnyEntitlement", if (activeEntitlementIds.isNotEmpty()) "true" else "false")
userJson.put("entitlements", org.json.JSONArray(activeEntitlementIds))
if (userProperties.isNotEmpty()) {
userJson.put("properties", org.json.JSONObject(userProperties as Map<*, *>))
}

val dataJson = org.json.JSONObject()
dataJson.put("device", deviceJson)
Expand All @@ -450,10 +485,26 @@ class ScreenFragment : Fragment(), ScreenContract.View {
}

override fun finishScreenPreparation() {
binding?.progressBarLayout?.progressBar?.visibility = View.GONE
binding?.webView?.visibility = View.VISIBLE
loadingView?.stopAnimating()
binding?.loadingViewContainer?.visibility = View.GONE
activity?.runOnUiThread {
binding?.progressBarLayout?.progressBar?.visibility = View.GONE
binding?.webView?.visibility = View.VISIBLE
loadingView?.stopAnimating()
binding?.loadingViewContainer?.visibility = View.GONE
}
}

override fun setVariable(name: String, value: String, completion: () -> Unit) {
val escapedName = name.replace("\\", "\\\\").replace("\"", "\\\"")
val escapedValue = value.replace("\\", "\\\\").replace("\"", "\\\"")
val js = "window.noCodesSetVariable?.(\"$escapedName\", \"$escapedValue\");"
val act = activity
if (act == null) {
completion()
return
}
act.runOnUiThread {
binding?.webView?.evaluateJavascript(js) { completion() }
}
}

@JavascriptInterface
Expand Down
Loading
Loading