diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
index e3652717..256ecbb5 100644
--- a/config/detekt/baseline.xml
+++ b/config/detekt/baseline.xml
@@ -19,7 +19,6 @@
EmptyFunctionBlock:QProductCenterManager.kt$QProductCenterManager.<no name provided>${}
Filename:com.qonversion.android.sdk.internal.storage.util.kt:1
FinalNewline:com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt:1
- FinalNewline:com.qonversion.android.sdk.internal.InternalConfigTest.kt:1
FinalNewline:com.qonversion.android.sdk.internal.QHandledPurchasesCacheTest.kt:1
FinalNewline:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:1
FinalNewline:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:1
@@ -33,6 +32,7 @@
FinalNewline:com.qonversion.android.sdk.utils.kt:1
LargeClass:QProductCenterManager.kt$QProductCenterManager : PurchasesListenerUserStateProvider
LongMethod:NoCodesSkeletonView.kt$NoCodesSkeletonView$private fun createSkeletonElements()
+ LongMethod:ScreenPresenter.kt$ScreenPresenter$override fun onWebViewMessageReceived(message: String)
MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10002
MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10003
MagicNumber:ApiErrorMapper.kt$ApiErrorMapper$10004
@@ -95,7 +95,6 @@
NewLineAtEndOfFile:ApiHelperTest.kt$com.qonversion.android.sdk.internal.api.ApiHelperTest.kt
NewLineAtEndOfFile:AppRequestTest.kt$com.qonversion.android.sdk.internal.requests.AppRequestTest.kt
NewLineAtEndOfFile:IncrementalDelayCalculatorTest.kt$com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt
- NewLineAtEndOfFile:InternalConfigTest.kt$com.qonversion.android.sdk.internal.InternalConfigTest.kt
NewLineAtEndOfFile:OsRequestTest.kt$com.qonversion.android.sdk.internal.requests.OsRequestTest.kt
NewLineAtEndOfFile:ProviderDataRequestTest.kt$com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt
NewLineAtEndOfFile:PurchasesCacheTest.kt$com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt
@@ -111,6 +110,8 @@
NoConsecutiveBlankLines:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:18
NoWildcardImports:com.qonversion.android.sdk.dto.products.QProduct.kt:3
NoWildcardImports:com.qonversion.android.sdk.internal.AdvertisingProvider.kt:7
+ NoWildcardImports:com.qonversion.android.sdk.internal.DeferredPurchasesListenerTest.kt:8
+ NoWildcardImports:com.qonversion.android.sdk.internal.DeferredPurchasesListenerTest.kt:9
NoWildcardImports:com.qonversion.android.sdk.internal.EnvironmentProvider.kt:11
NoWildcardImports:com.qonversion.android.sdk.internal.IncrementalDelayCalculator.kt:3
NoWildcardImports:com.qonversion.android.sdk.internal.IncrementalDelayCalculatorTest.kt:3
@@ -139,6 +140,7 @@
ReturnCount:QProductCenterManager.kt$QProductCenterManager$@Synchronized private fun executeProductsBlocks(loadStoreProductsError: QonversionError? = null)
ReturnCount:QProductCenterManager.kt$QProductCenterManager$fun identify(identityId: String, callback: QonversionUserCallback? = null)
ReturnCount:QProductCenterManager.kt$QProductCenterManager$private fun calculatePurchasePermissionsLocally( purchase: Purchase, callback: QonversionPurchaseCallback?, purchaseError: QonversionError )
+ ReturnCount:ScreenPresenter.kt$ScreenPresenter$private fun injectCustomVariables(completion: () -> Unit)
SpacingAroundColon:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:45
SpacingAroundCurly:com.qonversion.android.sdk.internal.QAttributionManagerTest.kt:39
SpacingAroundCurly:com.qonversion.android.sdk.internal.QAttributionManagerTest.kt:54
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/NoCodes.kt b/nocodes/src/main/java/io/qonversion/nocodes/NoCodes.kt
index 6efd48dc..37c1b617 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/NoCodes.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/NoCodes.kt
@@ -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 {
@@ -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.
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/NoCodesConfig.kt b/nocodes/src/main/java/io/qonversion/nocodes/NoCodesConfig.kt
index 7ddbf434..f1002201 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/NoCodesConfig.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/NoCodesConfig.kt
@@ -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
@@ -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,
) {
@@ -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
@@ -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.
@@ -211,6 +227,7 @@ class NoCodesConfig internal constructor(
screenCustomizationDelegate,
purchaseDelegate,
purchaseDelegateWithCallbacks,
+ customVariablesDelegate,
locale,
theme,
)
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/interfaces/CustomVariablesDelegate.kt b/nocodes/src/main/java/io/qonversion/nocodes/interfaces/CustomVariablesDelegate.kt
new file mode 100644
index 00000000..f32c333c
--- /dev/null
+++ b/nocodes/src/main/java/io/qonversion/nocodes/interfaces/CustomVariablesDelegate.kt
@@ -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
+}
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/NoCodesInternal.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/NoCodesInternal.kt
index 19ea0f79..ef0988db 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/NoCodesInternal.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/NoCodesInternal.kt
@@ -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
@@ -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 { 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()
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/di/controllers/ControllersAssemblyImpl.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/di/controllers/ControllersAssemblyImpl.kt
index bf3a6fea..347de7b9 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/di/controllers/ControllersAssemblyImpl.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/di/controllers/ControllersAssemblyImpl.kt
@@ -37,7 +37,8 @@ internal class ControllersAssemblyImpl(
mappersAssembly.actionMapper(),
servicesAssembly.screenEventsService(),
{ miscAssembly.customLocale() },
- { miscAssembly.theme() }
+ { miscAssembly.theme() },
+ { internalConfig.customVariablesDelegate }
)
}
}
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/dto/config/InternalConfig.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/dto/config/InternalConfig.kt
index 88e5b89a..2a82848a 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/dto/config/InternalConfig.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/dto/config/InternalConfig.kt
@@ -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
@@ -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,
@@ -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
)
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenContract.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenContract.kt
index 6c760318..3c83f1b5 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenContract.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenContract.kt
@@ -27,6 +27,8 @@ internal class ScreenContract {
fun handleGetContext(variables: List)
fun finishScreenPreparation()
+
+ fun setVariable(name: String, value: String, completion: () -> Unit = {})
}
internal interface Presenter {
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenFragment.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenFragment.kt
index f5fbd493..092b479e 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenFragment.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenFragment.kt
@@ -325,23 +325,42 @@ class ScreenFragment : Fragment(), ScreenContract.View {
override fun handleGetContext(variables: List) {
val productIds = extractProductIds(variables)
+ var activeIds: List = emptyList()
+ var fetchedUserProperties: Map = 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) {
- 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()
+ 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()
}
})
}
@@ -355,16 +374,25 @@ class ScreenFragment : Fragment(), ScreenContract.View {
}.distinct()
}
- private fun loadProductsAndSendContext(activeIds: List, productIds: List) {
+ private fun loadProductsAndSendContext(
+ activeIds: List,
+ productIds: List,
+ userProperties: Map = emptyMap()
+ ) {
+ if (productIds.isEmpty()) {
+ sendContextResponse(activeIds, org.json.JSONObject(), userProperties)
+ return
+ }
+
Qonversion.shared.products(object : QonversionProductsCallback {
override fun onSuccess(products: Map) {
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)
}
})
}
@@ -399,7 +427,11 @@ class ScreenFragment : Fragment(), ScreenContract.View {
return json
}
- private fun sendContextResponse(activeEntitlementIds: List, productsContext: org.json.JSONObject) {
+ private fun sendContextResponse(
+ activeEntitlementIds: List,
+ productsContext: org.json.JSONObject,
+ userProperties: Map = emptyMap()
+ ) {
val deviceJson = org.json.JSONObject()
deviceJson.put("platform", "Android")
deviceJson.put("osVersion", Build.VERSION.RELEASE)
@@ -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)
@@ -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
diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenPresenter.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenPresenter.kt
index 2dc755cf..63a9d6a1 100644
--- a/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenPresenter.kt
+++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/screen/view/ScreenPresenter.kt
@@ -15,6 +15,7 @@ import io.qonversion.nocodes.error.NoCodesError
import io.qonversion.nocodes.error.NoCodesException
import io.qonversion.nocodes.internal.common.BaseClass
import io.qonversion.nocodes.internal.common.mappers.Mapper
+import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.internal.common.serializers.Serializer
import io.qonversion.nocodes.internal.dto.NoCodeScreen
import io.qonversion.nocodes.internal.dto.ScreenEvent
@@ -38,6 +39,7 @@ internal class ScreenPresenter(
private val screenEventsService: ScreenEventsService,
private val customLocaleProvider: () -> String? = { null },
private val themeProvider: () -> NoCodesTheme = { NoCodesTheme.Auto },
+ private val customVariablesDelegateProvider: () -> CustomVariablesDelegate? = { null },
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : ScreenContract.Presenter, BaseClass(logger) {
@@ -165,7 +167,9 @@ internal class ScreenPresenter(
view.handleGetContext(variables)
}
QAction.Type.ShowScreen -> {
- view.finishScreenPreparation()
+ injectCustomVariables {
+ view.finishScreenPreparation()
+ }
}
QAction.Type.Navigation -> {
action.parameters?.get(QAction.Parameter.ScreenId)?.let { screenId ->
@@ -338,6 +342,36 @@ internal class ScreenPresenter(
return res
}
+ private fun injectCustomVariables(completion: () -> Unit) {
+ val contextKey = currentScreen?.contextKey
+ if (contextKey == null) {
+ completion()
+ return
+ }
+ val delegate = customVariablesDelegateProvider()
+ if (delegate == null) {
+ completion()
+ return
+ }
+ val variables = delegate.getCustomVariables(contextKey)
+ if (variables.isEmpty()) {
+ completion()
+ return
+ }
+
+ val entries = variables.entries.toList()
+ var remaining = entries.size
+
+ for ((name, value) in entries) {
+ view.setVariable(name, value) {
+ remaining--
+ if (remaining == 0) {
+ completion()
+ }
+ }
+ }
+ }
+
private fun loadNextScreen(screenId: String) {
try {
scope.launch {
diff --git a/sample/src/main/java/io/qonversion/sample/NoCodesFragment.kt b/sample/src/main/java/io/qonversion/sample/NoCodesFragment.kt
index 203fd4cc..c4d49720 100644
--- a/sample/src/main/java/io/qonversion/sample/NoCodesFragment.kt
+++ b/sample/src/main/java/io/qonversion/sample/NoCodesFragment.kt
@@ -10,10 +10,11 @@ import io.qonversion.nocodes.NoCodes
import io.qonversion.nocodes.dto.NoCodesTheme
import io.qonversion.nocodes.dto.QAction
import io.qonversion.nocodes.error.NoCodesError
+import io.qonversion.nocodes.interfaces.CustomVariablesDelegate
import io.qonversion.nocodes.interfaces.NoCodesDelegate
import io.qonversion.sample.databinding.FragmentNocodesBinding
-class NoCodesFragment : Fragment(), NoCodesDelegate {
+class NoCodesFragment : Fragment(), NoCodesDelegate, CustomVariablesDelegate {
private var _binding: FragmentNocodesBinding? = null
private val binding get() = _binding!!
@@ -78,9 +79,17 @@ class NoCodesFragment : Fragment(), NoCodesDelegate {
private fun setupNoCodes() {
NoCodes.shared.setDelegate(this)
+ NoCodes.shared.setCustomVariablesDelegate(this)
addEvent(getString(R.string.nocodes_delegate_set))
}
+ // CustomVariablesDelegate
+ override fun getCustomVariables(contextKey: String): Map {
+ val variables = mapOf("custom_var" to "super")
+ android.util.Log.d("NoCodes", "Custom variables requested for $contextKey: $variables")
+ return variables
+ }
+
private fun showScreen(contextKey: String) {
addEvent(getString(R.string.showing_screen, contextKey))
NoCodes.shared.showScreen(contextKey)
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 7ff0cb33..0a930375 100644
--- a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt
+++ b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt
@@ -9,6 +9,7 @@ import com.qonversion.android.sdk.dto.products.QProduct
import com.qonversion.android.sdk.dto.properties.QUserPropertyKey
import com.qonversion.android.sdk.internal.InternalConfig
import com.qonversion.android.sdk.internal.QonversionInternal
+import com.qonversion.android.sdk.listeners.QonversionEmptyCallback
import com.qonversion.android.sdk.listeners.QonversionExperimentAttachCallback
import com.qonversion.android.sdk.listeners.QDeferredPurchasesListener
import com.qonversion.android.sdk.listeners.QEntitlementsUpdateListener
@@ -346,6 +347,15 @@ interface Qonversion {
*/
fun userProperties(callback: QonversionUserPropertiesCallback)
+ /**
+ * Force-flushes any pending user property updates to the server immediately.
+ * Use this when you need to ensure all previously set properties have been sent
+ * before performing an operation that depends on them.
+ *
+ * @param callback callback that will be called when the flush is complete.
+ */
+ fun forceSendProperties(callback: QonversionEmptyCallback)
+
/**
* Call this function to check if the fallback file is accessible.
* @return flag that indicates whether Qonversion was able to read data from the fallback file or not.
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 5b75b611..7b1af8f3 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
@@ -368,6 +368,10 @@ internal class QonversionInternal(
userPropertiesManager.userProperties(callback)
}
+ override fun forceSendProperties(callback: com.qonversion.android.sdk.listeners.QonversionEmptyCallback) {
+ userPropertiesManager.forceSendProperties(callback)
+ }
+
override fun isFallbackFileAccessible(): Boolean {
val fallbackObject = fallbackService.obtainFallbackData()