Skip to content

2.7.12#398

Merged
ianrumac merged 13 commits intomainfrom
develop
Apr 21, 2026
Merged

2.7.12#398
ianrumac merged 13 commits intomainfrom
develop

Conversation

@ianrumac
Copy link
Copy Markdown
Collaborator

@ianrumac ianrumac commented Apr 21, 2026

Changes in this pull request

2.7.12

Enhancements

  • Add customer info to paywall info and tracked events
  • Add stripe/paddle intro offer eligibility

Fixes

  • Fix dismiss animation for bottom sheets and modals on newer Samsung devices

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 PR adds CustomerInfo to PaywallInfo and tracked events, adds Stripe/Paddle intro-offer eligibility checking, and fixes Samsung bottom-sheet dismiss animation by deferring state changes via post {}.

  • P1 – responseLoadCompleteTime always equals start time: the secondary PaywallInfo constructor passes responseLoadStartTime to the responseLoadCompleteTime field, so paywall_response_load_complete_time in analytics is always wrong.
  • P1 – Premature super.finish() in dismiss animation: val e = (animatedValue as Int) / colorFrom performs integer division on signed ARGB ints; the value changes sign at ~36% of the animation, causing finish() to fire far too early and cutting the background fade short — partially defeating the Samsung fix.
  • P2 – Both shimmer timestamps use webViewLoadStartTime in the same constructor, a copy-paste bug causing incorrect shimmer timing in events.

Confidence Score: 3/5

Not safe to merge as-is — two P1 bugs affect analytics data correctness and the very animation fix this PR delivers.

Two P1 findings: the responseLoadCompleteTime copy-paste bug causes permanently wrong analytics timestamps, and the premature super.finish() call breaks the dismiss animation that this PR explicitly sets out to fix. Both are in actively modified code.

PaywallInfo.kt (timestamp bugs), SuperwallPaywallActivity.kt (ARGB arithmetic in dismiss animation).

Important Files Changed

Filename Overview
superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt Adds customerInfo to PaywallInfo; secondary constructor has two copy-paste bugs: responseLoadCompleteTime uses responseLoadStartTime, and both shimmer timestamps use webViewLoadStartTime.
superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt Samsung dismiss animation fix uses content.post {} for bottom-sheet state changes; background fade animation has a premature super.finish() call due to incorrect integer arithmetic on packed ARGB values.
superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallLogic.kt Adds Stripe/Paddle intro offer eligibility logic via isWebTrialAvailable and hasEverHadEntitlement; ELIGIBLE override ignores trialDays, which may be intentional but is undocumented.
superwall/src/main/java/com/superwall/sdk/models/customer/CustomerInfo.kt Adds toParams() serialisation for analytics; isPlaceholder internal flag leaks into serialised output as is_placeholder key.
superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallViewState.kt Adds customerInfo to PaywallViewState and computes PaywallInfo with .copy(customerInfo = customerInfo), cleanly injecting customer context at the view layer.
superwall/src/main/java/com/superwall/sdk/models/paywall/IntroOfferEligibility.kt New enum with three values (ELIGIBLE, INELIGIBLE, AUTOMATIC) serialised for Stripe/Paddle intro offer eligibility; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant PRM as PaywallRequestManager
    participant PL as PaywallLogic
    participant CI as CustomerInfo
    participant PVS as PaywallViewState
    participant PI as PaywallInfo

    PRM->>PRM: factory.currentCustomerInfo()
    PRM->>PL: getVariablesAndFreeTrial(customerInfo, introOfferEligibility)
    PL->>PL: computeHasFreeTrial()
    PL->>PL: isWebTrialAvailable() [Stripe/Paddle]
    PL->>CI: hasEverHadEntitlement()
    CI-->>PL: Boolean
    PL-->>PRM: ProductProcessingOutcome

    PRM->>PI: paywall.getInfo(event) customerInfo=empty
    PI-->>PRM: PaywallInfo (no customerInfo yet)

    Note over PVS: View layer injects customerInfo
    PVS->>PI: paywall.getInfo(event).copy(customerInfo=customerInfo)
    PI-->>PVS: PaywallInfo (with customerInfo)
    PVS->>PI: eventParams() includes customer_info map
Loading

Comments Outside Diff (3)

  1. superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt, line 122-125 (link)

    P1 responseLoadCompleteTime copies responseLoadStartTime

    The secondary constructor passes responseLoadStartTime to the responseLoadCompleteTime field, so both fields always contain the same timestamp. The responseLoadCompleteTime parameter from the constructor is silently ignored here, meaning the analytics event parameter paywall_response_load_complete_time will always equal the start time.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt
    Line: 122-125
    
    Comment:
    **`responseLoadCompleteTime` copies `responseLoadStartTime`**
    
    The secondary constructor passes `responseLoadStartTime` to the `responseLoadCompleteTime` field, so both fields always contain the same timestamp. The `responseLoadCompleteTime` parameter from the constructor is silently ignored here, meaning the analytics event parameter `paywall_response_load_complete_time` will always equal the start time.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt, line 690-696 (link)

    P1 Premature finish() due to ARGB sign-change in integer division

    colorFrom is Color.argb(200, 0, 0, 0) = 0xC8000000, which is negative as a signed 32-bit int. During the animation, ArgbEvaluator produces values that are negative when alpha > 127 and positive when alpha ≤ 127. Once alpha drops below 128 (at ≈36% of the animation), animatedValue becomes positive and e = positive / negative = -1, which is < 0.1, so super.finish() is called after only ~36% of the 300 ms fade — the background is still clearly visible. This undermines the Samsung dismiss fix added in the same function.

    The intent is likely to check that the animation is nearly done before finishing:

    addUpdateListener { animator ->
        val animatedColor = animator.animatedValue as Int
        window.setBackgroundDrawable(ColorDrawable(animatedColor))
        val animatedAlpha = android.graphics.Color.alpha(animatedColor)
        if (animatedAlpha < (0.1f * android.graphics.Color.alpha(colorFrom)).toInt()) {
            super.finish()
        }
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt
    Line: 690-696
    
    Comment:
    **Premature `finish()` due to ARGB sign-change in integer division**
    
    `colorFrom` is `Color.argb(200, 0, 0, 0)` = `0xC8000000`, which is negative as a signed 32-bit int. During the animation, `ArgbEvaluator` produces values that are negative when alpha > 127 and positive when alpha ≤ 127. Once alpha drops below 128 (at ≈36% of the animation), `animatedValue` becomes positive and `e = positive / negative = -1`, which is `< 0.1`, so `super.finish()` is called after only ~36% of the 300 ms fade — the background is still clearly visible. This undermines the Samsung dismiss fix added in the same function.
    
    The intent is likely to check that the animation is nearly done before finishing:
    
    ```kotlin
    addUpdateListener { animator ->
        val animatedColor = animator.animatedValue as Int
        window.setBackgroundDrawable(ColorDrawable(animatedColor))
        val animatedAlpha = android.graphics.Color.alpha(animatedColor)
        if (animatedAlpha < (0.1f * android.graphics.Color.alpha(colorFrom)).toInt()) {
            super.finish()
        }
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt, line 172-178 (link)

    P2 Shimmer timestamps both copied from webViewLoadStartTime

    Both shimmerLoadStartTime and shimmerLoadCompleteTime are set from the webViewLoadStartTime parameter instead of their own parameters. The shimmerLoadStartTime and shimmerLoadCompleteTime constructor arguments are silently ignored, causing incorrect shimmer timing in analytics events.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt
    Line: 172-178
    
    Comment:
    **Shimmer timestamps both copied from `webViewLoadStartTime`**
    
    Both `shimmerLoadStartTime` and `shimmerLoadCompleteTime` are set from the `webViewLoadStartTime` parameter instead of their own parameters. The `shimmerLoadStartTime` and `shimmerLoadCompleteTime` constructor arguments are silently ignored, causing incorrect shimmer timing in analytics events.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt
Line: 122-125

Comment:
**`responseLoadCompleteTime` copies `responseLoadStartTime`**

The secondary constructor passes `responseLoadStartTime` to the `responseLoadCompleteTime` field, so both fields always contain the same timestamp. The `responseLoadCompleteTime` parameter from the constructor is silently ignored here, meaning the analytics event parameter `paywall_response_load_complete_time` will always equal the start time.

```suggestion
        responseLoadCompleteTime =
            responseLoadCompleteTime?.let {
                DateFormatterUtil.format(it)
            } ?: "",
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt
Line: 690-696

Comment:
**Premature `finish()` due to ARGB sign-change in integer division**

`colorFrom` is `Color.argb(200, 0, 0, 0)` = `0xC8000000`, which is negative as a signed 32-bit int. During the animation, `ArgbEvaluator` produces values that are negative when alpha > 127 and positive when alpha ≤ 127. Once alpha drops below 128 (at ≈36% of the animation), `animatedValue` becomes positive and `e = positive / negative = -1`, which is `< 0.1`, so `super.finish()` is called after only ~36% of the 300 ms fade — the background is still clearly visible. This undermines the Samsung dismiss fix added in the same function.

The intent is likely to check that the animation is nearly done before finishing:

```kotlin
addUpdateListener { animator ->
    val animatedColor = animator.animatedValue as Int
    window.setBackgroundDrawable(ColorDrawable(animatedColor))
    val animatedAlpha = android.graphics.Color.alpha(animatedColor)
    if (animatedAlpha < (0.1f * android.graphics.Color.alpha(colorFrom)).toInt()) {
        super.finish()
    }
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt
Line: 172-178

Comment:
**Shimmer timestamps both copied from `webViewLoadStartTime`**

Both `shimmerLoadStartTime` and `shimmerLoadCompleteTime` are set from the `webViewLoadStartTime` parameter instead of their own parameters. The `shimmerLoadStartTime` and `shimmerLoadCompleteTime` constructor arguments are silently ignored, causing incorrect shimmer timing in analytics events.

```suggestion
        shimmerLoadStartTime =
            shimmerLoadStartTime?.let {
                DateFormatterUtil.format(it)
            } ?: "",
        shimmerLoadCompleteTime =
            shimmerLoadCompleteTime?.let {
                DateFormatterUtil.format(it)
            } ?: "",
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallLogic.kt
Line: 174-177

Comment:
**`ELIGIBLE` overrides `trialDays=0` products as having a free trial**

When `introOfferEligibility` is `ELIGIBLE`, `isWebTrialAvailable` returns `true` unconditionally — even if `trialDays` is `null` or `0`. This means a product explicitly configured with no trial days would still cause `isFreeTrialAvailable = true` for the paywall if the override is set. If `ELIGIBLE` is meant as a per-user override (user is eligible *if* a trial actually exists), the trial-days check should still apply; if it's a pure force-show flag, a comment clarifying the intended behaviour would help prevent accidental misuse.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/models/customer/CustomerInfo.kt
Line: 59-64

Comment:
**`isPlaceholder` internal field leaks into serialised analytics params**

`toParams()` serialises the full object using `encodeDefaults = true` and the SnakeCase naming strategy. The `isPlaceholder` field (serial name `"isPlaceholder"`, snake-cased to `"is_placeholder"`) is included in the output as `"is_placeholder": false` whenever real data has loaded. This internal implementation detail will appear under `customer_info` in every analytics event, which may pollute dashboards. Consider annotating `isPlaceholder` with `@Transient` in the serialisation context or excluding it explicitly in `toParams()`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Merge pull request #397 from superwall/i..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@ianrumac ianrumac merged commit 617dead into main Apr 21, 2026
5 of 9 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