From 74a8e19b2d3e84991be6f08f527939de93335727 Mon Sep 17 00:00:00 2001 From: Ranbir Singh Date: Sat, 16 May 2026 15:18:02 +0530 Subject: [PATCH] Fix delegate events from Compose paywalls --- .../sdk/compose/PaywallComposable.kt | 20 ++++++++-- .../sdk/analytics/internal/TrackingLogic.kt | 24 +++++++++++- .../analytics/internal/TrackingLogicTest.kt | 39 +++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt index 2863302b..07016515 100644 --- a/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt +++ b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt @@ -3,6 +3,7 @@ package com.superwall.sdk.compose import android.app.Activity import android.content.Context import android.content.ContextWrapper +import android.view.View import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -92,9 +93,6 @@ fun PaywallComposable( when { viewState.value != null -> { viewState.value?.let { viewToRender -> - LaunchedEffect(viewToRender) { - viewToRender.onViewCreated() - } val themeChanged = rememberThemeChanged() AndroidView( modifier = modifier, @@ -103,7 +101,21 @@ fun PaywallComposable( it.onThemeChanged() } }, - factory = { context -> + factory = { + if (viewToRender.isAttachedToWindow) { + viewToRender.onViewCreated() + } else { + viewToRender.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + view.removeOnAttachStateChangeListener(this) + viewToRender.onViewCreated() + } + + override fun onViewDetachedFromWindow(view: View) = Unit + }, + ) + } viewToRender }, onRelease = { diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt index f993bb81..3948896f 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt @@ -141,7 +141,7 @@ sealed class TrackingLogic { input?.let { value -> when (value) { is List<*> -> null - is Map<*, *> -> value + is Map<*, *> -> cleanMap(value) is String -> value is Int, is Float, is Double, is Long, is Boolean -> value is JsonElement -> value.convertFromJsonElement() @@ -161,6 +161,28 @@ sealed class TrackingLogic { } } + private fun cleanNested(input: Any?): Any? = + input?.let { value -> + when (value) { + is List<*> -> cleanList(value) + is Map<*, *> -> cleanMap(value) + else -> clean(value) + } + } + + private fun cleanMap(value: Map<*, *>): Map? = + value + .mapNotNull { (key, nestedValue) -> + val cleanedValue = cleanNested(nestedValue) ?: return@mapNotNull null + key?.toString()?.let { it to cleanedValue } + }.toMap() + .takeIf { it.isNotEmpty() } + + private fun cleanList(value: List<*>): List? { + val cleanedValues = value.mapNotNull { cleanNested(it) } + return cleanedValues.ifEmpty { null } + } + @Throws(Exception::class) fun checkNotSuperwallEvent(event: String) { // Try to create a SuperwallEvents event from the event string diff --git a/superwall/src/test/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt b/superwall/src/test/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt index 76f6d19d..929747b9 100644 --- a/superwall/src/test/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt @@ -76,6 +76,45 @@ class TrackingLogicTest { assertEquals("https://example.com", parameters.delegateParams["uri_value"]) } + @Test + fun processParameters_recursivelyCleansNestedMaps() = + runBlocking { + val trackable = + TrackableTestUtils.fakeTrackable( + rawName = "event_a", + superwallParams = + mapOf( + "customer_info" to + mapOf( + "userId" to "user-123", + "subscriptions" to + listOf( + mapOf( + "productId" to "pro_monthly", + "unsupported" to Any(), + "nullValue" to null, + ), + ), + "emptyMap" to mapOf("unsupported" to Any()), + "unsupported" to Any(), + ), + ), + canImplicitlyTrigger = false, + ) + + val parameters = TrackingLogic.processParameters(trackable, appSessionId = "session-123") + + val customerInfo = parameters.delegateParams["customer_info"] as Map<*, *> + val subscriptions = customerInfo["subscriptions"] as List<*> + val subscription = subscriptions.first() as Map<*, *> + assertEquals("user-123", customerInfo["userId"]) + assertEquals("pro_monthly", subscription["productId"]) + assertFalse(subscription.containsKey("unsupported")) + assertFalse(subscription.containsKey("nullValue")) + assertFalse(customerInfo.containsKey("emptyMap")) + assertFalse(customerInfo.containsKey("unsupported")) + } + @Test fun isNotDisabledVerboseEvent_handlesVariousEventTypes() { val paywallInfo = PaywallInfo.empty()