Skip to content

2.7.5#373

Merged
ianrumac merged 5 commits intomainfrom
develop
Mar 5, 2026
Merged

2.7.5#373
ianrumac merged 5 commits intomainfrom
develop

Conversation

@ianrumac
Copy link
Copy Markdown
Collaborator

@ianrumac ianrumac commented Mar 5, 2026

Changes in this pull request

2.7.5

Enhancements

  • Add appstack integration attribute identifier

Fixes

  • Ensure test mode does not interfere with expo
  • Ensure isActive is properly returned and not calculated via expiration date
  • Fix potential memory leak when webview crashes
  • Ensure O(n) cleanup doesn't run multiple times

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run ktlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Summary

This release (2.7.5) contains four targeted fixes and one enhancement: it adds the APPSTACK attribution provider, fixes test mode's Expo/React Native compatibility by using a CurrentActivityTracker to resolve the true foreground activity instead of the root MainActivity, corrects subscription active-status detection by basing it on Google Play's purchaseState rather than a naive expiration date calculation, addresses a memory leak when the WebView render process crashes, and prevents redundant O(n) cache cleanup runs.

Key changes:

  • AttributionProvider.kt: new APPSTACK("appstackId") enum entry — clean, no concerns.
  • PlayStorePurchaseAdapter.kt: calculateIsActive() now returns true for PurchaseState.PURCHASED regardless of expiration, correctly handling renewed subscriptions — well-tested.
  • ConfigManager.kt / TestModeTransactionHandler.kt: both now accept a CurrentActivityTracker? and prefer lifecycle-tracked activity to fix Expo/RN root-activity clobbering.
  • PaywallView.cleanup(): clears webview delegate, unregisters the activityResultLauncher, and removes views — but uses removeAllViews() on the parent instead of removeView(this), which could remove unintended sibling views (see comment).
  • CurrentActivityTracker.onActivityStopped: clears activityState but not currentActivity WeakReference, meaning getCurrentActivity() can return a stopped activity and bypass the awaitActivity fallback in presentTestModeModal and getForegroundActivity() (see comment).
  • RenewedSubscriptionActiveStatusTest.kt: new BDD-style test suite covering the isActive fix — a minor comment inconsistency on the product-not-found edge case says "null expiration means isActive=true" when expiration is no longer used.

Confidence Score: 3/5

  • PR is mostly safe but has two logic issues in the memory-leak and Expo fixes that could cause crashes or silent modal failures in edge cases.
  • The isActive fix and attribution enum addition are clean and well-tested. However, PaywallView.cleanup() calls removeAllViews() on the parent (rather than removeView(this)), which could destructively strip sibling views in any layout that places PaywallView alongside other children. Additionally, CurrentActivityTracker.onActivityStopped does not clear currentActivity, allowing getCurrentActivity() to return a stopped activity — which prevents the awaitActivity safety-net from activating and could cause test-mode modals to be shown on a backgrounded activity. Both issues were introduced as part of the intended fixes in this PR.
  • PaywallView.kt (cleanup logic) and CurrentActivityTracker.kt (onActivityStopped) need the most attention before merging.

Important Files Changed

Filename Overview
superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt Memory leak fix for webview crash: clears delegate, unregisters activity result launcher, and removes views on cleanup — but removeAllViews() on parent is overly broad and could remove unintended sibling views.
superwall/src/main/java/com/superwall/sdk/misc/CurrentActivityTracker.kt Lifecycle-aware activity tracker used to resolve Expo/RN compatibility issue — but onActivityStopped does not clear currentActivity WeakReference, so getCurrentActivity() can return a stopped activity, defeating the awaitActivity fallback.
superwall/src/main/java/com/superwall/sdk/store/abstractions/product/receipt/PlayStorePurchaseAdapter.kt Fixed isActive to use purchaseState == PURCHASED instead of expiration-date calculation, correctly handling renewed subscriptions that Google Play returns as PURCHASED with an outdated purchaseTime.
superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt Added activityTracker: CurrentActivityTracker? parameter and updated presentTestModeModal to prefer lifecycle-tracked activity over user-provided ActivityProvider, with awaitActivity as last fallback — resolves the Expo/RN root activity issue.
superwall/src/main/java/com/superwall/sdk/store/testmode/TestModeTransactionHandler.kt Added activityTracker: CurrentActivityTracker? parameter so purchase/restore drawers prefer the lifecycle-tracked foreground activity over the user-provided provider, consistent with the Expo fix in ConfigManager.
superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt Updated resetCache() to call destroyWebview() on non-active views before removeAll(), ensuring webview resources are cleaned up and cache clear doesn't run redundant O(n) iterations.
superwall/src/test/java/com/superwall/sdk/store/abstractions/product/receipt/RenewedSubscriptionActiveStatusTest.kt New comprehensive test suite verifying that renewed subscriptions are correctly reported as active based on Google Play's PURCHASED state rather than the expired naive expiration date calculation. Minor comment inconsistency on line 316.

Sequence Diagram

sequenceDiagram
    participant App
    participant ConfigManager
    participant CurrentActivityTracker
    participant ActivityProvider
    participant TestModeModal
    participant TestModeTransactionHandler
    participant PlayStorePurchaseAdapter

    App->>ConfigManager: processConfig(config)
    ConfigManager->>ConfigManager: evaluateTestMode()
    ConfigManager->>ConfigManager: ioScope.launch { presentTestModeModal() }

    ConfigManager->>CurrentActivityTracker: getCurrentActivity()
    alt Activity is available (Expo fix)
        CurrentActivityTracker-->>ConfigManager: foreground Activity
    else Lifecycle tracker returns null
        ConfigManager->>ActivityProvider: getCurrentActivity()
        ActivityProvider-->>ConfigManager: root Activity (may be stale in Expo/RN)
    else Both null
        ConfigManager->>CurrentActivityTracker: awaitActivity(10s)
        CurrentActivityTracker-->>ConfigManager: Activity (or null on timeout)
    end

    ConfigManager->>TestModeModal: show(activity, ...)
    TestModeModal-->>ConfigManager: TestModeModalResult

    App->>TestModeTransactionHandler: handlePurchase(product)
    TestModeTransactionHandler->>CurrentActivityTracker: getCurrentActivity() (prefers lifecycle-tracked)
    CurrentActivityTracker-->>TestModeTransactionHandler: foreground Activity

    App->>PlayStorePurchaseAdapter: isActive
    Note over PlayStorePurchaseAdapter: purchaseState == PURCHASED → true\n(no longer uses expiration date)\nFixes renewed subscription detection
    PlayStorePurchaseAdapter-->>App: true/false
Loading

Comments Outside Diff (1)

  1. General comment

    removeAllViews() on parent removes sibling views

    (parent as? ViewGroup)?.removeAllViews() removes all children of the parent container, not just this PaywallView. If the parent ViewGroup has any sibling views (backdrop overlays, dimming layers, etc.), they will also be removed.

    Compare this with prepareToDisplay() just above (line 1054), which correctly uses parentViewGroup?.removeView(this@PaywallView) to remove only this specific view.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt
    Line: 1067
    
    Comment:
    **`removeAllViews()` on parent removes sibling views**
    
    `(parent as? ViewGroup)?.removeAllViews()` removes **all children** of the parent container, not just this `PaywallView`. If the parent `ViewGroup` has any sibling views (backdrop overlays, dimming layers, etc.), they will also be removed.
    
    Compare this with `prepareToDisplay()` just above (line 1054), which correctly uses `parentViewGroup?.removeView(this@PaywallView)` to remove only this specific view.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 56e81c0

Greptile also left 2 inline comments on this PR.

@ianrumac ianrumac merged commit 21faad5 into main Mar 5, 2026
2 of 4 checks passed
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.

1 participant