Skip to content

Fix delegate events from Compose paywalls#410

Open
AndroidPoet wants to merge 1 commit into
superwall:developfrom
AndroidPoet:fix/compose-delegate-events-405
Open

Fix delegate events from Compose paywalls#410
AndroidPoet wants to merge 1 commit into
superwall:developfrom
AndroidPoet:fix/compose-delegate-events-405

Conversation

@AndroidPoet
Copy link
Copy Markdown

@AndroidPoet AndroidPoet commented May 16, 2026

Summary

  • call onViewCreated() when the Compose AndroidView is created so PaywallOpen is fired for PaywallComposable
  • recursively clean nested event params so customer_info does not break delegate event delivery
  • add a regression test for nested customer info params

Fixes #405

Testing

  • ./gradlew :superwall:testDebugUnitTest --tests com.superwall.sdk.analytics.internal.TrackingLogicTest
  • ./gradlew :superwall-compose:compileDebugKotlin

Greptile Summary

This PR fixes two separate bugs affecting Compose-hosted paywalls: PaywallOpen never firing because onViewCreated() was previously scheduled through LaunchedEffect (which runs asynchronously after composition) rather than the synchronous AndroidView.factory; and delegate event delivery silently breaking when nested params like customer_info contained unsupported types, because the clean() function returned nested maps verbatim without sanitising their contents.

  • PaywallComposable.kt: onViewCreated() is moved into AndroidView.factory so it runs synchronously when the view is created, ensuring PaywallOpen and the didPresentPaywall delegate callback fire reliably.
  • TrackingLogic.kt: Adds cleanMap, cleanList, and cleanNested helpers and wires them into clean() for Map<*,*> values, so deeply nested structures are recursively stripped of unsupported or null entries before reaching the delegate.
  • TrackingLogicTest.kt: Adds a regression test covering nested customer_info with mixed supported/unsupported/null values at multiple nesting levels.

Confidence Score: 4/5

Safe to merge with minor caveats — both fixes are well-scoped and the new test provides good regression coverage.

The TrackingLogic recursion is correct and the test validates the key scenario. The one open question is whether onViewCreated() relies on the view being window-attached; if any code path inside it reads windowToken or similar, calling it from factory before the view is in the hierarchy could surface a subtle timing issue. Worth a quick manual smoke test on a Compose paywall, but nothing that blocks the merge outright.

PaywallComposable.kt — the timing of onViewCreated() relative to window attachment deserves a focused manual test on a real device or emulator.

Important Files Changed

Filename Overview
superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt Moves onViewCreated() from LaunchedEffect to AndroidView.factory to ensure synchronous execution; fixes PaywallOpen not firing on Compose paywalls
superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt Adds recursive cleanMap/cleanList/cleanNested helpers and wires them into clean() so nested maps like customer_info are sanitised before being sent to delegate; empty map edge case is benign improvement over previous unsupported-value passthrough
superwall/src/test/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt Adds a regression test verifying nested map/list cleaning and that unsupported/null values are stripped from customer_info

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["PaywallComposable\nPaywallBuilder.build()"] --> B["viewState set"]
    B --> C["AndroidView factory lambda"]
    C --> D["onViewCreated() ← MOVED HERE"]
    D --> E["viewCreatedCompletion invoke"]
    D --> F["storePresentationObject (ioScope)"]
    D --> G["SetPresentedAndFinished"]
    D --> H["delegate.didPresentPaywall(info)"]
    C --> I["Return viewToRender"]

    subgraph "TrackingLogic.clean() — param sanitisation"
        J["clean(value)"] -->|"Map<*,*>"| K["cleanMap(value) NEW"]
        K --> L["cleanNested(nestedValue)"]
        L -->|"List"| M["cleanList recursive"]
        L -->|"Map"| K
        L -->|"other"| J
        J -->|"List"| N["null dropped"]
        J -->|"primitive / JsonElement"| O["value kept"]
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt:173-178
`cleanMap` returns an empty `Map<String, Any>` when every entry is filtered out (all values unsupported or null). That empty map is non-null, so the caller's `?.let { delegateParams[key] = it }` guard won't drop it — the delegate receives `customer_info: {}` instead of nothing. In most cases this is fine, but if downstream serialisation or the host app pattern-matches on the presence of known keys it could be surprising. Consider returning `null` for an empty result to keep behaviour consistent with how other clean helpers signal "nothing useful here".

```suggestion
        private fun cleanMap(value: Map<*, *>): Map<String, Any>? =
            value
                .mapNotNull { (key, nestedValue) ->
                    val cleanedValue = cleanNested(nestedValue) ?: return@mapNotNull null
                    key?.toString()?.let { it to cleanedValue }
                }.toMap()
                .takeIf { it.isNotEmpty() }
```

### Issue 2 of 2
superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt:103-106
**`onViewCreated()` called before view is attached to window**

`AndroidView`'s `factory` lambda runs on the main thread, but the view is not yet attached to the window hierarchy when `factory` executes — `onAttachedToWindow()` / `onWindowVisibilityChanged` haven't fired. `onViewCreated()` immediately calls `controller.updateState(SetPresentedAndFinished)` and `delegate().didPresentPaywall(info)`. If either of those paths queries window attachment or reads properties that are only valid post-attach (e.g. `windowToken`, focus state), they would see null/stale values. The previous `LaunchedEffect` ran after the first composition frame, which is after the `View` is attached. Verify that none of the operations inside `onViewCreated()` depend on the view being window-attached before treating this as fully safe.

Reviews (1): Last reviewed commit: "Fix delegate events from Compose paywall..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Comment thread superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt Outdated
@AndroidPoet AndroidPoet force-pushed the fix/compose-delegate-events-405 branch from f837e31 to 2e74bb4 Compare May 16, 2026 09:54
@AndroidPoet AndroidPoet force-pushed the fix/compose-delegate-events-405 branch from 2e74bb4 to 74a8e19 Compare May 16, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing SuperwallDelegate events (2.7.12+)

1 participant