diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt index de4ff1fc..a13bb848 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt @@ -117,6 +117,11 @@ data class Paywall( var experiment: Experiment? = null, @kotlinx.serialization.Transient() var closeReason: PaywallCloseReason = PaywallCloseReason.None, + /** + * The state of the paywall, updated on paywall did dismiss. + */ + @kotlinx.serialization.Transient() + var state: Map = emptyMap(), @SerialName("url_config") val urlConfig: PaywallWebviewUrl.Config? = null, @Serializable @@ -264,6 +269,7 @@ data class Paywall( cacheKey = cacheKey, buildId = buildId, isScrollEnabled = isScrollEnabled ?: true, + state = state, ) companion object { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 5da9afc3..db0a4ae7 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -59,6 +59,11 @@ data class PaywallInfo( val buildId: String, val cacheKey: String, val isScrollEnabled: Boolean, + /** + * The state of the paywall, updated on paywall did dismiss. + */ + @kotlinx.serialization.Transient + val state: Map = emptyMap(), ) { constructor( databaseId: String, @@ -92,6 +97,7 @@ data class PaywallInfo( buildId: String, cacheKey: String, isScrollEnabled: Boolean, + state: Map = emptyMap(), ) : this( databaseId = databaseId, identifier = identifier, @@ -178,6 +184,7 @@ data class PaywallInfo( cacheKey = cacheKey, buildId = buildId, isScrollEnabled = isScrollEnabled, + state = state, ) fun eventParams( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt index f05f06e8..baf723e1 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt @@ -469,6 +469,10 @@ class PaywallView( val isManualClose = closeReason is PaywallCloseReason.ManualClose suspend fun dismissView() { + // Get the paywall state from the webview before dismissing + val paywallState = webView.messageHandler.getState() + controller.updateState(SetPaywallState(paywallState)) + if (isDeclined && isManualClose) { val trackedEvent = InternalSuperwallEvent.PaywallDecline(paywallInfo = info) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallViewState.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallViewState.kt index 983e17d0..cd2629cf 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallViewState.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallViewState.kt @@ -259,6 +259,15 @@ data class PaywallViewState( it.copy(crashRetries = 0) }) + /** + * Updates the paywall state with data retrieved from the webview on dismiss. + */ + class SetPaywallState( + val state: Map, + ) : Updates({ viewState -> + viewState.copy(paywall = viewState.paywall.copy(state = state)) + }) + /** * Hides or displays the paywall spinner. * diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt index cb737943..27af255b 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt @@ -24,6 +24,7 @@ import com.superwall.sdk.storage.core_data.convertToJsonElement import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -32,6 +33,7 @@ import java.net.URI import java.util.Date import java.util.LinkedList import java.util.Queue +import kotlin.coroutines.resume interface PaywallStateDelegate { val state: PaywallViewState @@ -678,4 +680,69 @@ class PaywallMessageHandler( // Android doesn't have a direct equivalent to UIImpactFeedbackGenerator // TODO: Implement haptic feedback } + + /** + * Gets the current state from the paywall webview by evaluating JavaScript. + * @return A map containing the paywall state, or an empty map if evaluation fails. + */ + suspend fun getState(): Map { + val messageScript = "window.app.getAllState();" + + Logger.debug( + logLevel = LogLevel.debug, + scope = LogScope.paywallView, + message = "Getting state", + info = mapOf("message" to messageScript), + ) + + return suspendCancellableCoroutine { continuation -> + mainScope.launch { + messageHandler?.evaluate(messageScript) { result -> + if (result != null) { + try { + val parsed = json.parseToJsonElement(result) + val stateMap = jsonElementToMap(parsed) + continuation.resume(stateMap) + } catch (e: Exception) { + Logger.debug( + logLevel = LogLevel.error, + scope = LogScope.paywallView, + message = "Error parsing state JSON", + info = mapOf("message" to messageScript, "result" to result), + error = e, + ) + continuation.resume(emptyMap()) + } + } else { + continuation.resume(emptyMap()) + } + } ?: run { + continuation.resume(emptyMap()) + } + } + } + } + + private fun jsonElementToMap(element: kotlinx.serialization.json.JsonElement): Map = + when (element) { + is JsonObject -> element.mapValues { (_, value) -> jsonElementToAny(value) } + else -> emptyMap() + } + + private fun jsonElementToAny(element: kotlinx.serialization.json.JsonElement): Any = + when (element) { + is JsonObject -> element.mapValues { (_, value) -> jsonElementToAny(value) } + is kotlinx.serialization.json.JsonArray -> element.map { jsonElementToAny(it) } + is kotlinx.serialization.json.JsonPrimitive -> { + when { + element.isString -> element.content + element.content == "true" -> true + element.content == "false" -> false + element.content == "null" -> "" + element.content.contains('.') -> element.content.toDoubleOrNull() ?: element.content + else -> element.content.toLongOrNull() ?: element.content + } + } + else -> element.toString() + } }