diff --git a/CHANGELOG.md b/CHANGELOG.md index e1669d3f91..c0f4f29e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - Fixes the SDK getting stuck in test mode when a sheet such as the Customer Center is already open as it finishes loading. - Stops matching installs for attribution unless the Superwall MMP is turned on for your app. - Fixes paywalls on first launch missing acquisition attributes in their audience filters: they now wait up to 2 seconds for install attribution to finish. +- Fixes a paywall staying blank after iOS terminated its web content process. Paywalls that gate their entrance on `paywall_open`, which includes every framework paywall, rendered nothing in two cases. When the process died under a presented paywall, the SDK reloaded the page but never sent `paywall_open` to the new page. When the process died under a preloaded paywall, the SDK reloaded it at presentation but sent `paywall_open` before the new page was ready, so the page never received it. The SDK now tracks readiness per page load: it holds `paywall_open` until the new page says it is ready, and sends it again to a page reloaded while the paywall is open. The analytics `paywall_open` event is not tracked a second time. +- Fixes paywalls served by a `superwall dev` server never opening when they gate their entrance on `paywall_open`. The SDK sent `paywall_open` before the local page was ready, so the page never received it. ## 4.17.0 diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 8ca32fe45e..84bedd2d9c 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -468,6 +468,7 @@ public class PaywallViewController: UIViewController, LoadingDelegate { } loadingState = .loadingURL + webView.messageHandler.documentWillLoad() if let paywallArchiveManager = self.paywallArchiveManager, paywallArchiveManager.shouldAlwaysUseWebArchive(manifest: paywall.manifest) { @@ -537,6 +538,7 @@ public class PaywallViewController: UIViewController, LoadingDelegate { } @objc private func reloadWebView() { + webView.messageHandler.documentWillLoad() webView.reload() } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift index ec15c5d491..de785a453e 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift @@ -46,6 +46,8 @@ final class PaywallMessageHandler: WebEventDelegate { } /// Used to enqueue `paywall_open` messages if the paywall isn't ready to receive them yet. private var messageQueue: Queue = Queue() + private var isDocumentReady = false + private var isPaywallOpen = false init( receiptManager: ReceiptManager, @@ -77,6 +79,7 @@ final class PaywallMessageHandler: WebEventDelegate { } case .onReady(let paywalljsVersion): delegate?.paywall.paywalljsVersion = paywalljsVersion + isDocumentReady = true let loadedAt = Date() Task { await self.didLoadWebView(for: paywall, at: loadedAt) @@ -85,8 +88,9 @@ final class PaywallMessageHandler: WebEventDelegate { hapticFeedback() delegate?.eventDidOccur(.closed) case .paywallOpen: + isPaywallOpen = true let paywallOpen = SuperwallEventObjc.paywallOpen.description - if delegate?.paywall.paywalljsVersion == nil { + if !isDocumentReady { let message = EnqueuedMessage( name: paywallOpen, paywall: paywall @@ -98,8 +102,9 @@ final class PaywallMessageHandler: WebEventDelegate { } } case .paywallClose: + isPaywallOpen = false let paywallClose = SuperwallEventObjc.paywallClose.description - if delegate?.paywall.paywalljsVersion == nil { + if !isDocumentReady { let message = EnqueuedMessage( name: paywallClose, paywall: paywall @@ -275,6 +280,23 @@ final class PaywallMessageHandler: WebEventDelegate { } } + func documentWillLoad() { + isDocumentReady = false + messageQueue = Queue() + guard + isPaywallOpen, + let paywall = delegate?.paywall + else { + return + } + messageQueue.enqueue( + EnqueuedMessage( + name: SuperwallEventObjc.paywallOpen.description, + paywall: paywall + ) + ) + } + nonisolated private func pass( placement: String, from paywall: Paywall, diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift index c08cc001c0..4385755aa8 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift @@ -271,6 +271,7 @@ extension SWWebView: WKNavigationDelegate { } func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + messageHandler.documentWillLoad() if delegate?.isActive == true, activeProcessTerminationRetryCount < maxActiveProcessTerminationRetries { activeProcessTerminationRetryCount += 1 diff --git a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift index eb1a94281c..cf9a5fd09a 100644 --- a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerTests.swift @@ -181,6 +181,104 @@ struct PaywallMessageHandlerTests { #expect(didHandleJs == true) } + private func paywallOpenCount( + in webView: FakeWebView, + reaching expected: Int, + keeping delegate: PaywallMessageHandlerDelegateMock + ) async -> Int { + let count = { + self.passedEvents(in: webView).filter { $0["event_name"] as? String == "paywall_open" }.count + } + for _ in 0..<250 { + if count() >= expected { + break + } + try? await Task.sleep(nanoseconds: 20_000_000) + } + try? await Task.sleep(nanoseconds: 200_000_000) + withExtendedLifetime(delegate) {} + return count() + } + + @Test + func paywallOpenIsPassedStraightThroughOnceThePageIsReady() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + messageHandler.handle(.paywallOpen) + + #expect(await paywallOpenCount(in: webView, reaching: 1, keeping: delegate) == 1) + } + + @Test + func paywallOpenWaitsForTheReadyMessage() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.paywallOpen) + #expect(passedEvents(in: webView).isEmpty) + + messageHandler.handle(.onReady(paywallJsVersion: "2")) + + #expect(await paywallOpenCount(in: webView, reaching: 1, keeping: delegate) == 1) + } + + @Test + func paywallOpenIsResentToADocumentReloadedWhileOpen() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + messageHandler.handle(.paywallOpen) + #expect(await paywallOpenCount(in: webView, reaching: 1, keeping: delegate) == 1) + + messageHandler.documentWillLoad() + #expect(await paywallOpenCount(in: webView, reaching: 1, keeping: delegate) == 1) + + messageHandler.handle(.onReady(paywallJsVersion: "2")) + + #expect(await paywallOpenCount(in: webView, reaching: 2, keeping: delegate) == 2) + } + + @Test + func paywallOpenIsNotSentToADocumentReloadedWhileHidden() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + + #expect(await paywallOpenCount(in: webView, reaching: 1, keeping: delegate) == 0) + } + + @Test + func paywallOpenIsNotResentAfterThePaywallCloses() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + messageHandler.handle(.paywallOpen) + messageHandler.handle(.paywallClose) + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + + #expect(await paywallOpenCount(in: webView, reaching: 2, keeping: delegate) == 1) + } + + @Test + func paywallOpenIsSentOnceWhenTheDocumentDiesBeforeItIsReady() async { + let (messageHandler, webView, delegate) = makeHandler() + + messageHandler.documentWillLoad() + messageHandler.handle(.paywallOpen) + messageHandler.documentWillLoad() + messageHandler.handle(.onReady(paywallJsVersion: "2")) + + #expect(await paywallOpenCount(in: webView, reaching: 2, keeping: delegate) == 1) + } + @Test func close() { let dependencyContainer = DependencyContainer()