Skip to content

2.8.3 - #464

Merged
ianrumac merged 36 commits into
mainfrom
develop
Sep 11, 2026
Merged

2.8.3#464
ianrumac merged 36 commits into
mainfrom
develop

Conversation

@ianrumac

Copy link
Copy Markdown
Collaborator

Changes in this pull request

2.8.3

Fixes

  • Web purchase redemption now exposes the full checkout product in didRedeemLink, tracks freeTrial_start once per code, and schedules the active paywall's trial reminders from the checkout timestamp. Notification permission waits no longer block access or drop a late grant; ambiguous or already-elapsed reminders are skipped.
  • Fix multi-page paywalls only reporting the entry page view. paywall_open now waits for an in-flight template_variables send, so the runtime does not treat a late template payload as a fresh load and drop later page_views.
  • Fix an active paywall not being reopened after its webview process crashes and is recreated. Recovery cancels template work for the old document and sends the open after the replacement loads, only if the same presentation is still active.
  • Fix prices not showing when product/offers are fetched from cache
  • Fix video loading and playing in the background on preloaded paywalls
  • Fix a JSON null in placement parameters or user attributes reaching audience filters as the text "null", so a filter checking whether a field is null never matched.

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

ianrumac and others added 30 commits September 7, 2026 13:53
Fix prices missing when products are restored from cache
`JsonElement.toPassableValue` matched `JsonNull` against its
`is JsonPrimitive` branch, where every check failed and it fell to the
catch-all that reads `content` - which for a null is the string "null".
An audience filter asking `field == null` never matched, while
`field == "null"` did.

The sibling converter `convertFromJsonElement` already handles this, so
the two now agree.

Two existing assertions in JsonElementToPassableValueTest pinned the old
behaviour and are updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix: convert a JSON null to null, not to the text "null"
…/SWWebView.kt

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FY2V8fiWgvPpBhfp1Wjmc9
Pause/play media playback on backgrounded paywalls
…dering

Fix paywall open nd template ordering
Drop the instance mutex; persisted codes already dedupe trial events. Keep scheduling on a late grant while the paywall activity is alive, and let teardown own callback cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Code redemptions launch independently, so the persisted set alone cannot stop two in-flight calls from both seeing an empty marker across track(). Hold the lock through that write so freeTrial_start stays once per code.

Co-authored-by: Cursor <cursoragent@cursor.com>
ianrumac and others added 2 commits September 11, 2026 15:56

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — two rough edges inline, two scope questions below.

Reviewed changes — full diff of the 2.8.3 release rollup (main → develop), covering the five already-merged sub-branches plus the version bump. I re-reviewed the merged code as an integrated whole rather than re-litigating the individual PRs.

  • Web-trial redemption — RedemptionResult.PaywallInfo becomes a hand-rolled @Serializable class so the 2.8.2 bytecode signatures survive while gaining a nested PaywallProduct checkout snapshot; RedemptionStoreProduct adapts it to StoreProductType and WebTrialReminder re-bases config delays onto the checkout timestamp.
  • Redeem flow reorder — internallySetSubscriptionStatus is hoisted above the when (redemption) block so access lands before any permission wait, and codeResult is computed once with a fallback that also removes a pre-existing NoSuchElementException when the requested code is missing from the response.
  • freeTrial_start dedup — new persisted TrackedWebTrialCodes key plus a Mutex so overlapping same-code redemptions emit once.
  • Notification scheduling — attemptToScheduleNotifications moves to suspendCancellableCoroutine, resumes from onDestroy, releases a replaced waiter, subtracts the permission wait from absolute web delays, and skips sandbox /24/60 scaling for web reminders.
  • Paywall message ordering — paywall_open/paywall_close now wait on every in-flight template_variables send (bounded at TEMPLATE_OPEN_WAIT_MS), with resetForWebViewReload cancelling old-document work and re-queueing an open guarded by a lastOpen identity check.
  • Media playback — MediaPlaybackScript plus SWWebView lifecycle overrides keep hidden/preloaded paywall video paused; SuperwallPaywallActivity forwards onPause/onResume.
  • Two small fixes — ProductItemSerializer falls back from reference_name to product so disk-cached config keeps template reference names, and JsonElement.toPassableValue() gains an is JsonNull branch ahead of is JsonPrimitive.

I traced the lifecycleLock / inFlightTemplateSends machinery for deadlock, stranded queue entries and double delivery, and found none that this diff introduces: every queue mutation is serialized on the main thread (onRenderProcessGone → recreateWebview, and both flushPendingMessagesInternal call sites), and the lock is never held across a suspension. The added tests are genuine — TrialNotificationPermissionTest asserts the exact post-subtraction delay, SWWebViewMediaLifecycleTest alternates true/false on every transition so no assertion passes vacuously, and WebRedemptionTrialTest pins the full access → trial → schedule → restore → close → callback order.

ℹ️ The media-playback fix only reaches activity-hosted paywalls

hostPaused is only ever set by SuperwallPaywallActivity.onPause/onResume. superwall-compose's PaywallComposable uses a bare AndroidView with no lifecycle observer, so for embedded paywalls hostPaused stays false for the view's whole life and media is gated solely by attach/visibility state — which does not change on every backgrounding path. The changelog entry reads as a general fix, but compose hosts keep the old behavior.

Technical details
# Compose-hosted paywalls miss the new media pause/resume wiring

## Affected sites
- `superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt:793,807` — the only production callers of `SWWebView.onResume()`/`onPause()`.
- `superwall-compose`'s `PaywallComposable` — no `DisposableEffect`/`LifecycleEventObserver`, so neither is ever called.
- `superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt:193` — `!hostPaused` is a permanent no-op for that host.

## Required outcome
- Either forward the host lifecycle to `SWWebView.onPause()`/`onResume()` from the compose integration, or scope the changelog entry to activity-presented paywalls so the gap is explicit.

## Open questions for the human
- Is `superwall-compose` in scope for 2.8.3, or is activity-only parity acceptable for this patch?

ℹ️ A web-trial redemption can hold the paywall open for up to 30 s

handleTrialRedemption runs before triggerRestoreInPaywall, closePaywallIfExists and didRedeemLink, and can block on factory.scheduleTrialNotifications(...) for WEB_TRIAL_NOTIFICATION_TIMEOUT_MILLIS. Entitlement access is already applied by then, so this is not an access bug — but if the user leaves the notification-permission dialog unanswered without the activity being destroyed, the paywall stays up and the didRedeemLink delegate callback does not fire for the full 30 s. The tests pin this ordering deliberately, so the question is whether the bound is the one you want.

Technical details
# Redeem completion is gated on the notification-permission dialog

## Affected sites
- `superwall/src/main/java/com/superwall/sdk/web/WebPaywallRedeemer.kt:279` — `handleTrialRedemption(codeResult)` is awaited before the restore check and the trailing `closePaywallIfExists()` / `didRedeemLink(codeResult)` at lines 297-299.
- `superwall/src/main/java/com/superwall/sdk/web/WebPaywallRedeemer.kt:52,361` — the 30 s bound.

## Required outcome
- Confirm 30 s is the intended worst-case delay for paywall dismissal and the `didRedeemLink` delegate callback, or move the reminder scheduling off the completion path so dismissal is not gated on it.

## Open questions for the human
- The changelog says "Notification permission waits no longer block access"; should it also say dismissal and `didRedeemLink` are still deferred until the wait resolves or times out?

ℹ️ Nitpicks

  • superwall/src/test/java/com/superwall/sdk/paywall/request/CachedProductPricesReproductionTest.kt:113 — leftover debug println("Product bindings before refresh: …").
  • TrackedWebTrialCodes (superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt:325) is only ever unioned — nothing prunes it, and neither LocalStorage.reset() (it lives in APP_SPECIFIC_DOCUMENTS, not cacheDir) nor WebPaywallRedeemer.clear(ownership) touches it. Growth is negligible, but worth a deliberate decision on whether a logout should clear it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

Comment thread superwall/src/main/java/com/superwall/sdk/paywall/view/webview/PaywallWebUI.kt Outdated
github-actions Bot and others added 2 commits September 11, 2026 15:25
…/PaywallWebUI.kt

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
trialPeriodEndDate only caught DateTimeParseException, so an overflowing
date made toEpochMilli() throw ArithmeticException out of the lazy on every
read. It also used Instant.parse, which in threetenbp rejects offset
timestamps that webTrialReminderDelay accepts. Parse with OffsetDateTime
and catch DateTimeException/ArithmeticException to match the reminder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmJQyMUf6TdAoVa6jYMb12

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

This PR no longer compiles. 24a94eaa makes SWWebView.kt:200 and :204 ambiguous — confirmed by the unit-tests job on this SHA.

Reviewed changes — one commit since the prior pullfrog review (3d5db14).

  • Gave PaywallWebUI.onPause() / onResume() empty default bodies — applies the prior review's source-compatibility suggestion so external implementors of the public interface aren't broken by a patch release.

That suggestion was mine and it was wrong as written: I checked the interface's compatibility surface but not the super. call sites in its only production implementor. SWWebView extends WebView, which already has concrete onPause() / onResume(), so the interface defaults create two inherited implementations and the bare super.onPause() / super.onResume() added in this PR become ambiguous. Reproduced locally with kotlinc -language-version 2.0 against both a Kotlin and a Java superclass, and the CI log for 24a94ea shows exactly:

e: SWWebView.kt:200:9 Multiple supertypes available. Please specify the intended supertype in angle brackets, e.g. 'super<Foo>'.
e: SWWebView.kt:204:9 Multiple supertypes available. Please specify the intended supertype in angle brackets, e.g. 'super<Foo>'.

Qualifying the two super calls fixes it and keeps the compatibility win — inline suggestion below. Worth noting the interface's other View-shadowing members (scrollBy, scrollTo) stay abstract and are satisfied by View's implementations, which is why they never hit this.

The RedemptionStoreProduct.trialPeriodEndDate ArithmeticException thread from the prior review is unchanged by this commit and remains open.

ℹ️ Nitpicks

  • Now that the interface has defaults, FakePaywallWebUI's override fun onPause() = Unit / onResume() = Unit are redundant in both PaywallViewTest.kt:520,522 and PaywallMessageHandlerTest.kt:383,385.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

PaywallWebUI now provides default onPause/onResume bodies, so SWWebView
inherits concrete implementations from both WebView and PaywallWebUI.
Qualify the super calls with WebView to resolve the ambiguity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbUwGJ5vnr82Mag98Y2J2f

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The build break from 24a94eaa is fixed and every prior thread is resolved.

Reviewed changes — one commit since the prior pullfrog review (3025a1e).

  • Qualified the two ambiguous super calls in SWWebView — super<WebView>.onPause() / super<WebView>.onResume() replace the bare forms that stopped resolving once PaywallWebUI gained default bodies.

This is the right fix rather than reverting the interface defaults: WebView's implementations are what actually suspend timers and media, and the no-op defaults stay in place so external Kotlin implementors of the public PaywallWebUI aren't broken by a patch release. I verified both directions locally with kotlinc against a Java superclass — the qualified form compiles clean with no warnings, the bare form still fails at both call sites with Multiple supertypes available. CI (unit-tests, build) was still running when I submitted, so that's the confirming signal to watch.

Nothing else changed in this delta, and all three review threads on this PR are now resolved. The scope questions from the first review (compose-hosted media parity, the up-to-30 s didRedeemLink deferral, TrackedWebTrialCodes eviction) are unchanged and remain yours to decide — not blockers.

Pullfrog  | View workflow run | Using Claude Opus | 𝕏

@ianrumac
ianrumac merged commit 0322ce9 into main Sep 11, 2026
16 of 17 checks passed

This branch was successfully deployed

1 active deployment
github-pages — b3ae2b95 Deployed Sep 11, 2026 by ianrumac via deploy #153
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.

2 participants