Skip to content

Commit bc90448

Browse files
authored
Merge pull request #424 from superwall/develop
4.12.8
2 parents ca9907e + 9949cd3 commit bc90448

File tree

9 files changed

+73
-21
lines changed

9 files changed

+73
-21
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.
44

5+
## 4.12.8
6+
7+
### Enhancements
8+
9+
- Exposes the `introOfferToken` on `StoreProduct` so that those using a PurchaseController can take advantage of the introductory offer eligiblity override.
10+
11+
### Fixes
12+
13+
- Stop logging `paywallWebviewLoad_timeout` events because they were confusing.
14+
- Only refreshes terminated webviews once to avoid infinite reloading loops on low RAM devices.
15+
516
## 4.12.7
617

718
### Fixes

Sources/SuperwallKit/Misc/Constants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ let sdkVersion = """
1818
*/
1919

2020
let sdkVersion = """
21-
4.12.7
21+
4.12.8
2222
"""

Sources/SuperwallKit/Models/Paywall/IntroOfferToken.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@ struct IntroOfferTokenWrapper: Codable {
1515
}
1616
}
1717

18-
struct IntroOfferToken: Codable {
19-
let token: String
20-
let expiry: Date
18+
/// A token used to override Apple's automatic intro offer eligibility determination.
19+
///
20+
/// Use this token with StoreKit 2's `.introductoryOfferEligibility(compactJWS:)`
21+
/// purchase option to apply intro offer eligibility on iOS 18.2+.
22+
public struct IntroOfferToken: Codable, Sendable {
23+
/// The JWT token string to pass to StoreKit.
24+
public let token: String
25+
26+
/// The expiration date of the token.
27+
public let expiry: Date
2128

2229
enum CodingKeys: String, CodingKey {
2330
case token
@@ -29,15 +36,15 @@ struct IntroOfferToken: Codable {
2936
self.expiry = expiry
3037
}
3138

32-
init(from decoder: Decoder) throws {
39+
public init(from decoder: Decoder) throws {
3340
let container = try decoder.container(keyedBy: CodingKeys.self)
3441
token = try container.decode(String.self, forKey: .token)
3542

3643
let timestamp = try container.decode(Milliseconds.self, forKey: .expiry)
3744
expiry = Date(timeIntervalSince1970: timestamp / 1000)
3845
}
3946

40-
func encode(to encoder: Encoder) throws {
47+
public func encode(to encoder: Encoder) throws {
4148
var container = encoder.container(keyedBy: CodingKeys.self)
4249
try container.encode(token, forKey: .token)
4350
try container.encode(expiry.timeIntervalSince1970 * 1000, forKey: .expiry)

Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -621,14 +621,6 @@ public class PaywallViewController: UIViewController, LoadingDelegate {
621621
self.exitButton.isHidden = false
622622
self.exitButton.alpha = 0.0
623623

624-
Task(priority: .utility) {
625-
let webviewTimeout = await InternalSuperwallEvent.PaywallWebviewLoad(
626-
state: .timeout,
627-
paywallInfo: self.info
628-
)
629-
await Superwall.shared.track(webviewTimeout)
630-
}
631-
632624
UIView.springAnimate(withDuration: 2) {
633625
self.refreshPaywallButton.alpha = 1.0
634626
self.exitButton.alpha = 1.0

Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ class SWWebView: WKWebView {
4545
private var completion: ((Error?) -> Void)?
4646
private let enableIframeNavigation: Bool
4747

48+
/// Tracks the number of times the WebView process has terminated and been reloaded.
49+
/// Used to prevent infinite reload loops on memory-constrained devices.
50+
private var processTerminationRetryCount = 0
51+
52+
/// Maximum number of automatic reloads after process termination.
53+
/// After this limit, the WebView will be reloaded when presented instead.
54+
private let maxProcessTerminationRetries = 1
55+
4856
init(
4957
isMac: Bool,
5058
messageHandler: PaywallMessageHandler,
@@ -231,6 +239,8 @@ extension SWWebView: WKNavigationDelegate {
231239
}
232240

233241
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
242+
// Reset retry count on successful load
243+
processTerminationRetryCount = 0
234244
completion?(nil)
235245
}
236246

@@ -251,7 +261,17 @@ extension SWWebView: WKNavigationDelegate {
251261
}
252262

253263
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
254-
webView.reload()
264+
// Only reload if we haven't exceeded the retry limit.
265+
// This prevents infinite reload loops on memory-constrained devices
266+
// where iOS keeps terminating the WebView process.
267+
if processTerminationRetryCount < maxProcessTerminationRetries {
268+
processTerminationRetryCount += 1
269+
webView.reload()
270+
} else {
271+
// Mark as failed so the WebView will be reloaded when presented again
272+
// via PaywallViewController.viewWillAppear checking didFailToLoad.
273+
loadingHandler.didFailToLoad = true
274+
}
255275

256276
Task {
257277
guard let paywallInfo = delegate?.info else {

Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ public typealias SK2Product = StoreKit.Product
2929
public final class StoreProduct: NSObject, StoreProductType, Sendable {
3030
let product: StoreProductType
3131

32+
/// The intro offer eligibility token for this product, if available.
33+
///
34+
/// Use this token with StoreKit 2's `.introductoryOfferEligibility(compactJWS:)`
35+
/// purchase option to override Apple's automatic eligibility determination.
36+
///
37+
/// This property is only populated when purchasing from a Superwall paywall and
38+
/// is only applicable on iOS 18.2+.
39+
///
40+
/// Example usage in a custom `PurchaseController`:
41+
/// ```swift
42+
/// if let token = product.introOfferToken {
43+
/// options.insert(.introductoryOfferEligibility(compactJWS: token.token))
44+
/// }
45+
/// ```
46+
public nonisolated(unsafe) var introOfferToken: IntroOfferToken?
47+
3248
/// A `Set` of ``Entitlements`` associated with the product.
3349
public var entitlements: Set<Entitlement> {
3450
product.entitlements

Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ final class ProductPurchaserSK2: Purchasing {
8383
}
8484

8585
func purchase(product: StoreProduct) async -> PurchaseResult {
86-
guard let product = product.sk2Product else {
86+
guard let sk2Product = product.sk2Product else {
8787
return .cancelled
8888
}
8989
do {
@@ -95,8 +95,7 @@ final class ProductPurchaserSK2: Purchasing {
9595
#if compiler(>=6.1)
9696
// Add intro offer eligibility token if available for this product
9797
// This allows overriding Apple's automatic eligibility determination
98-
if let paywallViewController = Superwall.shared.paywallViewController,
99-
let token = await paywallViewController.introOfferTokenManager.getValidToken(for: product.id) {
98+
if let token = product.introOfferToken {
10099
options.insert(.introductoryOfferEligibility(compactJWS: token.token))
101100
}
102101
#endif
@@ -110,9 +109,9 @@ final class ProductPurchaserSK2: Purchasing {
110109
guard let scene = await sharedApplication.connectedScenes.first else {
111110
return .cancelled
112111
}
113-
result = try await product.purchase(confirmIn: scene, options: options)
112+
result = try await sk2Product.purchase(confirmIn: scene, options: options)
114113
#else
115-
result = try await product.purchase(options: options)
114+
result = try await sk2Product.purchase(options: options)
116115
#endif
117116

118117
switch result {

Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,13 @@ final class TransactionManager {
428428
_ product: StoreProduct,
429429
purchaseSource: PurchaseSource
430430
) async -> PurchaseResult {
431+
// Attach intro offer token if available from the paywall
432+
if case .internal(_, let paywallViewController, _) = purchaseSource {
433+
product.introOfferToken = await paywallViewController
434+
.introOfferTokenManager
435+
.getValidToken(for: product.productIdentifier)
436+
}
437+
431438
switch purchaseSource {
432439
case .internal:
433440
return await purchaseController.purchase(product: product)

SuperwallKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22

33
s.name = "SuperwallKit"
4-
s.version = "4.12.7"
4+
s.version = "4.12.8"
55
s.summary = "Superwall: In-App Paywalls Made Easy"
66
s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com"
77

0 commit comments

Comments
 (0)