Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any> = emptyMap(),
@SerialName("url_config")
val urlConfig: PaywallWebviewUrl.Config? = null,
@Serializable
Expand Down Expand Up @@ -264,6 +269,7 @@ data class Paywall(
cacheKey = cacheKey,
buildId = buildId,
isScrollEnabled = isScrollEnabled ?: true,
state = state,
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any> = emptyMap(),
) {
constructor(
databaseId: String,
Expand Down Expand Up @@ -92,6 +97,7 @@ data class PaywallInfo(
buildId: String,
cacheKey: String,
isScrollEnabled: Boolean,
state: Map<String, Any> = emptyMap(),
) : this(
databaseId = databaseId,
identifier = identifier,
Expand Down Expand Up @@ -178,6 +184,7 @@ data class PaywallInfo(
cacheKey = cacheKey,
buildId = buildId,
isScrollEnabled = isScrollEnabled,
state = state,
)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>,
) : Updates({ viewState ->
viewState.copy(paywall = viewState.paywall.copy(state = state))
})

/**
* Hides or displays the paywall spinner.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String, Any> {
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<String, Any> =
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()
}
}
Loading