From 470aa1c3b3eb13d0dacf368788943087d9287b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:59:02 +0200 Subject: [PATCH 01/11] Require iOS 15 and Xcode 26 (Swift 6.2) Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026, so no shipping app can build a new SDK release with an older Xcode. Raise swift-tools-version to 6.2 (keeping the Swift 5 language mode), the iOS deployment target to 15, and macOS to 12, the oldest the 6.2 manifest supports. Drop the compiler checks that are now always true, the Swift < 5.7 StoreKit product workaround, and the Xcode 12 script. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++++ CLAUDE.md | 9 ++++++- Package.swift | 11 ++++---- README.md | 2 +- Sources/SuperwallKit/Misc/Constants.swift | 2 +- .../Transaction/Transaction+LatestSince.swift | 2 -- .../Network/Device Helper/DeviceHelper.swift | 13 ++-------- .../PaywallViewController.swift | 4 +-- .../View Controller/Web View/SWWebView.swift | 6 ++--- .../EntitlementProcessor.swift | 4 --- .../Receipt Manager/ReceiptManager.swift | 2 -- .../StoreProduct/SK2StoreProduct.swift | 15 ----------- .../StoreKit 2/ProductPurchaserSK2.swift | 2 -- SuperwallKit.podspec | 4 +-- SuperwallKit.xcodeproj/project.pbxproj | 4 +-- project.yml | 2 +- xcode12.py | 25 ------------------- 17 files changed, 33 insertions(+), 80 deletions(-) delete mode 100644 xcode12.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b02ffdaae0..f356e0476e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub. +## 4.18.0 +### Breaking Changes + +- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the minimum macOS target for Swift Package Manager from macOS 10.12 to macOS 12. +- Requires Xcode 26 (Swift 6.2) or later to build the SDK. Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026. The SDK itself still compiles in the Swift 5 language mode. + ## 4.17.0 ### Enhancements diff --git a/CLAUDE.md b/CLAUDE.md index 58b6937963..6e8053f225 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Package Management - Swift Package Manager: Primary dependency management via `Package.swift` - CocoaPods: Also supported via `SuperwallKit.podspec` -- Dependencies: `superscript-ios-next` at exact version 1.0.14 (slim binary-target distribution; replaces the legacy `Superscript-iOS` repo whose committed xcframework bloated clones) +- Dependencies: `superscript-ios-next` at exact version 1.0.15 (slim binary-target distribution; replaces the legacy `Superscript-iOS` repo whose committed xcframework bloated clones) ## Architecture Overview @@ -56,6 +56,13 @@ SuperwallKit is an iOS SDK for remote paywall configuration and A/B testing. The 4. Purchases are handled by `StoreKitManager` with automatic retry logic 5. Events are tracked through the analytics system +### Minimum Toolchain and Platforms + +- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12. Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. +- Don't add `#if compiler(...)` checks for anything below 6.2; only gate APIs newer than that (e.g. `compiler(>=6.3.2)` for iOS 26.4 StoreKit APIs). +- The package pins `swiftLanguageModes: [.v5]`. Moving to Swift 6 language mode is a separate change. +- Keep `Package.swift`, `SuperwallKit.podspec` and `project.yml` deployment targets in sync. + ### Code Conventions - 2-space indentation (enforced by SwiftLint) diff --git a/Package.swift b/Package.swift index 77e8277232..b062d65ce8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,8 +7,8 @@ let package = Package( name: "SuperwallKit", defaultLocalization: "en", platforms: [ - .iOS(.v13), - .macOS(.v10_12), + .iOS(.v15), + .macOS(.v12), .watchOS("6.2") ], products: [ @@ -19,7 +19,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/superwall/superscript-ios-next", .exact("1.0.15")) + .package(url: "https://github.com/superwall/superscript-ios-next", exact: "1.0.15") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -40,5 +40,6 @@ let package = Package( name: "SuperwallKitTests", dependencies: ["SuperwallKit"] ) - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/README.md b/README.md index 21ad9fc5ef..b2d25c555c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Cocoapods Compatible - iOS Versions Supported + iOS Versions Supported MIT License diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index 7ca78bdad4..37a2cf07c6 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -4.17.0 +4.18.0 """ diff --git a/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift b/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift index 02071e1fd2..efac553f0d 100644 --- a/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift +++ b/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift @@ -34,7 +34,6 @@ extension Transaction { for productId: String, since purchaseDate: Date ) async -> VerificationResult? { - #if compiler(>=6.1) if #available(iOS 18.4, visionOS 2.4, *) { var best: VerificationResult? @@ -55,7 +54,6 @@ extension Transaction { return best } - #endif let verificationResult = await Transaction.currentEntitlement(for: productId) diff --git a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift index fad0807d79..118bcdd3a5 100644 --- a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift +++ b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift @@ -667,15 +667,6 @@ class DeviceHelper { }() let interfaceType: String = { - #if compiler(>=5.9.2) - if #available(iOS 17.0, *) { - if UIDevice.current.userInterfaceIdiom == .vision { - return "vision" - } - } - #endif - // Ignore the exhaustive message because we need to be able to let devs using lower versions - // of xcode to build and they don't have vision support. switch UIDevice.current.userInterfaceIdiom { case .pad: return "ipad" @@ -687,6 +678,8 @@ class DeviceHelper { return "carplay" case .tv: return "tv" + case .vision: + return "vision" case .unspecified: fallthrough @unknown default: @@ -1082,11 +1075,9 @@ class DeviceHelper { deviceDictionary["storeFrontCountryCode"] = storefront.countryCode deviceDictionary["storeFrontId"] = storefront.id - #if compiler(>=6.1) if #available(iOS 17.0, *) { deviceDictionary["storeFrontCurrency"] = storefront.currency?.identifier } - #endif } if Superwall.shared.options.enableExperimentalDeviceVariables { diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index df6182126c..6711f46fe4 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -1527,7 +1527,7 @@ extension PaywallViewController { if #available(iOS 15.0, *), !deviceHelper.isMac { - webView.setAllMediaPlaybackSuspended(false) // ignore-xcode-12 + webView.setAllMediaPlaybackSuspended(false) } if webView.loadingHandler.didFailToLoad, @@ -1671,7 +1671,7 @@ extension PaywallViewController { if #available(iOS 15.0, *), !deviceHelper.isMac { - webView.setAllMediaPlaybackSuspended(true) // ignore-xcode-12 + webView.setAllMediaPlaybackSuspended(true) } resetPresentationPreparations() diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift index dec62099c5..d2af3333ca 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift @@ -81,11 +81,10 @@ class SWWebView: WKWebView { if #available(iOS 15.0, *), !isMac { - preferences.isTextInteractionEnabled = featureFlags?.enableTextInteraction == true // ignore-xcode-12 + preferences.isTextInteractionEnabled = featureFlags?.enableTextInteraction == true } preferences.javaScriptCanOpenWindowsAutomatically = true - #if compiler(>=5.9.0) if #available(iOS 17.0, *) { if featureFlags?.enableThrottleSchedulingPolicy == true { preferences.inactiveSchedulingPolicy = .throttle @@ -93,7 +92,6 @@ class SWWebView: WKWebView { preferences.inactiveSchedulingPolicy = .none } } - #endif config.preferences = preferences wkConfig = config @@ -140,7 +138,7 @@ class SWWebView: WKWebView { scrollView.maximumZoomScale = 1.0 scrollView.minimumZoomScale = 1.0 isOpaque = false - #if compiler(>=5.8) && os(iOS) + #if os(iOS) if #available(iOS 16.4, *) { isInspectable = true } diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift index 73a3cc683c..bb0ecf0063 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift @@ -99,11 +99,9 @@ struct StoreKitSubscriptionStatusProvider: SubscriptionStatusProvider { @available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) func getOfferType(from transaction: Transaction) -> LatestSubscription.OfferType? { - #if compiler(>=6.0.0) if transaction.offer?.type == .winBack { return .winback } - #endif guard let offer = transaction.offer else { return nil } @@ -476,11 +474,9 @@ extension Transaction: EntitlementTransaction { var offerType: LatestSubscription.OfferType? { if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) { - #if compiler(>=6.0.0) if offer?.type == .winBack { return .winback } - #endif guard let offer = offer else { return nil } diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift index 0d266f332e..0065c412d1 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift @@ -91,7 +91,6 @@ actor ReceiptManager { } private func setAppTransactionId() async { - #if compiler(>=6.1) if #available(iOS 16.0, *), !shouldBypassAppTransactionCheck, !ProcessInfo.processInfo.arguments.contains("SUPERWALL_UNIT_TESTS") { @@ -109,7 +108,6 @@ actor ReceiptManager { } } } - #endif } private func registerAppTransactionIdIfNeeded() { diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index 058c97f0f5..f1e6dd9032 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -35,11 +35,7 @@ struct SK2StoreProduct: StoreProductType { entitlements: Set, billingPlanType: AppStoreProduct.BillingPlanType? = nil ) { - #if swift(<5.7) - self._underlyingSK2Product = sk2Product - #else self.underlyingSK2Product = sk2Product - #endif self.entitlements = entitlements self.billingPlanType = billingPlanType @@ -174,18 +170,7 @@ struct SK2StoreProduct: StoreProductType { ) } - #if swift(<5.7) - // We can't directly store instances of StoreKit.Product, since that causes - // linking issues in iOS < 15, even with @available checks correctly in place. - // So instead, we store the underlying product as Any and wrap it with casting. - private let _underlyingSK2Product: Any - var underlyingSK2Product: SK2Product { - // swiftlint:disable:next force_cast - _underlyingSK2Product as! SK2Product - } - #else let underlyingSK2Product: SK2Product - #endif var productIdentifier: String { underlyingSK2Product.id diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift index 7b584ec896..de5851eac4 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift @@ -93,13 +93,11 @@ final class ProductPurchaserSK2: Purchasing { options.insert(.appAccountToken(appAccountToken)) } - #if compiler(>=6.1) // Add intro offer eligibility token if available for this product // This allows overriding Apple's automatic eligibility determination if let token = product.introOfferToken { options.insert(.introductoryOfferEligibility(compactJWS: token.token)) } - #endif #if compiler(>=6.3.2) if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *), diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 39380da9e4..b1dd82f24c 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "4.17.0" + s.version = "4.18.0" s.summary = "Superwall: In-App Paywalls Made Easy" 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" @@ -35,7 +35,7 @@ Pod::Spec.new do |s| s.author = { "Jake Mor" => "jake@superwall.com" } s.documentation_url = "https://docs.superwall.com/" s.swift_versions = ['5.5'] - s.ios.deployment_target = '13.0' + s.ios.deployment_target = '15.0' s.requires_arc = true s.dependency 'Superscript', '1.0.15' diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index 09676e078c..59b549c205 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -4068,7 +4068,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SuperwallKit.xcodeproj/SuperwallKit_Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4092,7 +4092,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SuperwallKit.xcodeproj/SuperwallKit_Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/project.yml b/project.yml index 5d4094703d..eae758f36e 100644 --- a/project.yml +++ b/project.yml @@ -31,7 +31,7 @@ targets: NSPrincipalClass: "" type: framework platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "15.0" sources: [Sources/] dependencies: - package: Superscript diff --git a/xcode12.py b/xcode12.py deleted file mode 100644 index bbf496cf7f..0000000000 --- a/xcode12.py +++ /dev/null @@ -1,25 +0,0 @@ -import os, fnmatch - -pattern = "// ignore-xcode-12" - -def commentOut(n): - - if pattern in n: - return "// %s" % (n) - - return n - -def find(directory, filePattern): - for path, dirs, files in os.walk(os.path.abspath(directory)): - for filename in fnmatch.filter(files, filePattern): - filepath = os.path.join(path, filename) - with open(filepath) as f: - s = f.read() - if pattern in s: - lines = s.split("\n") - s = '\n'.join(list(map(commentOut, lines))) - with open(filepath, "w") as f: - f.write(s) - - -find("./Sources", "*.swift") From 4136dc8013fea5ab279bb038a174db9e48c76da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:09:04 +0200 Subject: [PATCH 02/11] Remove availability checks and fallbacks for iOS 15 and older With iOS 15, macOS 12 and watchOS 8 as the floor, every @available/#available check at or below them is always true. Drop them, their else branches (StoreKit 1 default on iOS 13/14, SecTrustEvaluate, action-sheet menus, pre-iOS 14 photo permission APIs), the now-unused action sheet helpers, and the matching test guards. Also guard keyboardDismissMode, which is unavailable on visionOS and broke that build. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- CLAUDE.md | 3 +- Package.swift | 2 +- .../Attribution/AttributionFetcher.swift | 53 ++++----- .../Attribution/AttributionPoster.swift | 12 +- .../TrackableSuperwallEvent.swift | 9 +- .../SuperwallKit/Config/ConfigManager.swift | 2 +- .../Config/Options/SuperwallOptions.swift | 6 +- .../DebugPaywallPickerViewController.swift | 17 ++- .../Debug/DebugViewController.swift | 8 +- .../Dependencies/DependencyContainer.swift | 1 - .../Dependencies/FactoryProtocols.swift | 1 - .../Extensions/PurchaseResult+Internal.swift | 1 - .../SystemInfo+NotificationName.swift | 5 +- .../Transaction/Transaction+LatestSince.swift | 1 - .../VerificationResult+Transaction.swift | 1 - .../Models/SW Product/SWProduct.swift | 5 +- .../Models/SW Product/SWProductDiscount.swift | 1 - .../SWProductSubscriptionPeriod.swift | 1 - .../Network/Device Helper/DeviceHelper.swift | 11 +- .../Get Paywall/SwiftUI/PaywallView.swift | 4 - .../CheckoutWebViewController.swift | 19 +-- .../PaywallViewController.swift | 24 ++-- .../View Controller/Web View/SWWebView.swift | 3 +- ...AuthorizationStatus+PermissionStatus.swift | 1 - .../Handlers/PermissionHandler+Camera.swift | 7 +- .../Handlers/PermissionHandler+Photos.swift | 18 +-- .../PermissionsHandler+Tracking.swift | 7 -- .../Product Fetching/ProductsFetcherSK2.swift | 1 - .../Product Fetching/ProductsManager.swift | 3 +- .../ASN1Swift/PKCS7/PKCS7.swift | 1 - .../EntitlementProcessor.swift | 4 - .../Receipt Manager/ReceiptManager.swift | 6 +- .../Receipt Manager/SK2ReceiptManager.swift | 1 - .../Validation/Validation.swift | 51 ++------ .../Discount/SK2StoreProductDiscount.swift | 2 - .../Discount/StoreProductDiscount.swift | 4 - .../StoreProduct/SK2StoreProduct.swift | 2 - .../StoreProduct/Sk1StoreProduct.swift | 1 - .../Products/StoreProduct/StoreProduct.swift | 6 - .../StoreProduct/StoreProductType.swift | 1 - .../StoreProduct/StripeProductType.swift | 1 - .../StoreProduct/SubscriptionPeriod.swift | 2 - .../Purchasing/ProductPurchaserLogic.swift | 16 ++- .../Purchasing/PurchaseManager.swift | 4 +- .../Purchasing/PurchasingCoordinator.swift | 31 ++--- .../StoreKit 1/ProductPurchaserSK1.swift | 20 ++-- .../StoreKit 2/ProductPurchaserSK2.swift | 1 - .../SK2ObserverModePurchaseDetector.swift | 2 - .../StoreKit 2/SK2TransactionListener.swift | 1 - .../SK2StoreTransaction.swift | 1 - .../StoreTransaction/StorePayment.swift | 1 - .../StoreTransaction/StoreTransaction.swift | 2 - .../Transactions/TransactionErrorLogic.swift | 15 +-- Sources/SuperwallKit/Superwall.swift | 7 +- .../Alert/TestModeEntitlementRowView.swift | 80 ++----------- .../TestMode/Alert/TestModeInfoCell.swift | 6 +- ...estModeModalViewController+TableView.swift | 41 ++----- .../TestMode/TestModePurchaseDrawer.swift | 8 +- .../TestMode/TestModeRestoreDrawer.swift | 8 +- .../AdServicesAttributionTests.swift | 12 -- .../OccurrenceLogicTests.swift | 1 - .../MockSkProduct.swift | 1 - .../Config/ConfigManagerTests.swift | 1 - .../Core Data/CoreDataManagerMock.swift | 1 - .../Products/ProductsFetcherSK2Tests.swift | 9 -- .../EntitlementProcessorTests.swift | 4 - .../SK2ReceiptManagerTests.swift | 9 -- .../Products/SK2StoreProductCyclesTests.swift | 16 --- .../SK2PriceFormatRoundingTests.swift | 3 - .../AutomaticPurchaseControllerTests.swift | 3 - .../Purchasing/ProductPurchaserSK1Tests.swift | 1 - .../Web/WebEntitlementRedeemerTests.swift | 111 ------------------ 73 files changed, 143 insertions(+), 584 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f356e0476e..fbb30572a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ## 4.18.0 ### Breaking Changes -- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the minimum macOS target for Swift Package Manager from macOS 10.12 to macOS 12. +- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS and watchOS to macOS 12 and watchOS 8. - Requires Xcode 26 (Swift 6.2) or later to build the SDK. Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026. The SDK itself still compiles in the Swift 5 language mode. ## 4.17.0 diff --git a/CLAUDE.md b/CLAUDE.md index 6e8053f225..493564428b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,8 +58,9 @@ SuperwallKit is an iOS SDK for remote paywall configuration and A/B testing. The ### Minimum Toolchain and Platforms -- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12. Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. +- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12, watchOS 8. Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. - Don't add `#if compiler(...)` checks for anything below 6.2; only gate APIs newer than that (e.g. `compiler(>=6.3.2)` for iOS 26.4 StoreKit APIs). +- Don't add `@available`/`#available` checks or fallback branches for iOS 15 or older (StoreKit 2, sheet detents, `UIMenu` buttons and friends are always there). - The package pins `swiftLanguageModes: [.v5]`. Moving to Swift 6 language mode is a separate change. - Keep `Package.swift`, `SuperwallKit.podspec` and `project.yml` deployment targets in sync. diff --git a/Package.swift b/Package.swift index b062d65ce8..fe030e19d6 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( platforms: [ .iOS(.v15), .macOS(.v12), - .watchOS("6.2") + .watchOS(.v8) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. diff --git a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift index db6fbff271..96d5d0eaba 100644 --- a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift +++ b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift @@ -39,32 +39,31 @@ final class AttributionFetcher { // should match available platforms here: // https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - if #available(macCatalyst 13.1, macOS 10.14, *) { - let identifierManagerProxy = AttributionTypeFactory.asIdProxy() - guard let identifierManagerProxy = identifierManagerProxy else { - Logger.debug( - logLevel: .warn, - scope: .analytics, - message: "AdSupport framework not imported. Attribution data incomplete." - ) - return nil - } - - guard let identifierValue = identifierManagerProxy.adsIdentifier else { - return nil - } + let identifierManagerProxy = AttributionTypeFactory.asIdProxy() + guard let identifierManagerProxy = identifierManagerProxy else { + Logger.debug( + logLevel: .warn, + scope: .analytics, + message: "AdSupport framework not imported. Attribution data incomplete." + ) + return nil + } - // When ATT hasn't been authorized iOS returns the all-zeros UUID - // sentinel. Don't pass that through as an IDFA — it pollutes attribution - // payloads with junk that downstream MMPs treat as a real id. - if identifierValue == Self.zeroAdvertisingIdentifier { - return nil - } + guard let identifierValue = identifierManagerProxy.adsIdentifier else { + return nil + } - return identifierValue.uuidString + // When ATT hasn't been authorized iOS returns the all-zeros UUID + // sentinel. Don't pass that through as an IDFA — it pollutes attribution + // payloads with junk that downstream MMPs treat as a real id. + if identifierValue == Self.zeroAdvertisingIdentifier { + return nil } - #endif + + return identifierValue.uuidString + #else return nil + #endif } // Non-optional construction via `init(uuid:)` — `init(uuidString:)` returns @@ -89,8 +88,6 @@ final class AttributionFetcher { #endif } - // should match OS availability in https://developer.apple.com/documentation/ad_services - @available(iOS 14.3, tvOS 14.3, macOS 11.1, watchOS 6.2, macCatalyst 14.3, *) var adServicesToken: String? { get async throws { #if canImport(AdServices) @@ -113,7 +110,6 @@ final class AttributionFetcher { } #if canImport(AdServices) - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) private static var realAdServicesToken: String? { get throws { return try AAAttribution.attributionToken() @@ -307,11 +303,10 @@ extension AttributionFetcher { return attStatusProvider() } #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || os(macOS) || os(visionOS) - if #available(iOS 14, macCatalyst 14, tvOS 14, macOS 11, *) { - return TrackingManagerProxy().trackingAuthorizationStatus() - } - #endif + return TrackingManagerProxy().trackingAuthorizationStatus() + #else return nil + #endif } private var currentDeviceIdentifiers: [String: String] { diff --git a/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift b/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift index bf118d70c8..c6207fcb03 100644 --- a/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift +++ b/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift @@ -58,9 +58,7 @@ final class AttributionPoster { self.configManager = configManager self.attributionFetcher = attributionFetcher - if #available(iOS 14.3, *) { - listenToConfig() - } + listenToConfig() NotificationCenter.default.addObserver( self, @@ -70,7 +68,6 @@ final class AttributionPoster { ) } - @available(iOS 14.3, *) private func listenToConfig() { // Track the enabled flag across config refreshes. `removeDuplicates` has // to see the toggles to detect a transition, so we dedup on the bool @@ -106,9 +103,7 @@ final class AttributionPoster { // deferred indefinitely under load, but the attribution token is only // useful within ~24h of install. Task(priority: .utility) { - if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - await getAdServicesTokenIfNeeded() - } + await getAdServicesTokenIfNeeded() } #endif } @@ -143,7 +138,6 @@ final class AttributionPoster { } // Should match OS availability in https://developer.apple.com/documentation/ad_services - @available(iOS 14.3, tvOS 14.3, watchOS 6.2, macOS 11.1, macCatalyst 14.3, *) @available(tvOS, unavailable) @available(watchOS, unavailable) func getAdServicesTokenIfNeeded() async { @@ -234,7 +228,6 @@ final class AttributionPoster { return true } - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) // swiftlint:disable:next function_body_length private func runAttempt(existingAttempts: AdServicesAttributionAttempts?) async { // Fire `.start` from inside the task body so the event isn't orphaned if @@ -342,7 +335,6 @@ final class AttributionPoster { /// errors a few times in-session. `AAAttribution.attributionToken()` can /// throw `networkError` very early in the app's lifecycle even though the /// next call milliseconds later would succeed. - @available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) private func fetchTokenWithBackoff() async throws -> String { var lastError: Error? for delay in [0.0] + Self.tokenFetchBackoff { diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 26495cb3c7..7639c8e953 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -727,12 +727,9 @@ enum InternalSuperwallEvent { } func getSuperwallParameters() async -> [String: Any] { - var storefrontCountryCode = "" - var storefrontId = "" - if #available(iOS 15.0, *) { - storefrontCountryCode = await Storefront.current?.countryCode ?? "" - storefrontId = await Storefront.current?.id ?? "" - } + let storefront = await Storefront.current + let storefrontCountryCode = storefront?.countryCode ?? "" + let storefrontId = storefront?.id ?? "" var placementParams: [String: Any] = [ "store": store.description, "source": source.rawValue diff --git a/Sources/SuperwallKit/Config/ConfigManager.swift b/Sources/SuperwallKit/Config/ConfigManager.swift index bc2c095813..3ef599d79f 100644 --- a/Sources/SuperwallKit/Config/ConfigManager.swift +++ b/Sources/SuperwallKit/Config/ConfigManager.swift @@ -261,7 +261,7 @@ class ConfigManager { // StoreKit 2 only: its read is the slow one, and it is the only one that // saves the device rows the restore rebuilds from. A StoreKit 1 receipt is // parsed locally and saves no rows, so there would be nothing to restore. - guard #available(iOS 15.0, *), options.storeKitVersion == .storeKit2 else { + guard options.storeKitVersion == .storeKit2 else { return nil } guard let customerInfo = storage.get(LatestCustomerInfo.self) else { diff --git a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift index e0a0bf34f2..62d5f01802 100644 --- a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift +++ b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift @@ -520,11 +520,7 @@ public final class SuperwallOptions: NSObject, Encodable { self.storeKitVersion = .storeKit1 } } else { - if #available(iOS 15.0, *) { - self.storeKitVersion = .storeKit2 - } else { - self.storeKitVersion = .storeKit1 - } + self.storeKitVersion = .storeKit2 } } diff --git a/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift b/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift index 29285c7a55..8b5cf07482 100644 --- a/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift @@ -24,7 +24,9 @@ final class DebugPaywallPickerViewController: UIViewController { table.separatorColor = UIColor.white.withAlphaComponent(0.1) table.dataSource = self table.delegate = self + #if !os(visionOS) table.keyboardDismissMode = .onDrag + #endif table.translatesAutoresizingMaskIntoConstraints = false return table }() @@ -141,16 +143,11 @@ extension DebugPaywallPickerViewController: UITableViewDataSource, UITableViewDe // A grouped header renders through its content configuration on iOS 14+, // which ignores `textLabel` — the default grey is unreadable on the // debugger's near-black sheet. - if #available(iOS 14.0, *) { - var configuration = header.defaultContentConfiguration() - configuration.text = sections[section].title - configuration.textProperties.color = UIColor.white.withAlphaComponent(0.5) - configuration.textProperties.font = .systemFont(ofSize: 13, weight: .semibold) - header.contentConfiguration = configuration - } else { - header.textLabel?.textColor = UIColor.white.withAlphaComponent(0.5) - header.textLabel?.font = .systemFont(ofSize: 13, weight: .semibold) - } + var configuration = header.defaultContentConfiguration() + configuration.text = sections[section].title + configuration.textProperties.color = UIColor.white.withAlphaComponent(0.5) + configuration.textProperties.font = .systemFont(ofSize: 13, weight: .semibold) + header.contentConfiguration = configuration } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index fbba1dfc30..2319f35203 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -500,11 +500,9 @@ final class DebugViewController: UIViewController { navigationController.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] navigationController.modalPresentationStyle = .pageSheet #if !os(visionOS) - if #available(iOS 15.0, *) { - if let sheet = navigationController.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = true - } + if let sheet = navigationController.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true } #endif present(navigationController, animated: true) diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index b33de65785..6d39174f64 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -527,7 +527,6 @@ extension DependencyContainer: StoreTransactionFactory { ) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) func makeStoreTransaction(from transaction: SK2Transaction) async -> StoreTransaction { return StoreTransaction( transaction: SK2StoreTransaction(transaction: transaction), diff --git a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift index f0ea1dce20..a7ece509d7 100644 --- a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift +++ b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift @@ -142,7 +142,6 @@ protocol ApiFactory: AnyObject { protocol StoreTransactionFactory: AnyObject { func makeStoreTransaction(from transaction: SK1Transaction) async -> StoreTransaction - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) func makeStoreTransaction(from transaction: SK2Transaction) async -> StoreTransaction func makeStoreTransaction(from transaction: CustomStoreTransaction) async -> StoreTransaction diff --git a/Sources/SuperwallKit/Misc/Extensions/PurchaseResult+Internal.swift b/Sources/SuperwallKit/Misc/Extensions/PurchaseResult+Internal.swift index e608e4b7f0..e0b601e8dc 100644 --- a/Sources/SuperwallKit/Misc/Extensions/PurchaseResult+Internal.swift +++ b/Sources/SuperwallKit/Misc/Extensions/PurchaseResult+Internal.swift @@ -7,7 +7,6 @@ import StoreKit -@available(iOS 15.0, *) extension StoreKit.Product.PurchaseResult { /// Converts a StoreKit 2 `PurchaseResult` to a Superwall `PurchaseResult`. func toInternalPurchaseResult(_ coordinator: PurchasingCoordinator) async -> PurchaseResult { diff --git a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift index bb5efcccc5..ee518aa1e0 100644 --- a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift +++ b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift @@ -34,12 +34,9 @@ enum SystemInfo { #elseif os(watchOS) if #available(watchOS 9, *) { return WKApplication.didBecomeActiveNotification - } else if #available(watchOS 7, *) { + } else { // Work around for "Symbol not found" dyld crashes on watchOS 7.0..<9.0 return Notification.Name("WKApplicationDidBecomeActiveNotification") - } else { - // There's no equivalent notification available on watchOS <7. - return nil } #endif } diff --git a/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift b/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift index efac553f0d..83415778cd 100644 --- a/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift +++ b/Sources/SuperwallKit/Misc/Extensions/Transaction/Transaction+LatestSince.swift @@ -7,7 +7,6 @@ import StoreKit -@available(iOS 15.0, *) extension Transaction { /// Gets the latest transaction for a given `productId` since /// an hour up to a given `purchaseDate`. diff --git a/Sources/SuperwallKit/Misc/Extensions/Transaction/VerificationResult+Transaction.swift b/Sources/SuperwallKit/Misc/Extensions/Transaction/VerificationResult+Transaction.swift index 517d1b266f..15b1989ec1 100644 --- a/Sources/SuperwallKit/Misc/Extensions/Transaction/VerificationResult+Transaction.swift +++ b/Sources/SuperwallKit/Misc/Extensions/Transaction/VerificationResult+Transaction.swift @@ -13,7 +13,6 @@ import StoreKit -@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) extension StoreKit.VerificationResult where SignedType == StoreKit.Transaction { var verifiedTransaction: StoreKit.Transaction? { switch self { diff --git a/Sources/SuperwallKit/Models/SW Product/SWProduct.swift b/Sources/SuperwallKit/Models/SW Product/SWProduct.swift index cec5853cbc..05840fb88b 100644 --- a/Sources/SuperwallKit/Models/SW Product/SWProduct.swift +++ b/Sources/SuperwallKit/Models/SW Product/SWProduct.swift @@ -37,9 +37,7 @@ struct SWProduct: Codable { #endif downloadContentVersion = product.downloadContentVersion - if #available(iOS 14.0, *) { - isFamilyShareable = product.isFamilyShareable - } + isFamilyShareable = product.isFamilyShareable discounts = product.discounts.map(SWProductDiscount.init) @@ -56,7 +54,6 @@ struct SWProduct: Codable { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) init(product: SK2Product) { localizedDescription = product.description localizedTitle = product.displayName diff --git a/Sources/SuperwallKit/Models/SW Product/SWProductDiscount.swift b/Sources/SuperwallKit/Models/SW Product/SWProductDiscount.swift index 2ce4625610..20fc387472 100644 --- a/Sources/SuperwallKit/Models/SW Product/SWProductDiscount.swift +++ b/Sources/SuperwallKit/Models/SW Product/SWProductDiscount.swift @@ -60,7 +60,6 @@ struct SWProductDiscount: Codable { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) init( offer: StoreKit.Product.SubscriptionOffer, fromProduct product: SK2Product diff --git a/Sources/SuperwallKit/Models/SW Product/SWProductSubscriptionPeriod.swift b/Sources/SuperwallKit/Models/SW Product/SWProductSubscriptionPeriod.swift index e05f28d609..46f37ce238 100644 --- a/Sources/SuperwallKit/Models/SW Product/SWProductSubscriptionPeriod.swift +++ b/Sources/SuperwallKit/Models/SW Product/SWProductSubscriptionPeriod.swift @@ -80,7 +80,6 @@ struct SWProductSubscriptionPeriod: Codable { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) init(period: StoreKit.Product.SubscriptionPeriod, numberOfPeriods: Int) { self.numberOfUnits = period.value * numberOfPeriods switch period.unit { diff --git a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift index 118bcdd3a5..0c932fcb29 100644 --- a/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift +++ b/Sources/SuperwallKit/Network/Device Helper/DeviceHelper.swift @@ -52,13 +52,7 @@ class DeviceHelper { ) }() - let isMac: Bool = { - var output = false - if #available(iOS 14.0, *) { - output = ProcessInfo.processInfo.isiOSAppOnMac - } - return output - }() + let isMac = ProcessInfo.processInfo.isiOSAppOnMac let model: String = { UIDevice.modelName @@ -1070,8 +1064,7 @@ class DeviceHelper { // the existing values. deviceDictionary.merge(enrichmentDict) { current, _ in current } - if #available(iOS 15.0, *), - let storefront = await Storefront.current { + if let storefront = await Storefront.current { deviceDictionary["storeFrontCountryCode"] = storefront.countryCode deviceDictionary["storeFrontId"] = storefront.id diff --git a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/SwiftUI/PaywallView.swift b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/SwiftUI/PaywallView.swift index 5b79d0491f..2fd6c481d9 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/SwiftUI/PaywallView.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Get Paywall/SwiftUI/PaywallView.swift @@ -13,7 +13,6 @@ import SwiftUI /// /// - Warning: You're responsible for the deallocation of this view. If you have a `PaywallView` presented somewhere /// and you try to present the same `PaywallView` elsewhere, you will get a crash. -@available(iOS 14.0, *) public struct PaywallView< OnErrorView: View, OnSkippedView: View @@ -150,7 +149,6 @@ public struct PaywallView< } } -@available(iOS 14.0, *) extension PaywallView where OnSkippedView == Never, OnErrorView == Never { /// A SwiftUI paywall view that you can embed into your app. /// @@ -190,7 +188,6 @@ extension PaywallView where OnSkippedView == Never, OnErrorView == Never { } } -@available(iOS 14.0, *) extension PaywallView where OnSkippedView == Never { /// A SwiftUI paywall view that you can embed into your app. /// @@ -233,7 +230,6 @@ extension PaywallView where OnSkippedView == Never { } } -@available(iOS 14.0, *) extension PaywallView where OnErrorView == Never { /// A SwiftUI paywall view that you can embed into your app. /// diff --git a/Sources/SuperwallKit/Paywall/View Controller/CheckoutWebViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/CheckoutWebViewController.swift index 3eb3f25d06..f3d13ee27d 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/CheckoutWebViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/CheckoutWebViewController.swift @@ -59,9 +59,7 @@ final class CheckoutWebViewController: UIViewController { webView.load(request) // Set self as presentation controller delegate to track detent changes - if #available(iOS 15.0, *) { - presentationController?.delegate = self - } + presentationController?.delegate = self // Add keyboard observers to detect when keyboard causes detent changes setupKeyboardObservers() @@ -108,9 +106,7 @@ final class CheckoutWebViewController: UIViewController { guard let sheetController = self?.sheetPresentationController else { return } sheetController.invalidateDetents() } - if #available(iOS 15.0, *) { - self?.updateScrollingForDetent() - } + self?.updateScrollingForDetent() } } @@ -119,7 +115,6 @@ final class CheckoutWebViewController: UIViewController { onDismiss?() } - @available(iOS 15.0, *) private func updateScrollingForDetent() { guard let sheetController = sheetPresentationController else { return @@ -191,7 +186,6 @@ final class CheckoutWebViewController: UIViewController { webView.evaluateJavaScript(fieldFocusScript) } - @available(iOS 15.0, *) private func expandToLargeDetent() { guard let sheetController = sheetPresentationController else { return @@ -237,9 +231,7 @@ extension CheckoutWebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { // Hide loading indicator if needed // Initial scroll state update after page loads - if #available(iOS 15.0, *) { - updateScrollingForDetent() - } + updateScrollingForDetent() // Inject JavaScript to detect field focus events injectFieldFocusDetection() @@ -247,7 +239,6 @@ extension CheckoutWebViewController: WKNavigationDelegate { } // MARK: - UISheetPresentationControllerDelegate -@available(iOS 15.0, *) extension CheckoutWebViewController: UISheetPresentationControllerDelegate { func sheetPresentationControllerDidChangeSelectedDetentIdentifier( _ sheetPresentationController: UISheetPresentationController @@ -263,9 +254,7 @@ extension CheckoutWebViewController: WKScriptMessageHandler { didReceive message: WKScriptMessage ) { if message.name == "fieldFocus" { - if #available(iOS 15.0, *) { - expandToLargeDetent() - } + expandToLargeDetent() } } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 6711f46fe4..4b8a06500b 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -1319,14 +1319,12 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { } checkoutVC.modalPresentationStyle = .pageSheet - if #available(iOS 15.0, *) { - if let sheet = checkoutVC.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = true - // sheet.prefersScrollingExpandsWhenScrolledToEdge = true - sheet.prefersEdgeAttachedInCompactHeight = true - sheet.preferredCornerRadius = 62 - } + if let sheet = checkoutVC.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + // sheet.prefersScrollingExpandsWhenScrolledToEdge = true + sheet.prefersEdgeAttachedInCompactHeight = true + sheet.preferredCornerRadius = 62 } self.isSafariVCPresented = true loadingState = .loadingPurchase @@ -1449,10 +1447,8 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { if let scene = view.window?.windowScene { if #available(iOS 16.0, *) { AppStore.requestReview(in: scene) - } else if #available(iOS 14.0, *) { - SKStoreReviewController.requestReview(in: scene) } else { - SKStoreReviewController.requestReview() + SKStoreReviewController.requestReview(in: scene) } } else { SKStoreReviewController.requestReview() @@ -1525,8 +1521,7 @@ extension PaywallViewController { return } - if #available(iOS 15.0, *), - !deviceHelper.isMac { + if !deviceHelper.isMac { webView.setAllMediaPlaybackSuspended(false) } @@ -1669,8 +1664,7 @@ extension PaywallViewController { await trackClose() } - if #available(iOS 15.0, *), - !deviceHelper.isMac { + if !deviceHelper.isMac { webView.setAllMediaPlaybackSuspended(true) } diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift index d2af3333ca..c08cc001c0 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift @@ -79,8 +79,7 @@ class SWWebView: WKWebView { let preferences = WKPreferences() - if #available(iOS 15.0, *), - !isMac { + if !isMac { preferences.isTextInteractionEnabled = featureFlags?.enableTextInteraction == true } preferences.javaScriptCanOpenWindowsAutomatically = true diff --git a/Sources/SuperwallKit/Permissions/AuthorizationStatus+PermissionStatus.swift b/Sources/SuperwallKit/Permissions/AuthorizationStatus+PermissionStatus.swift index b3eb4ca85b..e3006e676c 100644 --- a/Sources/SuperwallKit/Permissions/AuthorizationStatus+PermissionStatus.swift +++ b/Sources/SuperwallKit/Permissions/AuthorizationStatus+PermissionStatus.swift @@ -39,7 +39,6 @@ extension PHAuthorizationStatus { } } -@available(macCatalyst 14.0, *) extension AVAuthorizationStatus { var toPermissionStatus: PermissionStatus { switch self { diff --git a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift index 31355df979..e931fbb397 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift @@ -10,9 +10,7 @@ import AVFoundation extension PermissionHandler { func checkCameraPermission() -> PermissionStatus { #if targetEnvironment(macCatalyst) - if #available(macCatalyst 14.0, *) { - return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus - } + return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus return .unsupported #else return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus @@ -22,9 +20,6 @@ extension PermissionHandler { @MainActor func requestCameraPermission() async -> PermissionStatus { #if targetEnvironment(macCatalyst) - guard #available(macCatalyst 14.0, *) else { - return .unsupported - } #endif guard hasPlistKey(PlistKey.camera) else { diff --git a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Photos.swift b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Photos.swift index 9691cd901b..e1587a8ca6 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Photos.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Photos.swift @@ -10,11 +10,7 @@ import Photos extension PermissionHandler { func checkPhotosPermission() -> PermissionStatus { let status: PHAuthorizationStatus - if #available(iOS 14, *) { - status = PHPhotoLibrary.authorizationStatus(for: .readWrite) - } else { - status = PHPhotoLibrary.authorizationStatus() - } + status = PHPhotoLibrary.authorizationStatus(for: .readWrite) return status.toPermissionStatus } @@ -30,15 +26,7 @@ extension PermissionHandler { return .granted } - if #available(iOS 14, *) { - let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite) - return status.toPermissionStatus - } else { - return await withCheckedContinuation { continuation in - PHPhotoLibrary.requestAuthorization { status in - continuation.resume(returning: status.toPermissionStatus) - } - } - } + let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite) + return status.toPermissionStatus } } diff --git a/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift b/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift index b8223237d1..5d2e13ac33 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift @@ -9,9 +9,6 @@ import Foundation extension PermissionHandler { func checkTrackingPermission() -> PermissionStatus { - guard #available(iOS 14, macCatalyst 14.0, macOS 11.0, tvOS 14.0, *) else { - return .unsupported - } let proxy = TrackingManagerProxy() let raw = proxy.trackingAuthorizationStatus() return raw.toTrackingPermissionStatus @@ -19,10 +16,6 @@ extension PermissionHandler { @MainActor func requestTrackingPermission() async -> PermissionStatus { - guard #available(iOS 14, macCatalyst 14.0, macOS 11.0, tvOS 14.0, *) else { - return .unsupported - } - guard hasPlistKey(PlistKey.tracking) else { await showMissingPlistKeyAlert( for: PlistKey.tracking, diff --git a/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsFetcherSK2.swift b/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsFetcherSK2.swift index 84f4110186..c3a8a43927 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsFetcherSK2.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsFetcherSK2.swift @@ -19,7 +19,6 @@ enum ProductsFetcherSK2Error: LocalizedError { } } -@available(iOS 15.0, *) final actor ProductsFetcherSK2: ProductFetchable { private unowned let entitlementsInfo: EntitlementsInfo private let numberOfRetries: Int diff --git a/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsManager.swift b/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsManager.swift index 8890823724..16aa0d448e 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsManager.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Product Fetching/ProductsManager.swift @@ -46,8 +46,7 @@ final class ProductsManager { self.productsFetcher = productsFetcher return } - if #available(iOS 15.0, *), - storeKitVersion == .storeKit2 { + if storeKitVersion == .storeKit2 { self.productsFetcher = ProductsFetcherSK2(entitlementsInfo: entitlementsInfo) } else { self.productsFetcher = ProductsFetcherSK1(entitlementsInfo: entitlementsInfo) diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/ASN1Swift/PKCS7/PKCS7.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/ASN1Swift/PKCS7/PKCS7.swift index 0d5f81de4e..c73719b323 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/ASN1Swift/PKCS7/PKCS7.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/ASN1Swift/PKCS7/PKCS7.swift @@ -28,7 +28,6 @@ enum OID: String extension OID { - @available(iOS 10.0, *) func encryptionAlgorithm() -> SecKeyAlgorithm { switch self diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift index bb0ecf0063..7a712cf7f9 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift @@ -57,7 +57,6 @@ enum EntitlementTransactionType { } /// Protocol for providing subscription status information -@available(iOS 15.0, *) protocol SubscriptionStatusProvider { func getSubscriptionStatus(for transaction: Transaction) async -> StoreKit.Product.SubscriptionInfo.Status? func getWillAutoRenew(from status: StoreKit.Product.SubscriptionInfo.Status?) -> Bool @@ -67,7 +66,6 @@ protocol SubscriptionStatusProvider { } /// Default implementation using StoreKit directly -@available(iOS 15.0, *) struct StoreKitSubscriptionStatusProvider: SubscriptionStatusProvider { func getSubscriptionStatus(for transaction: Transaction) async -> StoreKit.Product.SubscriptionInfo.Status? { return await transaction.subscriptionStatus @@ -300,7 +298,6 @@ enum EntitlementProcessor { } /// Build entitlements with live subscription data from StoreKit - @available(iOS 15.0, *) static func buildEntitlementsWithLiveSubscriptionData( from transactionsByEntitlement: [String: [any EntitlementTransaction]], rawEntitlementsByProductId: [String: Set], @@ -435,7 +432,6 @@ enum EntitlementProcessor { } // MARK: - StoreKit Transaction Adapter -@available(iOS 15.0, *) extension Transaction: EntitlementTransaction { var productId: String { productID } var transactionId: String { String(id) } diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift index 0065c412d1..b674c7ec5a 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/ReceiptManager.swift @@ -60,8 +60,7 @@ actor ReceiptManager { if let receiptManager = receiptManager { self.manager = receiptManager - } else if #available(iOS 15.0, *), - storeKitVersion == .storeKit2 { + } else if storeKitVersion == .storeKit2 { self.manager = Self.versionedManager(storeKitVersion: storeKitVersion) } else { self.manager = SK1ReceiptManager() @@ -78,8 +77,7 @@ actor ReceiptManager { static func versionedManager( storeKitVersion: SuperwallOptions.StoreKitVersion ) -> ReceiptManagerType { - if #available(iOS 15.0, *), - storeKitVersion == .storeKit2 { + if storeKitVersion == .storeKit2 { return SK2ReceiptManager() } else { return SK1ReceiptManager() diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/SK2ReceiptManager.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/SK2ReceiptManager.swift index e616304fa7..cec522e655 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/SK2ReceiptManager.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Receipt Manager/SK2ReceiptManager.swift @@ -34,7 +34,6 @@ struct PurchaseSnapshot { let customerInfo: CustomerInfo } -@available(iOS 15.0, *) actor SK2ReceiptManager: ReceiptManagerType { nonisolated let loadsSubscriptionGroupsFromProducts = false /// Resolves intro-offer eligibility live from StoreKit. Injectable so tests can diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift index 9164d1ea10..9bb179f4f6 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift @@ -5,7 +5,6 @@ // Created by Pavel Tikhonenko on 19/01/17. // Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. // -// swiftlint:disable cyclomatic_complexity function_body_length #if os(iOS) || os(tvOS) import UIKit @@ -194,40 +193,19 @@ extension InAppReceipt { throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) } - var secTrustResult = SecTrustResultType.unspecified - - if #available(OSX 10.14, tvOS 12.0, *) { - var error: CFError? - guard - let wwdcTrust = wwdcTrust, - SecTrustEvaluateWithError(wwdcTrust, &error) - else { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } else { - guard - let wwdcTrust = wwdcTrust, - SecTrustEvaluate(wwdcTrust, &secTrustResult) == errSecSuccess - else { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } + var error: CFError? + guard + let wwdcTrust = wwdcTrust, + SecTrustEvaluateWithError(wwdcTrust, &error) + else { + throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) } - if #available(OSX 10.14, tvOS 12.0, *) { - var error: CFError? - guard - let iTunesTrust = iTunesTrust, - SecTrustEvaluateWithError(iTunesTrust, &error) - else { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } - } else { - guard - let iTunesTrust = iTunesTrust, - SecTrustEvaluate(iTunesTrust, &secTrustResult) == errSecSuccess - else { - throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) - } + guard + let iTunesTrust = iTunesTrust, + SecTrustEvaluateWithError(iTunesTrust, &error) + else { + throw IARError.validationFailed(reason: .signatureValidation(.invalidCertificateChainOfTrust)) } } @@ -321,12 +299,7 @@ func getMacAddress() -> Data? { } func ioService(named name: String, wantBuiltIn: Bool) -> io_service_t? { - let mainPort: mach_port_t - if #available(macOS 12.0, macCatalyst 15.0, *) { - mainPort = kIOMainPortDefault - } else { - mainPort = 0 // the kIOMasterPortDefault symbol is unavailable on xcode 14 and later. - } + let mainPort = kIOMainPortDefault var iterator = io_iterator_t() defer { diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/SK2StoreProductDiscount.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/SK2StoreProductDiscount.swift index f427389d93..fea1c84603 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/SK2StoreProductDiscount.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/SK2StoreProductDiscount.swift @@ -14,7 +14,6 @@ import StoreKit -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) struct SK2StoreProductDiscount: StoreProductDiscountType { let underlyingSK2Discount: SK2ProductDiscount @@ -55,7 +54,6 @@ struct SK2StoreProductDiscount: StoreProductDiscountType { // MARK: - Private private extension StoreProductDiscount.PaymentMode { - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) init?(subscriptionOfferPaymentMode paymentMode: StoreKit.Product.SubscriptionOffer.PaymentMode) { switch paymentMode { case .payUpFront: diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/StoreProductDiscount.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/StoreProductDiscount.swift index e9c9d7cb90..a56b0fc72f 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/StoreProductDiscount.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Discount/StoreProductDiscount.swift @@ -19,7 +19,6 @@ import StoreKit public typealias SK1ProductDiscount = SKProductDiscount /// TypeAlias to StoreKit 2's Discount type, called `StoreKit.Product.SubscriptionOffer` -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias SK2ProductDiscount = StoreKit.Product.SubscriptionOffer /// Type that wraps `StoreKit.Product.SubscriptionOffer` and `SKProductDiscount` @@ -194,7 +193,6 @@ extension StoreProductDiscount { self.init(discount) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) convenience init?(sk2Discount: SK2ProductDiscount, currencyCode: String?) { guard let discount = SK2StoreProductDiscount( @@ -226,7 +224,6 @@ extension StoreProductDiscount { } /// Returns the `SK2ProductDiscount` if this `StoreProductDiscount` represents a `Product.SubscriptionOffer`. - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public var sk2Discount: SK2ProductDiscount? { return (self.discount as? SK2StoreProductDiscount)?.underlyingSK2Discount } @@ -244,7 +241,6 @@ extension StoreProductDiscount.DiscountType { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) static func from(sk2Discount: SK2ProductDiscount) -> Self? { switch sk2Discount.type { case SK2ProductDiscount.OfferType.introductory: diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index f1e6dd9032..299c9253a5 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -16,7 +16,6 @@ import Foundation import StoreKit -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) struct SK2StoreProduct: StoreProductType { private let priceFormatterProvider = PriceFormatterProvider() let entitlements: Set @@ -750,7 +749,6 @@ struct SK2StoreProduct: StoreProductType { } // MARK: - Hashable -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) extension SK2StoreProduct: Hashable { static func == (lhs: SK2StoreProduct, rhs: SK2StoreProduct) -> Bool { return lhs.underlyingSK2Product == rhs.underlyingSK2Product diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Sk1StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Sk1StoreProduct.swift index eacb64f879..a6c3625b6d 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Sk1StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/Sk1StoreProduct.swift @@ -554,7 +554,6 @@ struct SK1StoreProduct: StoreProductType { .compactMap(StoreProductDiscount.init) } - @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) var isFamilyShareable: Bool { underlyingSK1Product.isFamilyShareable } diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift index 2cb565fb4a..aea78ac56b 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProduct.swift @@ -20,7 +20,6 @@ import StoreKit public typealias SK1Product = SKProduct /// TypeAlias to StoreKit 2's Product type, called `StoreKit.Product` -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias SK2Product = StoreKit.Product /// A convenience wrapper around a StoreKit 1 or StoreKit 2 product. @@ -104,7 +103,6 @@ public final class StoreProduct: NSObject, StoreProductType, Sendable { } /// Returns the `Product` if this `StoreProduct` represents a `StoreKit.Product`. - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public var sk2Product: SK2Product? { return (product as? SK2StoreProduct)?.underlyingSK2Product } @@ -356,7 +354,6 @@ public final class StoreProduct: NSObject, StoreProductType, Sendable { } /// A boolean that indicates whether the product is family shareable. - @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) public var isFamilyShareable: Bool { product.isFamilyShareable } @@ -442,14 +439,12 @@ public final class StoreProduct: NSObject, StoreProductType, Sendable { self.init(SK1StoreProduct(sk1Product: sk1Product, entitlements: entitlements)) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public convenience init( sk2Product: SK2Product ) { self.init(sk2Product: sk2Product, entitlements: []) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) convenience init( sk2Product: SK2Product, entitlements: Set @@ -525,7 +520,6 @@ private struct BlankStoreProduct: StoreProductType { var introductoryDiscount: StoreProductDiscount? { nil } var discounts: [StoreProductDiscount] { [] } - @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) var isFamilyShareable: Bool { false } func trialPeriodPricePerUnit(_ unit: SubscriptionPeriod.Unit) -> String { "" } diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProductType.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProductType.swift index 80e3958992..cac345d02f 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProductType.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StoreProductType.swift @@ -110,7 +110,6 @@ protocol StoreProductType: Sendable { /// /// #### Related Articles /// - https://support.apple.com/en-us/HT201079 - @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 8.0, *) var isFamilyShareable: Bool { get } /// The period details for products that are subscriptions. diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StripeProductType.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StripeProductType.swift index 709ac3380b..4fa4267e27 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StripeProductType.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/StripeProductType.swift @@ -665,7 +665,6 @@ struct StripeProductType: StoreProductType { } // MARK: - Hashable -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) extension StripeProductType: Hashable { static func == (lhs: StripeProductType, rhs: StripeProductType) -> Bool { return lhs.entitlements == rhs.entitlements diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SubscriptionPeriod.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SubscriptionPeriod.swift index 21c54aa638..a1244ab9a2 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SubscriptionPeriod.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SubscriptionPeriod.swift @@ -82,7 +82,6 @@ public final class SubscriptionPeriod: NSObject, Sendable { .normalized() } - @available(iOS 15.0, tvOS 15.0, watchOS 8, *) static func from(sk2SubscriptionPeriod: StoreKit.Product.SubscriptionPeriod) -> SubscriptionPeriod? { guard let unit = SubscriptionPeriod.Unit.from(sk2PeriodUnit: sk2SubscriptionPeriod.unit) else { return nil @@ -220,7 +219,6 @@ private extension SubscriptionPeriod.Unit { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8, *) static func from(sk2PeriodUnit: StoreKit.Product.SubscriptionPeriod.Unit) -> Self? { switch sk2PeriodUnit { case .day: return .day diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserLogic.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserLogic.swift index 7a9da8559f..00a92d78a5 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserLogic.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/ProductPurchaserLogic.swift @@ -21,16 +21,14 @@ enum ProductPurchaserLogic { // Get latest transaction since the purchase date using sk2. // Check that it's verified. - if #available(iOS 15.0, *) { - if let verificationResult = await Transaction.latest( - for: productId, - since: purchaseDate - ) { - guard case .verified = verificationResult else { - throw PurchaseError.unverifiedTransaction - } - return + if let verificationResult = await Transaction.latest( + for: productId, + since: purchaseDate + ) { + guard case .verified = verificationResult else { + throw PurchaseError.unverifiedTransaction } + return } // Otherwise, check that the local receipt is valid. diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchaseManager.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchaseManager.swift index 222d0bfe22..74b618af37 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchaseManager.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchaseManager.swift @@ -24,7 +24,6 @@ final class PurchaseManager: Purchasing { // swiftlint:disable:next identifier_name var _sk2TransactionListener: Any? - @available(iOS 15.0, *) var sk2TransactionListener: SK2TransactionListener { // swiftlint:disable:next force_cast force_unwrapping return self._sk2TransactionListener! as! SK2TransactionListener @@ -49,8 +48,7 @@ final class PurchaseManager: Purchasing { let options = factory.makeSuperwallOptions() let shouldFinishTransactions = !hasPurchaseController && !options.shouldObservePurchases - if #available(iOS 15.0, *), - storeKitVersion == .storeKit2 { + if storeKitVersion == .storeKit2 { purchaser = ProductPurchaserSK2( identityManager: identityManager, receiptManager: receiptManager, diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift index 7b58ad89b8..b605969434 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift @@ -67,28 +67,24 @@ actor PurchasingCoordinator { func getLatestSK2Transaction() async -> StoreTransaction? { // If on iOS 15+, try and get latest transaction using SK2. - if #available(iOS 15.0, *) { - if let verificationResult = await Transaction.latest( - for: productId, - since: purchaseDate - ) { - // Skip verification step as this has already been done. - let transaction = verificationResult.unsafePayloadValue - return await factory.makeStoreTransaction(from: transaction) - } + if let verificationResult = await Transaction.latest( + for: productId, + since: purchaseDate + ) { + // Skip verification step as this has already been done. + let transaction = verificationResult.unsafePayloadValue + return await factory.makeStoreTransaction(from: transaction) } return nil } func getCurrentEntitlementTransaction() async -> StoreTransaction? { - if #available(iOS 15.0, *) { - if let verificationResult = await Transaction.currentEntitlement( - for: productId, - since: purchaseDate - ) { - let transaction = verificationResult.unsafePayloadValue - return await factory.makeStoreTransaction(from: transaction) - } + if let verificationResult = await Transaction.currentEntitlement( + for: productId, + since: purchaseDate + ) { + let transaction = verificationResult.unsafePayloadValue + return await factory.makeStoreTransaction(from: transaction) } return nil } @@ -182,7 +178,6 @@ actor PurchasingCoordinator { completion = nil } - @available(iOS 15.0, *) func storeTransaction( _ transaction: SK2Transaction, result: PurchaseResult diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 1/ProductPurchaserSK1.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 1/ProductPurchaserSK1.swift index 6390df56e0..cb6736aee5 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 1/ProductPurchaserSK1.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 1/ProductPurchaserSK1.swift @@ -245,17 +245,15 @@ extension ProductPurchaserSK1: SKPaymentTransactionObserver { break } - if #available(iOS 14, *) { - switch error.code { - case .overlayTimeout: - await trackTimeout(paywallViewController: paywallViewController) - return await coordinator.completePurchase( - of: skTransaction, - result: .cancelled - ) - default: - break - } + switch error.code { + case .overlayTimeout: + await trackTimeout(paywallViewController: paywallViewController) + return await coordinator.completePurchase( + of: skTransaction, + result: .cancelled + ) + default: + break } } await coordinator.completePurchase( diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift index de5851eac4..f420add380 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift @@ -8,7 +8,6 @@ import Foundation import StoreKit -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) final class ProductPurchaserSK2: Purchasing { private unowned let identityManager: IdentityManager private unowned let receiptManager: ReceiptManager diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2ObserverModePurchaseDetector.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2ObserverModePurchaseDetector.swift index d87b4add08..98cd212dc6 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2ObserverModePurchaseDetector.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2ObserverModePurchaseDetector.swift @@ -155,7 +155,6 @@ actor SK2ObserverModePurchaseDetector { /// A wrapper protocol that allows for abstracting out calls to an `AsyncSequence>`. /// This will usually be `Transaction.all` in production but allows us to inject custom AsyncSequences for testing. -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) protocol AllTransactionsProviderType: Sendable { func getAllTransactions() async -> [StoreKit.VerificationResult] func getMostRecentVerifiedTransaction( @@ -165,7 +164,6 @@ protocol AllTransactionsProviderType: Sendable { /// A concretete implementation of `AllTransactionsProviderType` that fetches /// transactions from StoreKit's ``StoreKit/Transaction/all`` -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct SK2AllTransactionsProvider: AllTransactionsProviderType, Sendable { func getAllTransactions() async -> [StoreKit.VerificationResult] { return await StoreKit.Transaction.all.extractValues() diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2TransactionListener.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2TransactionListener.swift index 2275cff338..1f7a4daeac 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2TransactionListener.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/SK2TransactionListener.swift @@ -8,7 +8,6 @@ import Foundation import StoreKit -@available(iOS 15.0, *) actor SK2TransactionListener { private(set) var taskHandle: Task? private let shouldFinishTransactions: Bool diff --git a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/SK2StoreTransaction.swift b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/SK2StoreTransaction.swift index bab93f43cc..30545570ff 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/SK2StoreTransaction.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/SK2StoreTransaction.swift @@ -8,7 +8,6 @@ import Foundation import StoreKit -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) struct SK2StoreTransaction: StoreTransactionType { let underlyingSK2Transaction: SK2Transaction diff --git a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StorePayment.swift b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StorePayment.swift index aa8b76772f..2375220f6d 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StorePayment.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StorePayment.swift @@ -27,7 +27,6 @@ public final class StorePayment: NSObject, Encodable, Sendable { self.discountIdentifier = payment.paymentDiscount?.identifier } - @available(iOS 15.0, *) init(from transaction: Transaction) { self.productIdentifier = transaction.productID self.quantity = transaction.purchasedQuantity diff --git a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StoreTransaction.swift b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StoreTransaction.swift index 8c84014d06..f760e37a28 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StoreTransaction.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/StoreTransaction/StoreTransaction.swift @@ -12,7 +12,6 @@ import StoreKit public typealias SK1Transaction = SKPaymentTransaction /// TypeAlias to StoreKit 2's Transaction type, called `StoreKit.Transaction` -@available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias SK2Transaction = StoreKit.Transaction /// A convenience wrapper around a StoreKit 1 or StoreKit 2 transaction. @@ -47,7 +46,6 @@ public final class StoreTransaction: NSObject, StoreTransactionType, Encodable { } /// Returns the `StoreKit.Transaction` if this `StoreTransaction` represents a `StoreKit.Transaction`. - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) public var sk2Transaction: SK2Transaction? { return (self.transaction as? SK2StoreTransaction)?.underlyingSK2Transaction } diff --git a/Sources/SuperwallKit/StoreKit/Transactions/TransactionErrorLogic.swift b/Sources/SuperwallKit/StoreKit/Transactions/TransactionErrorLogic.swift index 0575c9c8bb..a9877105bd 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/TransactionErrorLogic.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/TransactionErrorLogic.swift @@ -28,8 +28,7 @@ enum TransactionErrorLogic { triggers: Set, shouldShowPurchaseFailureAlert: Bool ) -> ErrorOutcome? { - if #available(iOS 15.0, *), - let error = error as? StoreKitError { + if let error = error as? StoreKitError { switch error { case .userCancelled: return .cancelled @@ -47,13 +46,11 @@ enum TransactionErrorLogic { break } - if #available(iOS 14, *) { - switch error.code { - case .overlayTimeout: - return .cancelled - default: - break - } + switch error.code { + case .overlayTimeout: + return .cancelled + default: + break } } diff --git a/Sources/SuperwallKit/Superwall.swift b/Sources/SuperwallKit/Superwall.swift index 08d4f46bcf..5d867bd004 100644 --- a/Sources/SuperwallKit/Superwall.swift +++ b/Sources/SuperwallKit/Superwall.swift @@ -328,7 +328,6 @@ public final class Superwall: NSObject, ObservableObject { /// /// Alternatively, you can subscribe to the published variable ``customerInfo`` or use the delegate /// method ``SuperwallDelegate/customerInfoDidChange(from:to:)``. - @available(iOS 15.0, *) public var customerInfoStream: AsyncStream { AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in if !customerInfo.isPlaceholder { @@ -1156,9 +1155,7 @@ public final class Superwall: NSObject, ObservableObject { await Superwall.shared.track(InternalSuperwallEvent.Reset()) #if os(iOS) || os(macOS) || os(visionOS) - if #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) { - await dependencyContainer.attributionPoster.getAdServicesTokenIfNeeded() - } + await dependencyContainer.attributionPoster.getAdServicesTokenIfNeeded() #endif } } @@ -1229,7 +1226,6 @@ public final class Superwall: NSObject, ObservableObject { /// - Returns: A ``PurchaseResult``. /// - Note: You only need to finish the transaction after this if you're providing a ``PurchaseController`` /// when configuring the SDK. Otherwise ``Superwall`` will handle this for you. - @available(iOS 15.0, *) public func purchase(_ product: StoreKit.Product) async -> PurchaseResult { if options.shouldObservePurchases { Logger.debug( @@ -1286,7 +1282,6 @@ public final class Superwall: NSObject, ObservableObject { /// when configuring the SDK. Otherwise ``Superwall`` will handle this for you. /// - Warning: You cannot use this function while also setting ``SuperwallOptions/shouldObservePurchases`` /// to `true`. - @available(iOS 15.0, *) public func purchase( _ product: StoreKit.Product, completion: @escaping (PurchaseResult) -> Void diff --git a/Sources/SuperwallKit/TestMode/Alert/TestModeEntitlementRowView.swift b/Sources/SuperwallKit/TestMode/Alert/TestModeEntitlementRowView.swift index 6d121c7516..8f9861429f 100644 --- a/Sources/SuperwallKit/TestMode/Alert/TestModeEntitlementRowView.swift +++ b/Sources/SuperwallKit/TestMode/Alert/TestModeEntitlementRowView.swift @@ -207,10 +207,8 @@ final class EntitlementRowView: UIView { updateOfferTypeButtonTitle() updateAppearance() updateOfferTypeVisibilityWithoutReset() - if #available(iOS 14.0, *) { - updateStateMenu() - updateOfferTypeMenu() - } + updateStateMenu() + updateOfferTypeMenu() if notify { notifySelectionChanged() @@ -253,22 +251,12 @@ final class EntitlementRowView: UIView { } private func setupMenus() { - if #available(iOS 14.0, *) { - updateStateMenu() - updateOfferTypeMenu() - stateButton.showsMenuAsPrimaryAction = true - offerTypeButton.showsMenuAsPrimaryAction = true - } else { - stateButton.addTarget(self, action: #selector(showStateActionSheet), for: .touchUpInside) - offerTypeButton.addTarget( - self, - action: #selector(showOfferTypeActionSheet), - for: .touchUpInside - ) - } + updateStateMenu() + updateOfferTypeMenu() + stateButton.showsMenuAsPrimaryAction = true + offerTypeButton.showsMenuAsPrimaryAction = true } - @available(iOS 14.0, *) private func updateStateMenu() { let actions = EntitlementStateOption.allCases.map { [weak self] option in UIAction( @@ -282,7 +270,6 @@ final class EntitlementRowView: UIView { stateButton.menu = UIMenu(children: actions) } - @available(iOS 14.0, *) private func updateOfferTypeMenu() { let actions = OfferTypeOption.allCases.map { [weak self] option in UIAction( @@ -296,57 +283,6 @@ final class EntitlementRowView: UIView { offerTypeButton.menu = UIMenu(children: actions) } - @objc private func showStateActionSheet() { - guard let viewController = findViewController() else { return } - - let alertController = UIAlertController( - title: "Select State", - message: nil, - preferredStyle: .actionSheet - ) - - for option in EntitlementStateOption.allCases { - let action = UIAlertAction(title: option.displayName, style: .default) { [weak self] _ in - self?.selectedStateOption = option - } - alertController.addAction(action) - } - - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - viewController.present(alertController, animated: true) - } - - @objc private func showOfferTypeActionSheet() { - guard let viewController = findViewController() else { return } - - let alertController = UIAlertController( - title: "Select Offer Type", - message: nil, - preferredStyle: .actionSheet - ) - - for option in OfferTypeOption.allCases { - let action = UIAlertAction(title: option.displayName, style: .default) { [weak self] _ in - self?.selectedOfferType = option - } - alertController.addAction(action) - } - - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - viewController.present(alertController, animated: true) - } - - private func findViewController() -> UIViewController? { - var responder: UIResponder? = self - while let nextResponder = responder?.next { - if let viewController = nextResponder as? UIViewController { - return viewController - } - responder = nextResponder - } - return nil - } - private func updateStateButtonTitle() { stateButton.setTitle(selectedStateOption.displayName + " \u{25BE}", for: .normal) } @@ -362,9 +298,7 @@ final class EntitlementRowView: UIView { if selectedStateOption == .inactive { selectedOfferType = .none // Update the menu to reflect the reset - if #available(iOS 14.0, *) { - updateOfferTypeMenu() - } + updateOfferTypeMenu() } } diff --git a/Sources/SuperwallKit/TestMode/Alert/TestModeInfoCell.swift b/Sources/SuperwallKit/TestMode/Alert/TestModeInfoCell.swift index 8dc1437882..8038930e4e 100644 --- a/Sources/SuperwallKit/TestMode/Alert/TestModeInfoCell.swift +++ b/Sources/SuperwallKit/TestMode/Alert/TestModeInfoCell.swift @@ -113,10 +113,8 @@ final class TestModeInfoCell: UITableViewCell { if hasNavigation { let chevron = UIImageView() - if #available(iOS 13.0, *) { - let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold) - chevron.image = UIImage(systemName: "chevron.right", withConfiguration: config) - } + let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold) + chevron.image = UIImage(systemName: "chevron.right", withConfiguration: config) chevron.tintColor = primaryColor chevron.sizeToFit() accessoryView = chevron diff --git a/Sources/SuperwallKit/TestMode/Alert/TestModeModalViewController+TableView.swift b/Sources/SuperwallKit/TestMode/Alert/TestModeModalViewController+TableView.swift index 6c830b4826..f40cb1c5da 100644 --- a/Sources/SuperwallKit/TestMode/Alert/TestModeModalViewController+TableView.swift +++ b/Sources/SuperwallKit/TestMode/Alert/TestModeModalViewController+TableView.swift @@ -53,21 +53,17 @@ extension TestModeModalViewController { valueButton.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) valueButton.contentHorizontalAlignment = .right - if #available(iOS 14.0, *) { - let actions = FreeTrialOverride.allCases.map { [weak self] option in - UIAction( - title: option.displayName, - state: self?.selectedFreeTrialOverride == option ? .on : .off - ) { [weak self] _ in - self?.selectedFreeTrialOverride = option - self?.reloadFreeTrialSection() - } + let actions = FreeTrialOverride.allCases.map { [weak self] option in + UIAction( + title: option.displayName, + state: self?.selectedFreeTrialOverride == option ? .on : .off + ) { [weak self] _ in + self?.selectedFreeTrialOverride = option + self?.reloadFreeTrialSection() } - valueButton.menu = UIMenu(children: actions) - valueButton.showsMenuAsPrimaryAction = true - } else { - valueButton.addTarget(self, action: #selector(showFreeTrialActionSheet), for: .touchUpInside) } + valueButton.menu = UIMenu(children: actions) + valueButton.showsMenuAsPrimaryAction = true let textStack = UIStackView(arrangedSubviews: [titleLabel, detailLabel]) textStack.axis = .vertical @@ -87,25 +83,6 @@ extension TestModeModalViewController { valueButton.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor) ]) } - - @objc func showFreeTrialActionSheet() { - let alertController = UIAlertController( - title: "Free Trial Override", - message: nil, - preferredStyle: .actionSheet - ) - - for option in FreeTrialOverride.allCases { - let action = UIAlertAction(title: option.displayName, style: .default) { [weak self] _ in - self?.selectedFreeTrialOverride = option - self?.reloadFreeTrialSection() - } - alertController.addAction(action) - } - - alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - present(alertController, animated: true) - } } // MARK: - Row Building diff --git a/Sources/SuperwallKit/TestMode/TestModePurchaseDrawer.swift b/Sources/SuperwallKit/TestMode/TestModePurchaseDrawer.swift index dd605bbc6f..5474748e51 100644 --- a/Sources/SuperwallKit/TestMode/TestModePurchaseDrawer.swift +++ b/Sources/SuperwallKit/TestMode/TestModePurchaseDrawer.swift @@ -39,11 +39,9 @@ enum TestModePurchaseDrawer { ) modal.modalPresentationStyle = .pageSheet #if !os(visionOS) - if #available(iOS 15.0, *) { - if let sheet = modal.sheetPresentationController { - sheet.detents = [.medium()] - sheet.prefersGrabberVisible = false - } + if let sheet = modal.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = false } #endif viewController.present(modal, animated: true) diff --git a/Sources/SuperwallKit/TestMode/TestModeRestoreDrawer.swift b/Sources/SuperwallKit/TestMode/TestModeRestoreDrawer.swift index 8afc4a5342..05902614dd 100644 --- a/Sources/SuperwallKit/TestMode/TestModeRestoreDrawer.swift +++ b/Sources/SuperwallKit/TestMode/TestModeRestoreDrawer.swift @@ -28,11 +28,9 @@ enum TestModeRestoreDrawer { ) modal.modalPresentationStyle = .pageSheet #if !os(visionOS) - if #available(iOS 15.0, *) { - if let sheet = modal.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = false - } + if let sheet = modal.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = false } #endif viewController.present(modal, animated: true) diff --git a/Tests/SuperwallKitTests/Analytics/Attribution/AdServicesAttributionTests.swift b/Tests/SuperwallKitTests/Analytics/Attribution/AdServicesAttributionTests.swift index f5c82e735d..3f6bf7dc4a 100644 --- a/Tests/SuperwallKitTests/Analytics/Attribution/AdServicesAttributionTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Attribution/AdServicesAttributionTests.swift @@ -76,9 +76,6 @@ struct AdServicesAttributionTests { @Test func getAdServicesToken_noOpsWhenAlreadyPosted() async { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - return - } let dependencyContainer = DependencyContainer() let storage = dependencyContainer.storage! let poster = dependencyContainer.attributionPoster! @@ -119,9 +116,6 @@ struct AdServicesAttributionTests { @Test func getAdServicesToken_bailsWhenMaxAttemptsReached() async { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - return - } let dependencyContainer = DependencyContainer() let storage = dependencyContainer.storage! let poster = dependencyContainer.attributionPoster! @@ -148,9 +142,6 @@ struct AdServicesAttributionTests { @Test func getAdServicesToken_bailsWhenRetryWindowExpired() async { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - return - } let dependencyContainer = DependencyContainer() let storage = dependencyContainer.storage! let poster = dependencyContainer.attributionPoster! @@ -211,9 +202,6 @@ struct AdServicesAttributionTests { @Test func getAdServicesToken_bailsWhenPermanentlyUnsupported() async { - guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else { - return - } let dependencyContainer = DependencyContainer() let storage = dependencyContainer.storage! let poster = dependencyContainer.attributionPoster! diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/OccurrenceLogicTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/OccurrenceLogicTests.swift index 2b579fa81c..9b5508cb15 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/OccurrenceLogicTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/OccurrenceLogicTests.swift @@ -11,7 +11,6 @@ import Testing @testable import SuperwallKit -@available(iOS 14.0, *) class OccurrenceLogicTests { var coreDataManager: CoreDataManager! var coreDataStack: CoreDataStackMock! diff --git a/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/MockSkProduct.swift b/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/MockSkProduct.swift index b4cd718d66..eb6a54cdb1 100644 --- a/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/MockSkProduct.swift +++ b/Tests/SuperwallKitTests/Analytics/Trigger Session Manager/MockSkProduct.swift @@ -33,7 +33,6 @@ final class MockSkProduct: SKProduct { return internalPrice ?? 0 } - @available(iOS 12.0, *) override var subscriptionGroupIdentifier: String? { return internalSubscriptionGroupIdentifier ?? super.subscriptionGroupIdentifier } diff --git a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift index 4888d048c4..710b236052 100644 --- a/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift +++ b/Tests/SuperwallKitTests/Config/ConfigManagerTests.swift @@ -72,7 +72,6 @@ struct ConfigManagerTests { } @Test - @available(iOS 14.0, *) func configWithPrioritizedCampaignIdEncodedCorrectly() async { let dependencyContainer = DependencyContainer() diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift index ca8d47ce61..565db7e8de 100644 --- a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift @@ -8,7 +8,6 @@ import CoreData @testable import SuperwallKit -@available(iOS 14.0, *) final class CoreDataManagerMock: CoreDataManager { let internalDataStack: CoreDataStack diff --git a/Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK2Tests.swift b/Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK2Tests.swift index 1aa5c1452d..403c58a89b 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK2Tests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/ProductsFetcherSK2Tests.swift @@ -34,9 +34,6 @@ struct ProductsFetcherSK2Tests { @Test("ProductsFetcherSK2 should be sendable and thread-safe") func testSendableConformance() async throws { - guard #available(iOS 15.0, *) else { - return // Skip test on older iOS versions - } let fetcher = ProductsFetcherSK2(entitlementsInfo: entitlementsInfo) // Test that we can call the actor from multiple concurrent contexts @@ -56,9 +53,6 @@ struct ProductsFetcherSK2Tests { @Test("ProductsFetcherSK2 should throw error for non-existent products") func testErrorThrowingForNonExistentProducts() async throws { - guard #available(iOS 15.0, *) else { - return // Skip test on older iOS versions - } // Use 1 retry instead of 10 to make test run quickly let fetcher = ProductsFetcherSK2(entitlementsInfo: entitlementsInfo, numberOfRetries: 1) @@ -87,9 +81,6 @@ struct ProductsFetcherSK2Tests { @Test("ProductsFetcherSK2 should handle empty identifier set gracefully") func testEmptyIdentifiers() async throws { - guard #available(iOS 15.0, *) else { - return // Skip test on older iOS versions - } // Use 1 retry instead of 10 to make test run quickly let fetcher = ProductsFetcherSK2( diff --git a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift index be251bb328..741c1faa96 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift @@ -978,7 +978,6 @@ struct EntitlementProcessorTests { // - testRevokedNonConsumableDoesNotGrantLifetime() which tests revoked non-consumables don't grant lifetime access // - Integration tests with real StoreKit transactions that verify the full flow - @available(iOS 15.0, *) @Test("Process and enhance entitlements with subscription in grace period") func testProcessAndEnhanceEntitlements_gracePeriod() async { let transaction = createMockTransaction( @@ -1035,7 +1034,6 @@ struct EntitlementProcessorTests { #expect(subscriptions.first?.isInBillingRetryPeriod == false) } - @available(iOS 15.0, *) @Test("Process and enhance entitlements with subscription in billing retry") func testProcessAndEnhanceEntitlements_billingRetry() async { let transaction = createMockTransaction( @@ -1092,7 +1090,6 @@ struct EntitlementProcessorTests { #expect(subscriptions.first?.isInBillingRetryPeriod == true) } - @available(iOS 15.0, *) @Test("Process and enhance entitlements with revoked subscription") func testProcessAndEnhanceEntitlements_revoked() async { let transaction = createMockTransaction( @@ -1338,7 +1335,6 @@ struct EntitlementProcessorTests { // MARK: - Mock Subscription Status Provider -@available(iOS 15.0, *) struct MockSubscriptionStatusProvider: SubscriptionStatusProvider { var mockWillAutoRenew: Bool var mockState: LatestSubscription.State? diff --git a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/SK2ReceiptManagerTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/SK2ReceiptManagerTests.swift index bab3ff2f4e..fd8cd54ac3 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/SK2ReceiptManagerTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/SK2ReceiptManagerTests.swift @@ -28,9 +28,6 @@ struct SK2ReceiptManagerTests { @Test("isEligibleForIntroOffer re-queries StoreKit on every call and is never cached") func eligibilityIsNotCached() async { - guard #available(iOS 15.0, *) else { - return - } // Returns `true` on the first call and `false` afterwards, simulating Apple's // eligibility flipping once the user has consumed their intro offer. A cache // would freeze the first `true` and return it forever. @@ -53,9 +50,6 @@ struct SK2ReceiptManagerTests { @Test("loadIntroOfferEligibility is a no-op and does not freeze a value for later reads") func loadDoesNotFreezeEligibility() async { - guard #available(iOS 15.0, *) else { - return - } // Eligibility starts `true`, then becomes `false` (e.g. after the user starts a // trial). The old implementation called `isEligibleForIntroOffer` inside // `loadIntroOfferEligibility` and cached the result, so the later read returned a @@ -84,9 +78,6 @@ struct SK2ReceiptManagerTests { @Test("the default resolver runs the live StoreKit path end-to-end") func defaultResolverIsWired() async { - guard #available(iOS 15.0, *) else { - return - } // No custom resolver, so the production default `liveIntroOfferEligibility(for:)` // runs. A StoreKit 1-backed product isn't an `SK2StoreProduct`, so the live path // short-circuits to `false` without touching StoreKit. This proves the default diff --git a/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift index e92258ec6f..7ea2f401c3 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift @@ -17,7 +17,6 @@ struct SK2StoreProductCyclesTests { // `@available` can't go on the struct/tests (Swift Testing's `@Test` macro // rejects availability-annotated functions), so it lives on this helper and // each test guards with `#available` before calling it. - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) private func cycles( _ billingUnit: StoreKit.Product.SubscriptionPeriod.Unit, _ billingValue: Int, @@ -36,43 +35,36 @@ struct SK2StoreProductCyclesTests { @Test func monthlyOnYearly_is12() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 1, inCommitment: .year, 1) == 12) } @Test func biMonthlyOnYearly_is6() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 2, inCommitment: .year, 1) == 6) } @Test func quarterlyOnYearly_is4() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 3, inCommitment: .year, 1) == 4) } @Test func sixMonthOnYearly_is2() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 6, inCommitment: .year, 1) == 2) } @Test func monthlyOnSixMonthCommitment_is6() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 1, inCommitment: .month, 6) == 6) } @Test func biMonthlyOnSixMonthCommitment_is3() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 2, inCommitment: .month, 6) == 3) } @Test func yearlyOnTwoYearCommitment_is2() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.year, 1, inCommitment: .year, 2) == 2) } @@ -80,13 +72,11 @@ struct SK2StoreProductCyclesTests { @Test func dailyOnWeeklyCommitment_is7() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.day, 1, inCommitment: .week, 1) == 7) } @Test func weeklyOnFourWeekCommitment_is4() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.week, 1, inCommitment: .week, 4) == 4) } @@ -94,13 +84,11 @@ struct SK2StoreProductCyclesTests { @Test func yearlyUpFront_is1() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.year, 1, inCommitment: .year, 1) == 1) } @Test func monthlyUpFront_is1() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 1, inCommitment: .month, 1) == 1) } @@ -108,21 +96,18 @@ struct SK2StoreProductCyclesTests { @Test func fourWeeklyOnYearly_approximatesTo13() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } // week (day-family) vs year (month-family): 365 / 28 ≈ 13.04 → 13. #expect(cycles(.week, 4, inCommitment: .year, 1) == 13) } @Test func weeklyOnYearly_approximatesTo52() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } // 365 / 7 ≈ 52.14 → 52. #expect(cycles(.week, 1, inCommitment: .year, 1) == 52) } @Test func dailyOnMonthly_approximatesTo30() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } // 30 / 1 = 30 (month approximated as 30 days). #expect(cycles(.day, 1, inCommitment: .month, 1) == 30) } @@ -131,7 +116,6 @@ struct SK2StoreProductCyclesTests { @Test func zeroBillingValue_defaultsTo1() { - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { return } #expect(cycles(.month, 0, inCommitment: .year, 1) == 1) } } diff --git a/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift index f126755836..0becbef036 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift @@ -23,9 +23,6 @@ struct SK2PriceFormatRoundingTests { // The Decimal.FormatStyle APIs below are iOS 15+. SuperwallKit ships with // an iOS 13 minimum so we guard at runtime rather than on the method signature // (the @Test macro is incompatible with @available attributes). - guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) else { - return - } let gbp = Locale(identifier: "en_GB") let value: Decimal = 4.99 diff --git a/Tests/SuperwallKitTests/StoreKit/Purchase Controller/AutomaticPurchaseControllerTests.swift b/Tests/SuperwallKitTests/StoreKit/Purchase Controller/AutomaticPurchaseControllerTests.swift index e0e8c3a0c0..55f713924b 100644 --- a/Tests/SuperwallKitTests/StoreKit/Purchase Controller/AutomaticPurchaseControllerTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Purchase Controller/AutomaticPurchaseControllerTests.swift @@ -100,9 +100,6 @@ struct AutomaticPurchaseControllerTests { @Test("Full cold launch on weak internet keeps access for a Stripe subscriber") func testStripeSubscriberColdLaunchOnWeakInternet_keepsAccess() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) // Step 1: a previous session persisted `.active` for the Stripe subscriber. diff --git a/Tests/SuperwallKitTests/StoreKit/Transactions/Purchasing/ProductPurchaserSK1Tests.swift b/Tests/SuperwallKitTests/StoreKit/Transactions/Purchasing/ProductPurchaserSK1Tests.swift index d800cf6b58..78ac906483 100644 --- a/Tests/SuperwallKitTests/StoreKit/Transactions/Purchasing/ProductPurchaserSK1Tests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Transactions/Purchasing/ProductPurchaserSK1Tests.swift @@ -10,7 +10,6 @@ import XCTest @testable import SuperwallKit import StoreKit -@available(iOS 14.0, *) final class ProductPurchaserSK1Tests: XCTestCase { // TODO: Can't use this because the recording is done on a background priority thread. Github computer keeps failing this, despite it actually passing on the computer. /*func test_recordTransaction() async { diff --git a/Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift b/Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift index 592c48e869..fc073294b8 100644 --- a/Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift +++ b/Tests/SuperwallKitTests/Web/WebEntitlementRedeemerTests.swift @@ -35,9 +35,6 @@ struct WebEntitlementRedeemerTests { @Test("First redemption of code") func testRedeem_withCode_firstRedemption_savesCodeAndTracksEvents() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() let delegateAdapter = SuperwallDelegateAdapter() @@ -111,9 +108,6 @@ struct WebEntitlementRedeemerTests { @Test("didRedeemLink is called even when no paywall VC is presented") func testRedeem_noPaywallVC_callsDidRedeemLink() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) // Verify no paywall VC is presented @@ -188,9 +182,6 @@ struct WebEntitlementRedeemerTests { @Test("Redemption of existing code") func testRedeem_withCode_notFirstRedemption_savesCodeAndTracksEvents() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let existingCode = "TESTCODE" @@ -274,9 +265,6 @@ struct WebEntitlementRedeemerTests { @Test("Restores with paywall visible") func testRedeem_withCode_paywallVisible() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( receiptManager: dependencyContainer.receiptManager, @@ -543,9 +531,6 @@ struct WebEntitlementRedeemerTests { @Test("Revoked SUPERWALL entitlement is correctly removed") func testPollWebEntitlements_revokedSuperwallEntitlement() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) @@ -652,9 +637,6 @@ struct WebEntitlementRedeemerTests { @Test("Empty poll response does not clobber unexpired web entitlements") func testPollWebEntitlements_emptyResponse_keepsUnexpiredWebEntitlements() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) @@ -737,9 +719,6 @@ struct WebEntitlementRedeemerTests { @Test("Empty poll response removes expired web entitlements") func testPollWebEntitlements_emptyResponse_removesExpiredWebEntitlements() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) @@ -804,9 +783,6 @@ struct WebEntitlementRedeemerTests { @Test("External purchase controller with mixed web + appStore entitlements - polling removes web entitlements") func testPollWebEntitlements_externalPurchaseController_mixedEntitlements_webRemoved() async { - guard #available(iOS 14.0, *) else { - return - } // Create a simple external purchase controller (doesn't conform to InternalPurchaseController, so isInternal = false) class ExternalPurchaseController: PurchaseController { @@ -950,9 +926,6 @@ struct WebEntitlementRedeemerTests { @Test("External purchase controller with only web entitlements - polling removes all web entitlements") func testPollWebEntitlements_externalPurchaseController_onlyWebEntitlements_allRemoved() async { - guard #available(iOS 14.0, *) else { - return - } // Create a simple external purchase controller (doesn't conform to InternalPurchaseController, so isInternal = false) class ExternalPurchaseController: PurchaseController { @@ -1077,9 +1050,6 @@ struct WebEntitlementRedeemerTests { @Test("Concurrent redemptions blocked when paywall is open") func testRedeem_concurrent_paywallOpen_secondBlocked() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( @@ -1177,9 +1147,6 @@ struct WebEntitlementRedeemerTests { @Test("Concurrent redemptions allowed when no paywall is open") func testRedeem_concurrent_noPaywall_bothAllowed() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) @@ -1236,9 +1203,6 @@ struct WebEntitlementRedeemerTests { @Test("Schedules trial notification when redeemed product has free trial") func testRedeem_withFreeTrial_schedulesNotification() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( receiptManager: dependencyContainer.receiptManager, @@ -1423,9 +1387,6 @@ struct WebEntitlementRedeemerTests { @Test("Does not schedule trial notification when trialPeriodDays is 0") func testRedeem_withNoFreeTrial_doesNotScheduleNotification() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( receiptManager: dependencyContainer.receiptManager, @@ -1605,9 +1566,6 @@ struct WebEntitlementRedeemerTests { @Test("Does not schedule trial notification when isFreeTrialAvailable is false even with trialPeriodDays > 0") func testRedeem_withTrialDaysButNotEligible_doesNotScheduleNotification() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( receiptManager: dependencyContainer.receiptManager, @@ -1784,9 +1742,6 @@ struct WebEntitlementRedeemerTests { @Test("Does not schedule trial notification when isFreeTrialAvailable is true but trialPeriodDays is 0") func testRedeem_withEligibleButNoTrialDays_doesNotScheduleNotification() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( receiptManager: dependencyContainer.receiptManager, @@ -1963,9 +1918,6 @@ struct WebEntitlementRedeemerTests { @Test("ExistingCodes redemptions not blocked when paywall is open") func testRedeem_existingCodes_paywallOpen_notBlocked() async { - guard #available(iOS 14.0, *) else { - return - } let cache = dependencyContainer.paywallManager.cache let messageHandler = await PaywallMessageHandler( @@ -2054,9 +2006,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout submit persists pending context with default attempts") func testStripeCheckoutSubmit_persistsPendingState() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2085,9 +2034,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout submit replaces older pending context") func testStripeCheckoutSubmit_replacesPendingState() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2117,9 +2063,6 @@ struct WebEntitlementRedeemerTests { @Test("Paywall-open Stripe recovery loading shows only for non-expired pending context") func testStripePaywallOpenLoading_guardedByTimeout() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2157,9 +2100,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete polls immediately, invokes will/did callbacks, and clears pending on success") func testStripeCheckoutComplete_success_immediatePoll() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() @@ -2227,9 +2167,6 @@ struct WebEntitlementRedeemerTests { @Test("Legacy redeem keeps callback compatibility: willRedeemLink fires before /redeem request") func testLegacyRedeem_callbacksCompatibility() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() @@ -2290,9 +2227,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete retries no-redemption 5 times and keeps pending state") func testStripeCheckoutComplete_noRedemption_retriesAndKeepsPending() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2334,9 +2268,6 @@ struct WebEntitlementRedeemerTests { @Test("Foreground polling consumes attempts and clears pending after 5 tries") func testStripeForegroundPolling_consumesAttempts() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2380,9 +2311,6 @@ struct WebEntitlementRedeemerTests { @Test("Foreground Stripe recovery success uses fake callback timing and clears pending context") func testStripeForegroundPolling_success_fakeCallbacksAndClearsPending() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() @@ -2451,9 +2379,6 @@ struct WebEntitlementRedeemerTests { @Test("Foreground Stripe recovery no-redemption retries and consumes one attempt") func testStripeForegroundPolling_noRedemption_retriesAndConsumesAttempt() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2496,9 +2421,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete failed status clears pending state") func testStripeCheckoutComplete_failedStatus_clearsPendingState() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() @@ -2533,9 +2455,6 @@ struct WebEntitlementRedeemerTests { @Test("Foreground Stripe recovery failed status clears pending state") func testStripeForegroundPolling_failedStatus_clearsPendingState() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2564,9 +2483,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete status without codes clears pending state") func testStripeCheckoutComplete_completeStatusWithoutCodes_clearsPendingState() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2594,9 +2510,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout abandon tracks transaction_abandon and does not clear pending") func testStripeCheckoutAbandon_tracksAndKeepsPending() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockDelegate = MockSuperwallDelegate() @@ -2632,9 +2545,6 @@ struct WebEntitlementRedeemerTests { @Test("pollOrWaitForActiveStripePoll returns false when no pending state") func testPollOrWait_noPendingState_returnsFalse() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2668,9 +2578,6 @@ struct WebEntitlementRedeemerTests { @Test("pollOrWaitForActiveStripePoll starts own poll when no active poll") func testPollOrWait_noActivePoll_startsOwnPoll() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2702,9 +2609,6 @@ struct WebEntitlementRedeemerTests { @Test("pollOrWaitForActiveStripePoll returns true when waiting on active poll that redeems") func testPollOrWait_waitsForActivePollAndReturnsRedeemed() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2769,9 +2673,6 @@ struct WebEntitlementRedeemerTests { @Test("pollOrWaitForActiveStripePoll ignores redeemed result from different context") func testPollOrWait_waitsForActivePollDifferentContext_returnsFalse() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2835,9 +2736,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete preserves existing foreground attempts for same context") func testStripeCheckoutComplete_preservesExistingAttempts() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2902,9 +2800,6 @@ struct WebEntitlementRedeemerTests { @Test("Stripe checkout complete uses default attempts for new context") func testStripeCheckoutComplete_newContext_usesDefaultAttempts() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2945,9 +2840,6 @@ struct WebEntitlementRedeemerTests { @Test("Init doesn't start the cold-launch Stripe poll") func testInit_doesNotStartColdLaunchPoll() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) @@ -2984,9 +2876,6 @@ struct WebEntitlementRedeemerTests { @Test("Cold-launch kick-off polls a pending Stripe checkout") func testPollPendingStripeCheckoutOnColdLaunch_pollsPendingCheckout() async { - guard #available(iOS 14.0, *) else { - return - } let superwall = Superwall(dependencyContainer: dependencyContainer) let mockStorage = StorageMock(internalRedeemResponse: nil) From 8fc6fce3329363365a8bcaca4517bf39297636b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:38:42 +0200 Subject: [PATCH 03/11] Address review: tvOS floor, dead Catalyst camera code, visionOS CI Declare tvOS 15 so the unguarded StoreKit 2 declarations still compile for tvOS consumers, remove the unreachable `.unsupported` return and empty #if left in the camera permission handler, and build visionOS in CI through Package.swift (the xcodegen project is iOS-only). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-platforms.yml | 23 +++++++++++++++++++ CHANGELOG.md | 2 +- CLAUDE.md | 2 +- Package.swift | 1 + .../Handlers/PermissionHandler+Camera.swift | 8 ------- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-platforms.yml b/.github/workflows/build-platforms.yml index b2648023c6..9fa77eaa2b 100644 --- a/.github/workflows/build-platforms.yml +++ b/.github/workflows/build-platforms.yml @@ -5,6 +5,7 @@ on: paths: - '.github/workflows/build-platforms.yml' - 'project.yml' + - 'Package.swift' - '**/*.swift' - '!Examples/**' workflow_dispatch: @@ -29,3 +30,25 @@ jobs: - name: Build Mac Catalyst run: | xcodebuild -project SuperwallKit.xcodeproj -scheme SuperwallKit -destination 'generic/platform=macOS,variant=Mac Catalyst' build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" + + build-visionos: + runs-on: macos-26 + + steps: + - name: Git Checkout + uses: actions/checkout@v3 + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Install visionOS platform + # Only downloads when the runner image doesn't already ship the SDK. + run: | + xcodebuild -showsdks | grep -q xrsimulator || xcodebuild -downloadPlatform visionOS + - name: Remove Xcodeproj + # The xcodegen project only targets iOS; build through Package.swift instead. + run: | + rm -r SuperwallKit.xcodeproj + - name: Build visionOS + run: | + xcodebuild -scheme SuperwallKit -destination 'generic/platform=visionOS Simulator' build CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb30572a0..4007ea5fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ## 4.18.0 ### Breaking Changes -- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS and watchOS to macOS 12 and watchOS 8. +- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS, tvOS and watchOS to macOS 12, tvOS 15 and watchOS 8. - Requires Xcode 26 (Swift 6.2) or later to build the SDK. Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026. The SDK itself still compiles in the Swift 5 language mode. ## 4.17.0 diff --git a/CLAUDE.md b/CLAUDE.md index 493564428b..c7bb7baa9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ SuperwallKit is an iOS SDK for remote paywall configuration and A/B testing. The ### Minimum Toolchain and Platforms -- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12, watchOS 8. Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. +- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12, tvOS 15, watchOS 8. CI builds iOS (tests), Mac Catalyst and visionOS (`build-platforms.yml`). Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. - Don't add `#if compiler(...)` checks for anything below 6.2; only gate APIs newer than that (e.g. `compiler(>=6.3.2)` for iOS 26.4 StoreKit APIs). - Don't add `@available`/`#available` checks or fallback branches for iOS 15 or older (StoreKit 2, sheet detents, `UIMenu` buttons and friends are always there). - The package pins `swiftLanguageModes: [.v5]`. Moving to Swift 6 language mode is a separate change. diff --git a/Package.swift b/Package.swift index fe030e19d6..1d8df3089a 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let package = Package( platforms: [ .iOS(.v15), .macOS(.v12), + .tvOS(.v15), .watchOS(.v8) ], products: [ diff --git a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift index e931fbb397..5fe549534f 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift @@ -9,19 +9,11 @@ import AVFoundation extension PermissionHandler { func checkCameraPermission() -> PermissionStatus { - #if targetEnvironment(macCatalyst) return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus - return .unsupported - #else - return AVCaptureDevice.authorizationStatus(for: .video).toPermissionStatus - #endif } @MainActor func requestCameraPermission() async -> PermissionStatus { - #if targetEnvironment(macCatalyst) - #endif - guard hasPlistKey(PlistKey.camera) else { await showMissingPlistKeyAlert(for: PlistKey.camera, permissionName: "Camera") return .unsupported From 6e264daf976eef85cc9be0f96fd95aba3e90ee0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:43:37 +0200 Subject: [PATCH 04/11] Address Pullfrog review: iOS 13 location callback, stale comments Drop the pre-iOS 14 locationManager(_:didChangeAuthorization:) callback and its test (CLLocationManager only dispatches locationManagerDidChangeAuthorization(_:) on iOS 15+), and update the privacy-signature notes that listed it. Remove a test-only #if compiler(>=6.0) gate, refresh comments that still described iOS 13/14 or runtime #available guards, and explain in the changelog why the release is a minor and how iOS 13/14 apps can stay on 4.17.x. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../Attribution/AttributionFetcher.swift | 3 +- .../DebugPaywallPickerViewController.swift | 4 +-- .../SystemInfo+NotificationName.swift | 2 +- .../Location/LocationPermissionDelegate.swift | 29 +++++-------------- .../Purchasing/PurchasingCoordinator.swift | 2 +- .../PaywallMessageHandlerDelegateMock.swift | 7 ----- .../LocationPermissionDelegateTests.swift | 17 ----------- .../Products/SK2StoreProductCyclesTests.swift | 3 -- .../SK2PriceFormatRoundingTests.swift | 3 -- scripts/scan-privacy-signatures.sh | 7 ++--- 11 files changed, 17 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4007ea5fa0..9b4acfaa6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS, tvOS and watchOS to macOS 12, tvOS 15 and watchOS 8. - Requires Xcode 26 (Swift 6.2) or later to build the SDK. Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026. The SDK itself still compiles in the Swift 5 language mode. +- This ships as a minor release because every App Store Connect upload already needs Xcode 26, so any app that can ship an update already meets the new Xcode requirement. If your app still supports iOS 13 or 14, Swift Package Manager will fail to resolve this version for it: pin SuperwallKit to `4.17.x` (`.upToNextMinor(from: "4.17.0")`, or `~> 4.17.0` in CocoaPods) until you raise your deployment target. ## 4.17.0 ### Enhancements diff --git a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift index 96d5d0eaba..97b35ee878 100644 --- a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift +++ b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift @@ -321,8 +321,7 @@ extension AttributionFetcher { attributes["attStatus"] = String(attStatus) } - // Don't gate this on the ATT status. Before iOS 14.5 the IDFA is available - // while ATT still reads `notDetermined`, and `TrackingManagerProxy` returns + // Don't gate this on the ATT status. `TrackingManagerProxy` returns // `notDetermined` both for a genuine one and for a build where the class // can't be found, so the status can't tell those apart. The OS hands back // the all-zero id when it doesn't want to share one, and diff --git a/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift b/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift index 8b5cf07482..0d26cc5f02 100644 --- a/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugPaywallPickerViewController.swift @@ -140,8 +140,8 @@ extension DebugPaywallPickerViewController: UITableViewDataSource, UITableViewDe guard let header = view as? UITableViewHeaderFooterView else { return } - // A grouped header renders through its content configuration on iOS 14+, - // which ignores `textLabel` — the default grey is unreadable on the + // A grouped header renders through its content configuration, which + // ignores `textLabel` — the default grey is unreadable on the // debugger's near-black sheet. var configuration = header.defaultContentConfiguration() configuration.text = sections[section].title diff --git a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift index ee518aa1e0..aa8f41e3a3 100644 --- a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift +++ b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift @@ -35,7 +35,7 @@ enum SystemInfo { if #available(watchOS 9, *) { return WKApplication.didBecomeActiveNotification } else { - // Work around for "Symbol not found" dyld crashes on watchOS 7.0..<9.0 + // Work around for "Symbol not found" dyld crashes on watchOS 8.0..<9.0 return Notification.Name("WKApplicationDidBecomeActiveNotification") } #endif diff --git a/Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift b/Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift index 4867efc98b..1d9e21b13b 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift @@ -8,16 +8,15 @@ import Foundation /// Delegate class to handle location authorization callbacks. -/// Implements both iOS 14+ and iOS 13 delegate methods dynamically. /// -/// The two `@objc` methods below put CoreLocation's exact delegate selectors into +/// The `@objc` method below puts CoreLocation's exact delegate selector into /// the binary's Objective-C metadata — the section the proxies' mangling otherwise /// keeps Apple's names out of. That's accepted, not overlooked: `CLLocationManager` -/// dispatches its delegate callbacks by these selectors at runtime, so the metadata -/// must carry them for the callbacks to arrive. Removing them would mean assembling -/// this class at runtime with `objc_allocateClassPair`. They're also callback names, -/// not request-API names or usage-description keys — nothing scanners are known to -/// react to. `scan-privacy-signatures.sh` deliberately leaves them off its list. +/// dispatches its delegate callbacks by selector at runtime, so the metadata +/// must carry it for the callback to arrive. Removing it would mean assembling +/// this class at runtime with `objc_allocateClassPair`. It's also a callback name, +/// not a request-API name or usage-description key — nothing scanners are known to +/// react to. `scan-privacy-signatures.sh` deliberately leaves it off its list. final class LocationPermissionDelegate: NSObject { private let onStatusChange: (Int) -> Void private var hasCompleted = false @@ -27,27 +26,15 @@ final class LocationPermissionDelegate: NSObject { super.init() } - /// iOS 14+ delegate method /// Selector: locationManagerDidChangeAuthorization: @objc func locationManagerDidChangeAuthorization(_ manager: AnyObject) { let status = currentAuthorizationStatus(from: manager) completeIfDetermined(status) } - /// iOS 13 and earlier delegate method - /// Selector: locationManager:didChangeAuthorization: - #if !os(visionOS) - @objc func locationManager( - _ manager: AnyObject, - didChangeAuthorization status: Int - ) { - completeIfDetermined(status) - } - #endif - private func currentAuthorizationStatus(from manager: AnyObject) -> Int { - // Try instance property first (iOS 14+). The key is decoded at runtime so the - // name doesn't sit in the binary as a plaintext literal. + // The key is decoded at runtime so the name doesn't sit in the binary as a + // plaintext literal. let key = LocationManagerProxy.mangledAuthorizationStatusSelector.rot13() if let status = manager.value(forKey: key) as? Int { return status diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift index b605969434..e55b449f87 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift @@ -66,7 +66,7 @@ actor PurchasingCoordinator { } func getLatestSK2Transaction() async -> StoreTransaction? { - // If on iOS 15+, try and get latest transaction using SK2. + // Try and get the latest transaction using SK2. if let verificationResult = await Transaction.latest( for: productId, since: purchaseDate diff --git a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift index 23a247015b..1d2ae107e8 100644 --- a/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift +++ b/Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift @@ -26,17 +26,10 @@ final class FakeWebView: SWWebView { var willHandleJs = false var evaluatedScripts: [String] = [] - #if compiler(>=6.0) override func evaluateJavaScript(_ javaScriptString: String, completionHandler: (@MainActor (Any?, (any Error)?) -> Void)? = nil) { willHandleJs = true evaluatedScripts.append(javaScriptString) } - #else - override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, (any Error)?) -> Void)? = nil) { - willHandleJs = true - evaluatedScripts.append(javaScriptString) - } - #endif } final class PaywallMessageHandlerDelegateMock: PaywallMessageHandlerDelegate { diff --git a/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift b/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift index c41af94e60..a111b621f0 100644 --- a/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift +++ b/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift @@ -79,23 +79,6 @@ struct LocationPermissionDelegateTests { #expect(receivedStatus == FakeLocationAuthorizationStatus.authorizedAlways.rawValue) } - - #if !os(visionOS) - @Test func iOS13DelegateMethod_callsCallback() { - var receivedStatus: Int? - let delegate = LocationPermissionDelegate { status in - receivedStatus = status - } - - let mockManager = MockLocationManager() - delegate.locationManager( - mockManager, - didChangeAuthorization: FakeLocationAuthorizationStatus.authorizedWhenInUse.rawValue - ) - - #expect(receivedStatus == FakeLocationAuthorizationStatus.authorizedWhenInUse.rawValue) - } - #endif } // MARK: - Mock diff --git a/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift index 7ea2f401c3..fe267066d4 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift @@ -14,9 +14,6 @@ import Testing // swiftlint:disable all struct SK2StoreProductCyclesTests { - // `@available` can't go on the struct/tests (Swift Testing's `@Test` macro - // rejects availability-annotated functions), so it lives on this helper and - // each test guards with `#available` before calling it. private func cycles( _ billingUnit: StoreKit.Product.SubscriptionPeriod.Unit, _ billingValue: Int, diff --git a/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift index 0becbef036..e930a04837 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift @@ -20,9 +20,6 @@ struct SK2PriceFormatRoundingTests { /// by `SK2StoreProduct` for computed period prices preserves 4.99. @Test("priceFormatterForSK2 preserves fractional values that a rounded currency style would distort") func testNumberFormatterPreservesValueThatPriceFormatStyleWouldRound() throws { - // The Decimal.FormatStyle APIs below are iOS 15+. SuperwallKit ships with - // an iOS 13 minimum so we guard at runtime rather than on the method signature - // (the @Test macro is incompatible with @available attributes). let gbp = Locale(identifier: "en_GB") let value: Decimal = 4.99 diff --git a/scripts/scan-privacy-signatures.sh b/scripts/scan-privacy-signatures.sh index e15154a74e..c37352b89b 100755 --- a/scripts/scan-privacy-signatures.sh +++ b/scripts/scan-privacy-signatures.sh @@ -42,10 +42,9 @@ fi # them all. An entry must never legitimately appear in the scanned sections, so a hit # is always a real leak. # -# Deliberately absent: `LocationPermissionDelegate`'s callback selectors -# (locationManagerDidChangeAuthorization:, locationManager:didChangeAuthorization:). -# CLLocationManager dispatches those by selector at runtime, so their metadata has to -# exist for the callbacks to arrive. See the note on that class. +# Deliberately absent: `LocationPermissionDelegate`'s callback selector +# (locationManagerDidChangeAuthorization:). CLLocationManager dispatches it by +# selector at runtime, so its metadata has to exist for the callback to arrive. See the note on that class. # # Also absent: names from the camera, photos, and notification handlers. Those call # their frameworks directly rather than through mangled runtime lookups — e.g. From 9603288ad137b46f8e0a483b7b745487acac885b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:48:12 +0200 Subject: [PATCH 05/11] Drop Customer Center availability checks below the iOS 15 floor Also stop saying the SDK falls back to StoreKit 1 below iOS 15. Co-Authored-By: Claude Opus 5.5 --- .../Config/Options/SuperwallOptions.swift | 3 +- .../Actions/StoreKitTransactionLookup.swift | 1 - .../CustomerCenterManager.swift | 1 - .../Delegate/CustomerCenterDelegate.swift | 3 -- .../Logic/AppStoreVersionLookup.swift | 5 +-- ...stomerCenterConfiguration+Appearance.swift | 1 - .../SwiftUI/View+CustomerCenter.swift | 5 --- .../UIKit/CustomerCenterDelegateAdapter.swift | 1 - .../UIKit/CustomerCenterPushNavigator.swift | 3 -- .../UIKit/CustomerCenterViewController.swift | 2 -- .../CustomerCenterDependencies.swift | 11 ++----- .../CustomerCenterViewModel+Catalogue.swift | 1 - .../CustomerCenterViewModel+Dismissal.swift | 1 - ...stomerCenterViewModel+PurchaseDetail.swift | 1 - ...stomerCenterViewModel+PushedSurfaces.swift | 1 - .../CustomerCenterViewModel+Refund.swift | 1 - .../CustomerCenterViewModel+Support.swift | 1 - ...CustomerCenterViewModel+UpdateBanner.swift | 1 - .../ViewModel/CustomerCenterViewModel.swift | 1 - .../Views/AccountDetailsSection.swift | 1 - .../Views/AppUpdateWarningView.swift | 1 - .../Views/CustomerCenterEnvironment.swift | 4 --- .../Views/CustomerCenterLifecycleProbe.swift | 2 -- .../Views/CustomerCenterNavigator.swift | 6 ---- .../Views/CustomerCenterSheets.swift | 4 --- .../Views/CustomerCenterView.swift | 2 -- .../Views/DuplicateSubscriptionBanner.swift | 1 - .../Views/FeedbackSurveyView.swift | 1 - .../Views/ManagementScreenView.swift | 2 -- .../Views/NoPurchasesScreenView.swift | 1 - .../CustomerCenter/Views/PathsListView.swift | 1 - .../Views/PurchaseCardView.swift | 2 -- .../CustomerCenter/Views/RestoreOverlay.swift | 1 - .../Views/ShimmerModifier.swift | 2 -- .../Dependencies/DependencyContainer.swift | 7 ++--- .../Superwall+CustomerCenter.swift | 3 -- .../CustomerCenterManagerTests.swift | 4 --- .../Logic/AppStoreUpdateCheckTests.swift | 10 ------ .../Logic/CatalogueCacheTests.swift | 5 --- .../Logic/WebSubscriptionPathTests.swift | 10 ------ .../CustomerCenterConfigurationTests.swift | 1 - .../CustomerCenterViewControllerTests.swift | 9 ------ .../PurchaseDetailActionsTests.swift | 11 ------- .../CustomerCenterSheetOwnershipTests.swift | 31 ------------------- .../Views/CustomerCenterViewSmokeTests.swift | 5 --- .../Views/DesignReviewSnapshots.swift | 1 - .../CustomerCenter/Views/PathTitleTests.swift | 7 ----- 47 files changed, 6 insertions(+), 173 deletions(-) diff --git a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift index c91ecc56bf..cbd2207f85 100644 --- a/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift +++ b/Sources/SuperwallKit/Config/Options/SuperwallOptions.swift @@ -126,8 +126,7 @@ public final class SuperwallOptions: NSObject, Encodable { /// The StoreKit version that the SDK should use. /// - /// The SDK will use StoreKit 2 by default if the app is running on iOS 15+, otherwise it - /// will fallback to StoreKit 1. + /// Defaults to StoreKit 2. public var storeKitVersion: StoreKitVersion /// **WARNING**: The different network environments that the SDK should use. diff --git a/Sources/SuperwallKit/CustomerCenter/Actions/StoreKitTransactionLookup.swift b/Sources/SuperwallKit/CustomerCenter/Actions/StoreKitTransactionLookup.swift index 92bd62d593..0d31c9b9bf 100644 --- a/Sources/SuperwallKit/CustomerCenter/Actions/StoreKitTransactionLookup.swift +++ b/Sources/SuperwallKit/CustomerCenter/Actions/StoreKitTransactionLookup.swift @@ -13,7 +13,6 @@ protocol StoreKitTransactionLooking: Sendable { func isFamilyShared(productId: String) async -> Bool } -@available(iOS 15.0, *) struct StoreKitTransactionLookup: StoreKitTransactionLooking { func latestTransactionID(for productId: String) async -> UInt64? { guard case .verified(let transaction)? = await Transaction.latest(for: productId) else { diff --git a/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift b/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift index ffa5500daf..29c79c2e99 100644 --- a/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift +++ b/Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift @@ -9,7 +9,6 @@ import UIKit /// Builds the dependencies backing ``CustomerCenterView`` and owns the single Customer Center /// presentation for ``Superwall/presentCustomerCenter(configuration:from:delegate:onDismiss:)``. -@available(iOS 15.0, *) @MainActor final class CustomerCenterManager { private unowned let container: DependencyContainer diff --git a/Sources/SuperwallKit/CustomerCenter/Delegate/CustomerCenterDelegate.swift b/Sources/SuperwallKit/CustomerCenter/Delegate/CustomerCenterDelegate.swift index 4f89af0dd4..6e8cabd483 100644 --- a/Sources/SuperwallKit/CustomerCenter/Delegate/CustomerCenterDelegate.swift +++ b/Sources/SuperwallKit/CustomerCenter/Delegate/CustomerCenterDelegate.swift @@ -13,7 +13,6 @@ import Foundation /// The view controller does not retain its delegate. Keep a strong reference to it for the /// duration of the presentation — or present via `Superwall.shared.presentCustomerCenter(delegate:)`, /// which retains the delegate while the Customer Center is presented. -@available(iOS 15.0, *) @MainActor public protocol CustomerCenterDelegate: AnyObject { /// Called before purchases are restored. Return `false` to cancel, for example after the user @@ -31,7 +30,6 @@ public protocol CustomerCenterDelegate: AnyObject { func customerCenterDidDismiss() } -@available(iOS 15.0, *) public extension CustomerCenterDelegate { func customerCenterShouldRestorePurchases() async -> Bool { true } func customerCenterDidSelectAction(_ action: CustomerCenterAction, pathId: String, purchase: CustomerCenterPurchase?) {} @@ -45,7 +43,6 @@ public extension CustomerCenterDelegate { /// The view controller does not retain its delegate. Keep a strong reference to it for the /// duration of the presentation — or present via `Superwall.shared.presentCustomerCenter(delegate:)`, /// which retains the delegate while the Customer Center is presented. -@available(iOS 15.0, *) @objc(SWKCustomerCenterDelegate) @MainActor public protocol CustomerCenterDelegateObjc: AnyObject { diff --git a/Sources/SuperwallKit/CustomerCenter/Logic/AppStoreVersionLookup.swift b/Sources/SuperwallKit/CustomerCenter/Logic/AppStoreVersionLookup.swift index e17accc4b2..780c333458 100644 --- a/Sources/SuperwallKit/CustomerCenter/Logic/AppStoreVersionLookup.swift +++ b/Sources/SuperwallKit/CustomerCenter/Logic/AppStoreVersionLookup.swift @@ -73,10 +73,7 @@ struct AppStoreVersionLookup: CustomerCenterAppStoreVersionProviding { } static func currentStorefrontCountryCode() async -> String? { - if #available(iOS 15.0, *) { - return await Storefront.current?.countryCode - } - return nil + return await Storefront.current?.countryCode } /// The two-letter country to look the app up in: the App Store's, else the device region. diff --git a/Sources/SuperwallKit/CustomerCenter/Models/CustomerCenterConfiguration+Appearance.swift b/Sources/SuperwallKit/CustomerCenter/Models/CustomerCenterConfiguration+Appearance.swift index 4b84893557..9a00c63745 100644 --- a/Sources/SuperwallKit/CustomerCenter/Models/CustomerCenterConfiguration+Appearance.swift +++ b/Sources/SuperwallKit/CustomerCenter/Models/CustomerCenterConfiguration+Appearance.swift @@ -58,7 +58,6 @@ extension CustomerCenterConfiguration { /// Disfavored so that `.init(light: .red, dark: .blue)` keeps meaning `UIColor`, which also /// has a `.red`, rather than becoming ambiguous. @_disfavoredOverload - @available(iOS 14.0, *) @nonobjc public convenience init(light: Color, dark: Color) { self.init(light: UIColor(light), dark: UIColor(dark)) } diff --git a/Sources/SuperwallKit/CustomerCenter/SwiftUI/View+CustomerCenter.swift b/Sources/SuperwallKit/CustomerCenter/SwiftUI/View+CustomerCenter.swift index e39b722091..c893d68c40 100644 --- a/Sources/SuperwallKit/CustomerCenter/SwiftUI/View+CustomerCenter.swift +++ b/Sources/SuperwallKit/CustomerCenter/SwiftUI/View+CustomerCenter.swift @@ -7,18 +7,15 @@ import SwiftUI -@available(iOS 15.0, *) private struct CustomerCenterCallbacksKey: EnvironmentKey { static let defaultValue = CustomerCenterCallbacksBox() } /// Reference box so modifiers can accumulate callbacks down the view tree. -@available(iOS 15.0, *) final class CustomerCenterCallbacksBox { var callbacks = CustomerCenterCallbacks() } -@available(iOS 15.0, *) extension EnvironmentValues { var customerCenterCallbacks: CustomerCenterCallbacksBox { get { self[CustomerCenterCallbacksKey.self] } @@ -26,7 +23,6 @@ extension EnvironmentValues { } } -@available(iOS 15.0, *) public extension View { /// Presents the Customer Center as a sheet. /// @@ -85,7 +81,6 @@ public extension View { } } -@available(iOS 15.0, *) private struct CustomerCenterCallbackModifier: ViewModifier { let update: (inout CustomerCenterCallbacks) -> Void @Environment(\.customerCenterCallbacks) private var box diff --git a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterDelegateAdapter.swift b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterDelegateAdapter.swift index 091160b7a7..bca1573722 100644 --- a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterDelegateAdapter.swift +++ b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterDelegateAdapter.swift @@ -8,7 +8,6 @@ import Foundation /// An adapter between the internal SDK and the public swift/objective-c ``CustomerCenterDelegate``. -@available(iOS 15.0, *) struct CustomerCenterDelegateAdapter { // Weak so the view controller never retains its delegate — a host that both presents the // Customer Center and is its own delegate would otherwise cycle with the VC/view model that diff --git a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterPushNavigator.swift b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterPushNavigator.swift index 1ff6ffdf7b..dc3cd2ec9f 100644 --- a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterPushNavigator.swift +++ b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterPushNavigator.swift @@ -14,7 +14,6 @@ import UIKit /// ancestor for `NavigationLink` to use. Each destination becomes its own hosting controller, so /// the host's navigation bar drives it — their back button, their title treatment, their /// appearance — and nothing of theirs is modified. -@available(iOS 15.0, *) @MainActor final class CustomerCenterPushNavigator: CustomerCenterNavigating { weak var presenter: UIViewController? @@ -58,7 +57,6 @@ final class CustomerCenterPushNavigator: CustomerCenterNavigating { /// Applies the Customer Center's theme to a pushed screen, recomputing it whenever the colour /// scheme changes. A pushed destination is its own SwiftUI root, so it inherits nothing from the /// screen that pushed it. -@available(iOS 15.0, *) private struct CustomerCenterThemedContainer: View { let appearance: CustomerCenterConfiguration.Appearance let content: Content @@ -77,7 +75,6 @@ private struct CustomerCenterThemedContainer: View { /// A hosting controller that reports being popped, so the navigator can release its claim, and /// vetoes the dismissal debounce when it is merely covered. -@available(iOS 15.0, *) private final class CustomerCenterPushedHostingController: UIHostingController { var onRemovedFromParent: (() -> Void)? var onCoveredWhileStillInStack: (() -> Void)? diff --git a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift index b03fe0abad..bfe8de379d 100644 --- a/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift +++ b/Sources/SuperwallKit/CustomerCenter/UIKit/CustomerCenterViewController.swift @@ -33,7 +33,6 @@ public enum CustomerCenterPresentationStyle: Int, Sendable { /// /// Present it modally, or push it onto a navigation controller of your own with /// ``CustomerCenterPresentationStyle/embedded``. -@available(iOS 15.0, *) @objc(SWKCustomerCenterViewController) public final class CustomerCenterViewController: UIHostingController { let viewModel: CustomerCenterViewModel @@ -209,7 +208,6 @@ public final class CustomerCenterViewController: UIHostingController CustomerInfo { await Superwall.shared.getCustomerInfo() } func refreshReceipts() async -> CustomerInfo { @@ -119,7 +118,6 @@ final class LiveCustomerInfoProvider: CustomerCenterCustomerInfoProviding { } var customerInfoPublisher: AnyPublisher { Superwall.shared.$customerInfo.eraseToAnyPublisher() } } -@available(iOS 15.0, *) struct LiveProductsProvider: CustomerCenterProductsProviding { let container: DependencyContainer @@ -211,7 +209,6 @@ struct LiveProductsProvider: CustomerCenterProductsProviding { /// catalogue several times inside one visit, for a product list that changes on the dashboard's /// timescale rather than the customer's. The window is deliberately short: long enough to cover a /// single visit, short enough that a price edit shows up the next time anyone opens the screen. -@available(iOS 15.0, *) actor CatalogueCache { static let shared = CatalogueCache() static let ttl: TimeInterval = 5 * 60 @@ -259,7 +256,6 @@ actor CatalogueCache { } } -@available(iOS 15.0, *) struct LiveRestorer: CustomerCenterRestoring { func restorePurchases() async -> RestorationResult { await Superwall.shared.restorePurchases(presentsFailureAlert: false) @@ -270,11 +266,9 @@ struct LiveURLOpener: CustomerCenterURLOpening { func canOpen(_ url: URL) -> Bool { UIApplication.sharedApplication?.canOpenURL(url) ?? false } func open(_ url: URL) { UIApplication.sharedApplication?.open(url) } } -@available(iOS 15.0, *) struct LiveEventTracker: CustomerCenterEventTracking { func track(_ event: Trackable) async { _ = await Superwall.shared.track(event) } } -@available(iOS 15.0, *) struct LiveEnvironment: CustomerCenterEnvironmentProviding { let container: DependencyContainer let webManagementOverride: URL? @@ -306,7 +300,6 @@ struct LiveEnvironment: CustomerCenterEnvironmentProviding { } extension CustomerCenterDependencies { - @available(iOS 15.0, *) static func live(container: DependencyContainer, configuration: CustomerCenterConfiguration) -> CustomerCenterDependencies { CustomerCenterDependencies( customerInfo: LiveCustomerInfoProvider(), diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Catalogue.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Catalogue.swift index ac2b7d250d..7e2493a5e4 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Catalogue.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Catalogue.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Web product details -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// Products the Superwall catalogue may know but StoreKit never will: those bought outside the /// App Store. diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Dismissal.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Dismissal.swift index b908b99a50..32f9111a11 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Dismissal.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Dismissal.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Visibility-driven dismissal -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// Call from any Customer Center surface's `onAppear` — the root view, and any screen it pushes /// itself. Pushing a screen removes the previous surface from the hierarchy without the Customer diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PurchaseDetail.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PurchaseDetail.swift index 952d4fe736..4e9979bc05 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PurchaseDetail.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PurchaseDetail.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - What the purchase detail screen shows below the card -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// Why a detail screen has no actions, when it has none. Every subscription row opens its /// detail screen — that is the rule, and it holds whether or not there is anything left to do — diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PushedSurfaces.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PushedSurfaces.swift index 3847b02f50..5838079e84 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PushedSurfaces.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+PushedSurfaces.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Pushed screens -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// Records a screen the Customer Center has pushed. Claiming again under the same `id` changes /// nothing, so a screen can claim each time it appears. diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Refund.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Refund.swift index 3f30448494..5b28191332 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Refund.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Refund.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Refund sheet -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// Called by the refund sheet's completion. The product comes from when the sheet was opened, /// not from `sheet`: the sheet's binding clears `sheet` as it closes, which can happen before diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Support.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Support.swift index 4cf2a4169e..ce2d775b8e 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Support.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+Support.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Support email -@available(iOS 15.0, *) extension CustomerCenterViewModel { var supportMailtoURL: URL? { SupportEmailComposer.mailtoURL( diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+UpdateBanner.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+UpdateBanner.swift index 2ebdecac20..59fcf8f13b 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+UpdateBanner.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel+UpdateBanner.swift @@ -9,7 +9,6 @@ import Foundation // MARK: - Update banner -@available(iOS 15.0, *) extension CustomerCenterViewModel { /// The version the banner compares against: whatever the host configured, otherwise whatever the /// App Store lookup returned. A configured value always wins and suppresses the lookup entirely. diff --git a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel.swift b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel.swift index d207e28932..2dbb68728e 100644 --- a/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel.swift +++ b/Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterViewModel.swift @@ -9,7 +9,6 @@ import Combine import Foundation /// Drives the Customer Center UI: loads customer info and products, resolves paths, and performs actions. -@available(iOS 15.0, *) @MainActor final class CustomerCenterViewModel: ObservableObject { typealias PendingSurvey = (path: CustomerCenterConfiguration.Path, survey: CustomerCenterConfiguration.FeedbackSurvey) diff --git a/Sources/SuperwallKit/CustomerCenter/Views/AccountDetailsSection.swift b/Sources/SuperwallKit/CustomerCenter/Views/AccountDetailsSection.swift index cbfcefb021..9f98a40c28 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/AccountDetailsSection.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/AccountDetailsSection.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct AccountDetailsSection: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings diff --git a/Sources/SuperwallKit/CustomerCenter/Views/AppUpdateWarningView.swift b/Sources/SuperwallKit/CustomerCenter/Views/AppUpdateWarningView.swift index 23c0c61c19..41de5ad637 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/AppUpdateWarningView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/AppUpdateWarningView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct AppUpdateWarningView: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings diff --git a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterEnvironment.swift b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterEnvironment.swift index c6d96a214c..6c8c732433 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterEnvironment.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterEnvironment.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct CustomerCenterTheme { var accent: Color? @@ -35,15 +34,12 @@ extension UIColor { } } -@available(iOS 15.0, *) private struct CustomerCenterStringsKey: EnvironmentKey { static let defaultValue = CustomerCenterStrings.english } -@available(iOS 15.0, *) private struct CustomerCenterThemeKey: EnvironmentKey { static let defaultValue = CustomerCenterTheme(appearance: .init(), colorScheme: .light) } -@available(iOS 15.0, *) extension EnvironmentValues { var customerCenterStrings: CustomerCenterStrings { get { self[CustomerCenterStringsKey.self] } diff --git a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterLifecycleProbe.swift b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterLifecycleProbe.swift index b2fa17ae41..d5378e34d3 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterLifecycleProbe.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterLifecycleProbe.swift @@ -15,7 +15,6 @@ import UIKit /// so it keeps its claim on the sheets and mustn't deliver a dismissal. A screen that was popped /// or dismissed is gone. UIKit knows which happened and SwiftUI doesn't say, so a hidden child /// controller in the screen listens for it. -@available(iOS 15.0, *) struct CustomerCenterLifecycleProbe: UIViewControllerRepresentable { /// The screen disappeared but is still in the stack. var onCovered: () -> Void @@ -49,7 +48,6 @@ struct CustomerCenterLifecycleProbe: UIViewControllerRepresentable { } } -@available(iOS 15.0, *) final class CustomerCenterLifecycleProbeController: UIViewController { var onCovered: (() -> Void)? var onRemoved: (() -> Void)? diff --git a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterNavigator.swift b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterNavigator.swift index e1bfc9649c..8a053b4b29 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterNavigator.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterNavigator.swift @@ -14,23 +14,19 @@ import SwiftUI /// own stack there's no such ancestor, so the drill-downs have to be pushed through UIKit instead. /// Everywhere else — presented modally, or embedded in the host's own SwiftUI navigation — this is /// `nil` and `NavigationLink` does the work. -@available(iOS 15.0, *) @MainActor protocol CustomerCenterNavigating: AnyObject { func push(_ destination: Destination) } -@available(iOS 15.0, *) private struct CustomerCenterNavigatorKey: EnvironmentKey { static let defaultValue: CustomerCenterNavigating? = nil } -@available(iOS 15.0, *) private struct CustomerCenterSurfaceDepthKey: EnvironmentKey { static let defaultValue = 0 } -@available(iOS 15.0, *) extension EnvironmentValues { var customerCenterNavigator: CustomerCenterNavigating? { get { self[CustomerCenterNavigatorKey.self] } @@ -47,7 +43,6 @@ extension EnvironmentValues { /// A row that drills into another Customer Center screen, by whichever mechanism the surrounding /// navigation supports. -@available(iOS 15.0, *) struct CustomerCenterDrillDown: View { let viewModel: CustomerCenterViewModel @Environment(\.customerCenterNavigator) private var navigator @@ -98,7 +93,6 @@ struct CustomerCenterDrillDown: View { /// popped or dismissed, or SwiftUI takes it down. Being covered — by a deeper screen, a tab switch, /// anything the host presents — releases nothing, and neither does a swipe back the user cancels. /// `onDisappear` fires for a cover too, which is why it can't be the release. -@available(iOS 15.0, *) private struct CustomerCenterLinkedScreen: ViewModifier { let viewModel: CustomerCenterViewModel let surfaceDepth: Int diff --git a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterSheets.swift b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterSheets.swift index 45be42b53d..f25ea0799c 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterSheets.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterSheets.swift @@ -9,7 +9,6 @@ import SafariServices import StoreKit import SwiftUI -@available(iOS 15.0, *) extension View { /// - Parameter surfaceDepth: How deep this screen sits in the Customer Center's own pushed /// stack; `0` for the root. Passed rather than read from the environment because a modifier @@ -96,7 +95,6 @@ final class StoreKitSheetRenderRecord: ObservableObject { /// Internal rather than private so a test can drive the sheet bindings directly. The gate they /// apply has been wrong twice — once inert, once over-eager — and both times the bug was in the /// binding rather than in the rule it calls, which a test of the rule alone cannot catch. -@available(iOS 15.0, *) struct CustomerCenterSheetsModifier: ViewModifier { @ObservedObject var viewModel: CustomerCenterViewModel let surfaceDepth: Int @@ -241,7 +239,6 @@ struct CustomerCenterSheetsModifier: ViewModifier { } /// Internal so a test can check which of StoreKit's manage sheets a request goes to. -@available(iOS 15.0, *) struct ManageSubscriptionsSheet: ViewModifier { let isPresented: Binding /// Empty when the request has no subscription group. @@ -285,7 +282,6 @@ private extension Binding where Value == Bool { } } -@available(iOS 15.0, *) private struct ChangePlanSheet: View { let groupId: String? let productIds: [String]? diff --git a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterView.swift b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterView.swift index ed814304f5..f4780b3f12 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/CustomerCenterView.swift @@ -8,7 +8,6 @@ import SwiftUI /// Navigation behaviour of ``CustomerCenterView``. -@available(iOS 15.0, *) public struct CustomerCenterNavigationOptions { /// `.sheet` wraps the view in its own navigation; `.embedded` uses the navigation you place it in. public var style: CustomerCenterPresentationStyle @@ -40,7 +39,6 @@ public struct CustomerCenterNavigationOptions { } /// A self-service screen where users can view and manage their subscriptions and purchases. -@available(iOS 15.0, *) public struct CustomerCenterView: View { @StateObject private var viewModel: CustomerCenterViewModel private let navigationOptions: CustomerCenterNavigationOptions diff --git a/Sources/SuperwallKit/CustomerCenter/Views/DuplicateSubscriptionBanner.swift b/Sources/SuperwallKit/CustomerCenter/Views/DuplicateSubscriptionBanner.swift index 51bb86f61e..82a9c666bf 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/DuplicateSubscriptionBanner.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/DuplicateSubscriptionBanner.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct DuplicateSubscriptionBanner: View { @Environment(\.customerCenterStrings) private var strings var body: some View { diff --git a/Sources/SuperwallKit/CustomerCenter/Views/FeedbackSurveyView.swift b/Sources/SuperwallKit/CustomerCenter/Views/FeedbackSurveyView.swift index f87fbe674a..fc20ba2c50 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/FeedbackSurveyView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct FeedbackSurveyView: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings diff --git a/Sources/SuperwallKit/CustomerCenter/Views/ManagementScreenView.swift b/Sources/SuperwallKit/CustomerCenter/Views/ManagementScreenView.swift index 8ca2a4d41b..f9a18c2c1b 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/ManagementScreenView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/ManagementScreenView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct ManagementScreenView: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings @@ -66,7 +65,6 @@ struct ManagementScreenView: View { /// Detail for one subscription, reached by tapping its row on the management screen. Carries the /// actions that only make sense against that subscription. -@available(iOS 15.0, *) struct PurchaseDetailScreenView: View { @ObservedObject var viewModel: CustomerCenterViewModel let purchase: PurchasePresentation diff --git a/Sources/SuperwallKit/CustomerCenter/Views/NoPurchasesScreenView.swift b/Sources/SuperwallKit/CustomerCenter/Views/NoPurchasesScreenView.swift index 2d78b523e4..8082948038 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/NoPurchasesScreenView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/NoPurchasesScreenView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct NoPurchasesScreenView: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings diff --git a/Sources/SuperwallKit/CustomerCenter/Views/PathsListView.swift b/Sources/SuperwallKit/CustomerCenter/Views/PathsListView.swift index f54264af5e..6ebb5e2d11 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/PathsListView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/PathsListView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct PathsListView: View { @ObservedObject var viewModel: CustomerCenterViewModel let purchase: PurchasePresentation? diff --git a/Sources/SuperwallKit/CustomerCenter/Views/PurchaseCardView.swift b/Sources/SuperwallKit/CustomerCenter/Views/PurchaseCardView.swift index 770b374dd4..5c391783d1 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/PurchaseCardView.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/PurchaseCardView.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct PurchaseCardView: View { let purchase: PurchasePresentation let refundResult: (productId: String, status: CustomerCenterRefundStatus)? @@ -61,7 +60,6 @@ struct PurchaseCardView: View { } } -@available(iOS 15.0, *) struct BadgeView: View { let badge: PurchaseBadge var rowId: String? diff --git a/Sources/SuperwallKit/CustomerCenter/Views/RestoreOverlay.swift b/Sources/SuperwallKit/CustomerCenter/Views/RestoreOverlay.swift index 8c63e680c0..41fe555ca3 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/RestoreOverlay.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/RestoreOverlay.swift @@ -7,7 +7,6 @@ import SwiftUI -@available(iOS 15.0, *) struct RestoreOverlay: View { @ObservedObject var viewModel: CustomerCenterViewModel @Environment(\.customerCenterStrings) private var strings diff --git a/Sources/SuperwallKit/CustomerCenter/Views/ShimmerModifier.swift b/Sources/SuperwallKit/CustomerCenter/Views/ShimmerModifier.swift index a46c3a3911..a314ab00b1 100644 --- a/Sources/SuperwallKit/CustomerCenter/Views/ShimmerModifier.swift +++ b/Sources/SuperwallKit/CustomerCenter/Views/ShimmerModifier.swift @@ -10,7 +10,6 @@ import SwiftUI /// Sweeps a highlight across a placeholder so it reads as loading rather than as a grey bar. /// /// The highlight is masked to the placeholder's own shape, and it stays put under Reduce Motion. -@available(iOS 15.0, *) struct ShimmerModifier: ViewModifier { @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.colorScheme) private var colorScheme @@ -48,7 +47,6 @@ struct ShimmerModifier: ViewModifier { } } -@available(iOS 15.0, *) extension View { func shimmering() -> some View { modifier(ShimmerModifier()) diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index cd64a9c8c3..856249a6c2 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -48,9 +48,7 @@ final class DependencyContainer { // swiftlint:enable implicitly_unwrapped_optional let paywallArchiveManager = PaywallArchiveManager() - // `CustomerCenterManager` is `@available(iOS 15.0, *)`, and stored properties can't carry an - // availability attribute, so the typed accessor below is backed by an untyped `Any?`. - private var _customerCenterManager: Any? + private var _customerCenterManager: CustomerCenterManager? /// Builds the dependencies backing the Customer Center and owns its presentation state. /// @@ -59,10 +57,9 @@ final class DependencyContainer { /// main thread), while `CustomerCenterManager` is `@MainActor`. All production call sites /// (`Superwall.presentCustomerCenter`/`dismissCustomerCenter`/the Objective-C variant) are /// themselves `@MainActor`, so this accessor is only ever reached from the main actor. - @available(iOS 15.0, *) @MainActor var customerCenterManager: CustomerCenterManager { - if let manager = _customerCenterManager as? CustomerCenterManager { + if let manager = _customerCenterManager { return manager } let manager = CustomerCenterManager(container: self) diff --git a/Sources/SuperwallKit/Superwall+CustomerCenter.swift b/Sources/SuperwallKit/Superwall+CustomerCenter.swift index 2d32263811..5984e55319 100644 --- a/Sources/SuperwallKit/Superwall+CustomerCenter.swift +++ b/Sources/SuperwallKit/Superwall+CustomerCenter.swift @@ -22,7 +22,6 @@ extension Superwall { /// - delegate: Receives Customer Center events. Strongly retained for the duration of the /// presentation. /// - onDismiss: Called after the Customer Center is dismissed. - @available(iOS 15.0, *) @MainActor public func presentCustomerCenter( configuration: CustomerCenterConfiguration? = nil, @@ -50,7 +49,6 @@ extension Superwall { /// ``presentCustomerCenter(configuration:from:delegate:onDismiss:)``. A no-op if none is presented. /// /// Available to Objective-C as `dismissCustomerCenterWithCompletion:`. - @available(iOS 15.0, *) @MainActor @objc(dismissCustomerCenterWithCompletion:) public func dismissCustomerCenter(completion: (() -> Void)? = nil) { @@ -67,7 +65,6 @@ extension Superwall { /// Objective-C: presents the Customer Center. See /// ``presentCustomerCenter(configuration:from:delegate:onDismiss:)``. - @available(iOS 15.0, *) @available(swift, obsoleted: 1.0) @MainActor @objc(presentCustomerCenterWithConfiguration:from:delegate:onDismiss:) diff --git a/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift b/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift index 9eb9b065c0..03c1f21206 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/CustomerCenterManagerTests.swift @@ -13,7 +13,6 @@ import UIKit @Suite("CustomerCenterManager", .serialized) @MainActor struct CustomerCenterManagerTests { - @available(iOS 15.0, *) @Test("resolveConfiguration: override > options > default") func resolution() { let container = DependencyContainer() @@ -24,7 +23,6 @@ struct CustomerCenterManagerTests { #expect(manager.resolveConfiguration(custom) === custom) } - @available(iOS 15.0, *) @Test("second present while presented is ignored") func singleInstance() { let container = DependencyContainer() @@ -42,7 +40,6 @@ struct CustomerCenterManagerTests { window.isHidden = true } - @available(iOS 15.0, *) @Test("retains the delegate while presented, releases it after dismiss") func retainsDelegateForPresentationDuration() { final class ProbeDelegate: CustomerCenterDelegate {} @@ -87,7 +84,6 @@ struct CustomerCenterManagerTests { window.isHidden = true } - @available(iOS 15.0, *) @Test("viewDidDisappear fires didDismiss while the delegate is still retained") func viewDidDisappearFiresDelegateBeforeRelease() { final class ProbeDelegate: CustomerCenterDelegate { diff --git a/Tests/SuperwallKitTests/CustomerCenter/Logic/AppStoreUpdateCheckTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Logic/AppStoreUpdateCheckTests.swift index eb9da52937..2ca5663d5c 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Logic/AppStoreUpdateCheckTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Logic/AppStoreUpdateCheckTests.swift @@ -44,7 +44,6 @@ struct AppStoreUpdateCheckTests { // MARK: - The lookup drives the banner - @available(iOS 15.0, *) @Test("shows the banner when the App Store is ahead of the installed build") func showsBannerWhenStoreIsAhead() async { let (viewModel, provider) = makeViewModel(installed: "1.4.0", appStoreVersion: "1.5.0") @@ -53,7 +52,6 @@ struct AppStoreUpdateCheckTests { #expect(viewModel.showsUpdateBanner) } - @available(iOS 15.0, *) @Test("stays hidden when the installed build matches the App Store") func hiddenWhenUpToDate() async { let (viewModel, _) = makeViewModel(installed: "1.5.0", appStoreVersion: "1.5.0") @@ -63,7 +61,6 @@ struct AppStoreUpdateCheckTests { /// The case that rules out an `installed != latest` comparison: a build ahead of the store is /// normal for testers, and telling them to "update" would send them backwards. - @available(iOS 15.0, *) @Test("stays hidden when the installed build is ahead of the App Store") func hiddenWhenAheadOfStore() async { let (viewModel, _) = makeViewModel(installed: "2.0.0", appStoreVersion: "1.9.3") @@ -73,7 +70,6 @@ struct AppStoreUpdateCheckTests { /// Calendar versioning is still a monotonically increasing numeric tuple, so ordered comparison /// works on it exactly as it does on semantic versions. - @available(iOS 15.0, *) @Test("orders calendar versions correctly", arguments: [ ("2026.2.9", "2026.3.1", true), ("2026.3.1", "2026.2.9", false), @@ -87,7 +83,6 @@ struct AppStoreUpdateCheckTests { // MARK: - When the lookup must not run - @available(iOS 15.0, *) @Test("never looks the version up on TestFlight, sandbox or simulator builds") func skipsLookupInSandbox() async { let (viewModel, provider) = makeViewModel( @@ -100,7 +95,6 @@ struct AppStoreUpdateCheckTests { #expect(!viewModel.showsUpdateBanner) } - @available(iOS 15.0, *) @Test("a configured version wins and suppresses the lookup") func configuredVersionWins() async { let (viewModel, provider) = makeViewModel( @@ -113,7 +107,6 @@ struct AppStoreUpdateCheckTests { #expect(!viewModel.showsUpdateBanner, "the configured version says we're current") } - @available(iOS 15.0, *) @Test("opting out skips the lookup") func optOutSkipsLookup() async { let (viewModel, provider) = makeViewModel( @@ -126,7 +119,6 @@ struct AppStoreUpdateCheckTests { #expect(!viewModel.showsUpdateBanner) } - @available(iOS 15.0, *) @Test("warnsAboutUpdates off skips the lookup entirely") func warningOffSkipsLookup() async { let (viewModel, provider) = makeViewModel( @@ -139,7 +131,6 @@ struct AppStoreUpdateCheckTests { #expect(!viewModel.showsUpdateBanner) } - @available(iOS 15.0, *) @Test("a failed lookup hides the banner rather than guessing") func failedLookupHidesBanner() async { let (viewModel, provider) = makeViewModel(installed: "1.4.0", appStoreVersion: nil) @@ -148,7 +139,6 @@ struct AppStoreUpdateCheckTests { #expect(!viewModel.showsUpdateBanner) } - @available(iOS 15.0, *) @Test("the lookup runs once per presentation, not once per reload") func lookupIsNotRepeated() async { let (viewModel, provider) = makeViewModel(installed: "1.4.0", appStoreVersion: "1.5.0") diff --git a/Tests/SuperwallKitTests/CustomerCenter/Logic/CatalogueCacheTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Logic/CatalogueCacheTests.swift index 471e840794..3037532544 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Logic/CatalogueCacheTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Logic/CatalogueCacheTests.swift @@ -19,7 +19,6 @@ struct CatalogueCacheTests { /// The reason this exists: `apply(customerInfo:refetchProducts:)` runs on load, on every restore /// and after every sheet dismissal, and all four call sites refetch. - @available(iOS 15.0, *) @Test("a second visit inside the window doesn't refetch") func cachesWithinTheWindow() async throws { var clock = Date(timeIntervalSince1970: 1_000_000) @@ -33,7 +32,6 @@ struct CatalogueCacheTests { #expect(fetches == 1) } - @available(iOS 15.0, *) @Test("the cache expires, so a price edit shows up") func expiresAfterTheWindow() async throws { var clock = Date(timeIntervalSince1970: 1_000_000) @@ -49,7 +47,6 @@ struct CatalogueCacheTests { /// A failed load must not be remembered: the cards render without prices and the next `apply` /// should try again, rather than serving the failure for five minutes. - @available(iOS 15.0, *) @Test("a failure isn't cached") func doesNotCacheFailures() async throws { struct Boom: Error {} @@ -67,7 +64,6 @@ struct CatalogueCacheTests { /// An actor is reentrant across `await`. Without an in-flight handoff, two callers landing on a /// cold cache both pass the freshness check and both fetch — on the Customer Center that is /// `load()` and the customer-info publisher calling `apply` at nearly the same moment. - @available(iOS 15.0, *) @Test("callers overlapping on a cold cache share one fetch") func overlappingCallersShareOneFetch() async throws { let cache = CatalogueCache() @@ -87,7 +83,6 @@ struct CatalogueCacheTests { #expect(await fetches.value == 1) } - @available(iOS 15.0, *) @Test("a fresh catalogue can be read without fetching, and only while fresh") func freshResponseReadsWithoutFetching() async throws { var clock = Date(timeIntervalSince1970: 1_000_000) diff --git a/Tests/SuperwallKitTests/CustomerCenter/Logic/WebSubscriptionPathTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Logic/WebSubscriptionPathTests.swift index 206e61bf73..22091caebd 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Logic/WebSubscriptionPathTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Logic/WebSubscriptionPathTests.swift @@ -71,7 +71,6 @@ struct WebSubscriptionPathTests { // MARK: - Only one row, and it goes to the management page - @available(iOS 15.0, *) @Test("a web subscriber gets the management row and nothing App Store-only") func webSubscriberSeesOneManagementRow() async { let viewModel = await makeViewModel(webManagementURL: managementURL) @@ -84,7 +83,6 @@ struct WebSubscriptionPathTests { #expect(managePath(viewModel)?.destination == .webManage(managementURL)) } - @available(iOS 15.0, *) @Test("the management row survives a missing management URL", arguments: [ ProductStore.stripe, .paddle, .superwall ]) @@ -94,7 +92,6 @@ struct WebSubscriptionPathTests { #expect(managePath(viewModel)?.destination == .webManageUnavailable) } - @available(iOS 15.0, *) @Test("tapping the row without a URL explains where to find the link") func unavailableRowShowsTheBlurb() async { let viewModel = await makeViewModel(webManagementURL: nil) @@ -105,7 +102,6 @@ struct WebSubscriptionPathTests { #expect(viewModel.sheet == .webManageUnavailable) } - @available(iOS 15.0, *) @Test("tapping the row with a URL opens the management page") func availableRowOpensTheManagementPage() async { let viewModel = await makeViewModel(webManagementURL: managementURL) @@ -141,7 +137,6 @@ struct WebSubscriptionPathTests { /// `Entitlement(id:)` is no help here: the public convenience initializer hardcodes /// `store: .appStore`, so the purchase never reaches the web branch and the case passes on the /// App Store branch's `guard let sub` instead — green with this rule deleted. - @available(iOS 15.0, *) @Test("a comped entitlement isn't told to check a receipt it never had") func compedEntitlementGetsNoReceiptBlurb() async { let viewModel = await makeEntitlementOnlyViewModel(store: nil) @@ -155,7 +150,6 @@ struct WebSubscriptionPathTests { /// The other half of that rule, and the reason it keys on the store rather than the kind: a web /// purchase arrives as a bare entitlement whenever the backend sends no matching transaction. /// Those customers are paying, and the row telling them where the link is has to survive. - @available(iOS 15.0, *) @Test("a web purchase with no transaction behind it keeps its management row", arguments: [ EntitlementStore.stripe, .paddle ]) @@ -172,7 +166,6 @@ struct WebSubscriptionPathTests { /// alone, so anything that merely *came from* a web store was offered a management row — a /// single Stripe charge with no subscription behind it, and a subscription that had already /// lapsed or been revoked. - @available(iOS 15.0, *) @Test("nothing left to manage means no management row") func nothingToManageMeansNoRow() async { let lapsed = webSubscription(isActive: false, isRevoked: false) @@ -189,7 +182,6 @@ struct WebSubscriptionPathTests { } } - @available(iOS 15.0, *) @Test("a one-off web purchase has no subscription to manage") func oneOffWebPurchaseHasNoRow() async { let purchase = NonSubscriptionTransaction( @@ -221,7 +213,6 @@ struct WebSubscriptionPathTests { /// The survey gates an action. On a web flow that action leaves the app — or, with no URL, can't /// happen at all — so asking the question here collects an answer for something we never see /// the outcome of. - @available(iOS 15.0, *) @Test("no survey is shown before handing off to the web", arguments: [true, false]) func webFlowsSkipTheSurvey(hasManagementURL: Bool) async { let survey = CustomerCenterConfiguration.FeedbackSurvey( @@ -246,7 +237,6 @@ struct WebSubscriptionPathTests { // MARK: - Labelling - @available(iOS 15.0, *) @Test("web management destinations are labelled as managing, not cancelling") func webDestinationsAreLabelledAsManagement() { #expect(ResolvedPathDestination.webManage(managementURL).isWebManagement) diff --git a/Tests/SuperwallKitTests/CustomerCenter/Models/CustomerCenterConfigurationTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Models/CustomerCenterConfigurationTests.swift index 6f40bce85b..4092f9fdb1 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Models/CustomerCenterConfigurationTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Models/CustomerCenterConfigurationTests.swift @@ -181,7 +181,6 @@ struct CustomerCenterConfigurationTests { } @Test("navigation options show a close button only for a sheet, unless told otherwise") - @available(iOS 15.0, *) func navigationOptionDefaults() { #expect(CustomerCenterNavigationOptions().showsCloseButton) #expect(!CustomerCenterNavigationOptions(style: .embedded).showsCloseButton) diff --git a/Tests/SuperwallKitTests/CustomerCenter/UIKit/CustomerCenterViewControllerTests.swift b/Tests/SuperwallKitTests/CustomerCenter/UIKit/CustomerCenterViewControllerTests.swift index 9d6ce9a48f..06181aed79 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/UIKit/CustomerCenterViewControllerTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/UIKit/CustomerCenterViewControllerTests.swift @@ -20,7 +20,6 @@ struct CustomerCenterViewControllerTests { func customerCenterDidDismiss() { didDismissCount += 1 } } - @available(iOS 15.0, *) private func makeController( style: CustomerCenterPresentationStyle, delegate: CustomerCenterDelegate?, @@ -67,7 +66,6 @@ struct CustomerCenterViewControllerTests { /// `presentingViewController == nil` for its entire lifetime, so the previous teardown check /// treated every cover event — a push on top, a tab switch — as the Customer Center closing. /// `dismiss()` latches, so that also permanently silenced the real teardown. - @available(iOS 15.0, *) @Test("pushed: being covered on the host's stack does not fire the dismissal") func pushedCoveredDoesNotDismiss() { let delegate = ProbeDelegate() @@ -97,7 +95,6 @@ struct CustomerCenterViewControllerTests { /// arms a debounced dismissal from SwiftUI's `onDisappear`, which a `UIHostingController` /// forwards on a cover just as it does on a teardown — so without a veto the dismissal simply /// arrives late, and the latch then silences the genuine pop. This waits past the debounce. - @available(iOS 15.0, *) @Test("pushed: a cover does not fire a late dismissal, and the real pop still does") func pushedCoverDoesNotFireLateDismissal() async { let debounce: TimeInterval = 0.2 @@ -137,7 +134,6 @@ struct CustomerCenterViewControllerTests { /// runloop turn after `viewDidDisappear` returns — arming the debounce with nothing left to /// cancel it. A veto that only drops an already-armed dismissal passes the test above and fires /// the premature `didDismiss` here, latching and silencing the genuine teardown afterwards. - @available(iOS 15.0, *) @Test("pushed: a cover survives SwiftUI reporting the disappearance late") func pushedCoverSurvivesALateDisappearance() async { let debounce: TimeInterval = 0.2 @@ -168,7 +164,6 @@ struct CustomerCenterViewControllerTests { window.isHidden = true } - @available(iOS 15.0, *) @Test("pushed: being popped off the host's stack fires the dismissal exactly once") func pushedPopFiresDismissal() { let delegate = ProbeDelegate() @@ -191,7 +186,6 @@ struct CustomerCenterViewControllerTests { window.isHidden = true } - @available(iOS 15.0, *) @Test("modal: dismissing fires the dismissal exactly once") func modalDismissFiresDismissal() { let delegate = ProbeDelegate() @@ -215,7 +209,6 @@ struct CustomerCenterViewControllerTests { /// A controller that was never presented and is not being removed is not a teardown. This is the /// case the old `presentingViewController == nil` check got wrong, since it is indistinguishable /// from a pushed controller sitting on a back stack. - @available(iOS 15.0, *) @Test("a controller that was never presented does not report a dismissal") func neverPresentedDoesNotDismiss() { let delegate = ProbeDelegate() @@ -233,7 +226,6 @@ struct CustomerCenterViewControllerTests { /// The Customer Center used to hide a host's navigation bar in `.embedded` and hand it back on /// the way out. It no longer touches the bar in any style — the host's chrome is theirs. - @available(iOS 15.0, *) @Test("neither style modifies the host's navigation bar", arguments: [ CustomerCenterPresentationStyle.embedded, .sheet ]) @@ -261,7 +253,6 @@ struct CustomerCenterViewControllerTests { // MARK: - Analytics - @available(iOS 15.0, *) @Test("presentation style is reported on Customer Center events") func reportsPresentationMode() { #expect(makeController(style: .sheet, delegate: nil).viewModel.presentationMode == .sheet) diff --git a/Tests/SuperwallKitTests/CustomerCenter/ViewModel/PurchaseDetailActionsTests.swift b/Tests/SuperwallKitTests/CustomerCenter/ViewModel/PurchaseDetailActionsTests.swift index 27ca71b6bb..ef0c8f39f0 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/ViewModel/PurchaseDetailActionsTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/ViewModel/PurchaseDetailActionsTests.swift @@ -40,7 +40,6 @@ struct PurchaseDetailActionsTests { ) } - @available(iOS 15.0, *) private func makeViewModel( subscriptions: [SubscriptionTransaction] = [], entitlements: [Entitlement] = [] @@ -57,7 +56,6 @@ struct PurchaseDetailActionsTests { return viewModel } - @available(iOS 15.0, *) private func emptyState(_ viewModel: CustomerCenterViewModel) throws -> EmptyState? { let purchase = try #require(viewModel.purchases.first, "the row exists — nothing is hidden") return viewModel.detailEmptyState(for: purchase) @@ -65,21 +63,18 @@ struct PurchaseDetailActionsTests { // MARK: Genuinely nothing to do - @available(iOS 15.0, *) @Test("a revoked App Store subscription: nothing to do") func revokedAppStore() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(isRevoked: true)]) #expect(try emptyState(viewModel) == .nothingToDo) } - @available(iOS 15.0, *) @Test("a lapsed web subscription: nothing to do") func lapsedWeb() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(store: .stripe, isActive: false)]) #expect(try emptyState(viewModel) == .nothingToDo) } - @available(iOS 15.0, *) @Test("a comped grant with no management page: nothing to do") func compedGrant() async throws { let viewModel = await makeViewModel(entitlements: [Entitlement(id: "pro", store: nil)]) @@ -89,7 +84,6 @@ struct PurchaseDetailActionsTests { /// A lifetime grant from a store this SDK can't drive is not "managed elsewhere": nothing /// renews, so there is nothing to manage anywhere, and the card's "Lifetime" badge would sit /// over a sentence about a subscription. Liveness alone isn't the test; renewal is. - @available(iOS 15.0, *) @Test("a lifetime Play Store grant: nothing to do, not a subscription to manage elsewhere") func lifetimePlayStoreGrant() async throws { let lifetime = Entitlement(id: "pro", isActive: true, store: .playStore, isLifetime: true) @@ -104,14 +98,12 @@ struct PurchaseDetailActionsTests { /// The shape the last review caught: an active Play Store subscription on an iOS client. Its /// card reads "Active — renews on …", so "nothing to manage" would be a lie. The store has a /// name, and the sentence uses it. - @available(iOS 15.0, *) @Test("a live Play Store subscription is managed elsewhere, by name") func livePlayStore() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(store: .playStore)]) #expect(try emptyState(viewModel) == .managedElsewhere(storeLabelKey: "customer_center_store_google_play")) } - @available(iOS 15.0, *) @Test("a live subscription from an unnamed store is managed elsewhere, generically") func liveOtherStore() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(store: .other)]) @@ -120,7 +112,6 @@ struct PurchaseDetailActionsTests { /// A lapsed Play Store subscription is not "managed elsewhere" — there is nothing left to /// manage anywhere. Liveness, not store, decides between the two sentences. - @available(iOS 15.0, *) @Test("a lapsed Play Store subscription: nothing to do") func lapsedPlayStore() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(store: .playStore, isActive: false)]) @@ -129,7 +120,6 @@ struct PurchaseDetailActionsTests { // MARK: Has actions, so no explanation at all - @available(iOS 15.0, *) @Test("an active App Store subscription has actions") func activeAppStore() async throws { let viewModel = await makeViewModel(subscriptions: [subscription()]) @@ -138,7 +128,6 @@ struct PurchaseDetailActionsTests { /// A live web subscription with no management URL still gets the row that explains where the /// link is — fixed once already, and this keeps it from being read as an empty state. - @available(iOS 15.0, *) @Test("an active web subscription has the management row even with no URL") func activeWeb() async throws { let viewModel = await makeViewModel(subscriptions: [subscription(store: .stripe)]) diff --git a/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterSheetOwnershipTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterSheetOwnershipTests.swift index d012d0fcc4..7276f973e3 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterSheetOwnershipTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterSheetOwnershipTests.swift @@ -27,7 +27,6 @@ struct CustomerCenterSheetOwnershipTests { func customerCenterDidDismiss() { didDismissCount += 1 } } - @available(iOS 15.0, *) private func makeViewModel( delegate: CustomerCenterDelegate? = nil, dismissDebounceInterval: TimeInterval = 0.6 @@ -155,7 +154,6 @@ struct CustomerCenterSheetOwnershipTests { // MARK: - Which screen presents a sheet - @available(iOS 15.0, *) @Test("a sheet is presented by the screen on top when it's requested") func sheetGoesToTheScreenOnTop() { let viewModel = makeViewModel() @@ -174,7 +172,6 @@ struct CustomerCenterSheetOwnershipTests { /// its transaction. The request lands mid-pop, so the outgoing detail presents it. When the pop /// finished, a sheet that followed the depth was presented again by the root: a second refund /// request for one tap. - @available(iOS 15.0, *) @Test("a sheet whose screen is popped isn't presented again by the root") func poppedScreensSheetStaysWithIt() { let viewModel = makeViewModel() @@ -203,13 +200,11 @@ struct CustomerCenterSheetOwnershipTests { /// Whether `modifier` would present the refund sheet, once StoreKit has been rendered with /// `transactionId`. - @available(iOS 15.0, *) private func presentsRefund(_ modifier: CustomerCenterSheetsModifier, rendered transactionId: UInt64) -> Bool { let rendered = StoreKitSheetParameters(refundTransactionId: transactionId) return modifier.storeKitSheetBinding(.refund, rendered: rendered).wrappedValue } - @available(iOS 15.0, *) @Test("a sheet stays with its screen until that screen leaves") func sheetOutlivesUnrelatedDepartures() { let viewModel = makeViewModel() @@ -230,7 +225,6 @@ struct CustomerCenterSheetOwnershipTests { /// Claims are compared by identity, so which screen claims matters: the owner claiming again as /// it reappears keeps its sheet, while a different screen taking its depth has replaced it. - @available(iOS 15.0, *) @Test("a sheet survives its screen claiming again, not another screen taking its place") func reclaimKeepsTheSheetReplacementDropsIt() { let viewModel = makeViewModel() @@ -248,7 +242,6 @@ struct CustomerCenterSheetOwnershipTests { /// The other half of the race: the request that went with its screen doesn't linger in `sheet`, /// where nothing could present it, and neither does the survey answer it was waiting for. - @available(iOS 15.0, *) @Test("a request whose screen leaves is dropped along with its pending survey") func abandonedSurveyRequestIsCleared() async throws { let viewModel = await makeLoadedViewModel() @@ -270,7 +263,6 @@ struct CustomerCenterSheetOwnershipTests { /// A refund is the one request StoreKit may already be showing when its screen goes, and the /// outcome of that sheet still has to reach the host. - @available(iOS 15.0, *) @Test("a dropped refund request still reports the outcome StoreKit delivers") func abandonedRefundStillReportsItsOutcome() async throws { let lookup = StoreKitTransactionLookupMock() @@ -327,7 +319,6 @@ struct CustomerCenterSheetOwnershipTests { /// The two above prove the rule; this proves the binding applies it. Testing only the rule /// would pass just as happily against the unguarded setter that made it necessary. - @available(iOS 15.0, *) @Test("a stale dismissal write does not tear down another surface's sheet") func staleDismissalLeavesTheOpenSheetAlone() async { let viewModel = makeViewModel() @@ -353,7 +344,6 @@ struct CustomerCenterSheetOwnershipTests { /// StoreKit writes `false` into its sheets' `isPresented` bindings from the background thread /// their presentation finishes on. The view model is main-actor state SwiftUI observes, so the /// write has to reach it on the main thread — and still clear the sheet when it gets there. - @available(iOS 15.0, *) @Test("a dismissal StoreKit writes off the main thread updates the view model on it", arguments: [ CustomerCenterSheetOwnership.SheetKind.manageSubscriptions, .refund @@ -395,7 +385,6 @@ struct CustomerCenterSheetOwnershipTests { /// Both parameters come from `sheet`, so presenting in the render they change in hands StoreKit /// the previous value: transaction 0, a refund request that can only fail, or no subscription /// group, a manage sheet with no subscriptions on it. - @available(iOS 15.0, *) @Test("a StoreKit sheet waits until its parameter has been rendered", arguments: [ CustomerCenterSheetOwnership.SheetKind.refund, .manageSubscriptions @@ -422,7 +411,6 @@ struct CustomerCenterSheetOwnershipTests { /// Each StoreKit sheet is gated on its own kind of request as well as on what has rendered. With /// the gate open for one, the other must still stay down. - @available(iOS 15.0, *) @Test("a StoreKit sheet presents only its own kind of request") func storeKitSheetsPresentOnlyTheirOwnKind() { let viewModel = makeViewModel() @@ -440,7 +428,6 @@ struct CustomerCenterSheetOwnershipTests { } /// With no group to hand over there's nothing to wait for. - @available(iOS 15.0, *) @Test("a manage sheet without a subscription group presents at once") func manageSheetWithoutAGroupPresentsAtOnce() { let viewModel = makeViewModel() @@ -453,7 +440,6 @@ struct CustomerCenterSheetOwnershipTests { /// From iOS 17, StoreKit's manage sheet for a subscription group, handed an empty group, says the /// customer has no subscriptions. A request that has no group goes to the plain manage sheet. - @available(iOS 15.0, *) @Test("a manage request without a subscription group opens the plain manage sheet") func manageRequestWithoutAGroupUsesThePlainSheet() { var isPresented = true @@ -479,7 +465,6 @@ struct CustomerCenterSheetOwnershipTests { /// /// Waits by suspending: below iOS 26.4, SwiftUI queues a `.task` on the main actor rather than /// starting it immediately, and a test that only spins the run loop never lets it run. - @available(iOS 15.0, *) @Test("the sheet modifier records what it has rendered into StoreKit's sheets") func sheetModifierRecordsTheRenderedParameters() async { let viewModel = makeViewModel() @@ -513,7 +498,6 @@ struct CustomerCenterSheetOwnershipTests { // MARK: - Covered or removed - @available(iOS 15.0, *) private func makeProbe(log: ProbeLog) -> CustomerCenterLifecycleProbeController { let probe = CustomerCenterLifecycleProbeController() probe.onCovered = { log.events.append("covered") } @@ -531,7 +515,6 @@ struct CustomerCenterSheetOwnershipTests { return screen } - @available(iOS 15.0, *) @Test("a screen the host pushes over is covered") func pushOverIsACover() { let log = ProbeLog() @@ -550,7 +533,6 @@ struct CustomerCenterSheetOwnershipTests { /// A full-screen presentation takes the screen out of the window before it's told it /// disappeared, so the window can't be read off the screen at that point. - @available(iOS 15.0, *) @Test("the window's root is covered when something is presented over it") func coveredWindowRootIsACover() { let log = ProbeLog() @@ -567,7 +549,6 @@ struct CustomerCenterSheetOwnershipTests { #expect(log.events == ["covered"]) } - @available(iOS 15.0, *) @Test("a screen popped off a navigation controller is removed") func popIsARemoval() { let log = ProbeLog() @@ -587,7 +568,6 @@ struct CustomerCenterSheetOwnershipTests { /// SwiftUI's `NavigationStack` detaches a popped screen from its navigation controller before the /// screen is told it disappeared, and marks nothing on the way. - @available(iOS 15.0, *) @Test("a screen detached before it's told it disappeared is removed") func detachedScreenIsARemoval() { let log = ProbeLog() @@ -612,7 +592,6 @@ struct CustomerCenterSheetOwnershipTests { #expect(log.events == ["removed"]) } - @available(iOS 15.0, *) @Test("a screen inside a container being dismissed is removed") func dismissedContainerIsARemoval() { let log = ProbeLog() @@ -628,7 +607,6 @@ struct CustomerCenterSheetOwnershipTests { #expect(log.events == ["removed"]) } - @available(iOS 15.0, *) @Test("SwiftUI taking a screen down is reported") func dismantleIsReported() async { let log = ProbeLog() @@ -654,7 +632,6 @@ struct CustomerCenterSheetOwnershipTests { // MARK: - Driving the real navigator - @available(iOS 15.0, *) @Test("pushing a drill-down takes ownership, popping it hands ownership back") func pushAndPopMoveOwnership() { let viewModel = makeViewModel() @@ -686,7 +663,6 @@ struct CustomerCenterSheetOwnershipTests { /// Two screens popped at once are both removed and UIKit doesn't promise which reports first. /// Driven through the real controllers rather than by restating the navigator's arithmetic. - @available(iOS 15.0, *) @Test("popping two drill-downs at once does not strand ownership") func poppingTwoAtOnceDoesNotStrand() { let viewModel = makeViewModel() @@ -721,7 +697,6 @@ struct CustomerCenterSheetOwnershipTests { /// A pushed screen being covered is not a teardown, and the debounce must be vetoed there just /// as it is on the root controller — otherwise `didDismiss` latches and the real teardown is /// silent. - @available(iOS 15.0, *) @Test("covering a drill-down does not deliver a dismissal") func coveringADrillDownDoesNotDismiss() async { let debounce: TimeInterval = 0.2 @@ -755,7 +730,6 @@ struct CustomerCenterSheetOwnershipTests { /// user is on a drill-down, nothing on the drill-down itself is set — so a check of `self` alone /// reads a real teardown as a cover, vetoes the debounce, and delivers no dismissal at all. The /// root underneath is covered, so it never gets a `viewDidDisappear` of its own to correct it. - @available(iOS 15.0, *) @Test("dismissing a container the Customer Center sits inside is a teardown, not a cover") func dismissingTheContainingNavigationControllerDismisses() async { let debounce: TimeInterval = 0.2 @@ -800,7 +774,6 @@ struct CustomerCenterSheetOwnershipTests { private static let now = Date() - @available(iOS 15.0, *) private static func subscription(willRenew: Bool = true) -> SubscriptionTransaction { SubscriptionTransaction( transactionId: "t1", @@ -818,7 +791,6 @@ struct CustomerCenterSheetOwnershipTests { ) } - @available(iOS 15.0, *) private func makeLoadedViewModel( dismissDebounceInterval: TimeInterval = 0.6, lookup: StoreKitTransactionLookupMock = StoreKitTransactionLookupMock() @@ -827,7 +799,6 @@ struct CustomerCenterSheetOwnershipTests { } /// Also hands back the customer info provider, so a test can publish an update. - @available(iOS 15.0, *) private func makeLoadedViewModelAndInfo( dismissDebounceInterval: TimeInterval = 0.6, lookup: StoreKitTransactionLookupMock = StoreKitTransactionLookupMock() @@ -889,7 +860,6 @@ struct CustomerCenterSheetOwnershipTests { /// Asks for the cancellation survey from the subscription's screen and reports whether anything /// was presented over it. - @available(iOS 15.0, *) private func requestSurvey(from viewModel: CustomerCenterViewModel, window: UIWindow) async throws -> Bool { let purchase = try #require(viewModel.purchases.first) let cancel = try #require( @@ -1140,7 +1110,6 @@ private final class ProbeVisibility: ObservableObject { } /// A lifecycle probe SwiftUI can take down, by flipping `visibility`. -@available(iOS 15.0, *) private struct ProbeHost: View { @ObservedObject var visibility: ProbeVisibility let log: ProbeLog diff --git a/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterViewSmokeTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterViewSmokeTests.swift index c3a0e8e334..18ff9e00e9 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterViewSmokeTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Views/CustomerCenterViewSmokeTests.swift @@ -13,7 +13,6 @@ import SwiftUI @MainActor struct CustomerCenterViewSmokeTests { @Test("hosts without crashing in management and no-active states and exposes accessibility ids") - @available(iOS 15.0, *) func hosts() async throws { let now = Date() let sub = SubscriptionTransaction( @@ -52,7 +51,6 @@ struct CustomerCenterViewSmokeTests { } @Test("survey and history views host") - @available(iOS 15.0, *) func secondaryViews() async { let now = Date() let sub = SubscriptionTransaction( @@ -91,7 +89,6 @@ struct CustomerCenterViewSmokeTests { } @Test("presentSuperwallCustomerCenter(isPresented:) compiles and hosts") - @available(iOS 15.0, *) func presentCustomerCenterHosts() { struct Host: View { @State var isPresented = true @@ -110,7 +107,6 @@ struct CustomerCenterViewSmokeTests { } @Test("onCustomerCenterAction applied outside CustomerCenterView merges into the view model and fires on selection") - @available(iOS 15.0, *) func environmentCallbackMergesAndFires() async throws { let now = Date() let sub = SubscriptionTransaction( @@ -168,7 +164,6 @@ struct CustomerCenterViewSmokeTests { } @Test("merged prefers the environment's non-nil closures and keeps un-overridden ones") - @available(iOS 15.0, *) func mergedHelperSemantics() async { var existing = CustomerCenterCallbacks() var existingRestoreCalled = false diff --git a/Tests/SuperwallKitTests/CustomerCenter/Views/DesignReviewSnapshots.swift b/Tests/SuperwallKitTests/CustomerCenter/Views/DesignReviewSnapshots.swift index 6b56d3e184..d26f4c174a 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Views/DesignReviewSnapshots.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Views/DesignReviewSnapshots.swift @@ -328,7 +328,6 @@ struct DesignReviewSnapshots { // MARK: - The screens - @available(iOS 15.0, *) @Test("render every Customer Center state for design review") func renderAll() async throws { let directory = try #require(Self.outputDirectory) diff --git a/Tests/SuperwallKitTests/CustomerCenter/Views/PathTitleTests.swift b/Tests/SuperwallKitTests/CustomerCenter/Views/PathTitleTests.swift index 5f56d5b9c7..7f198604c8 100644 --- a/Tests/SuperwallKitTests/CustomerCenter/Views/PathTitleTests.swift +++ b/Tests/SuperwallKitTests/CustomerCenter/Views/PathTitleTests.swift @@ -11,7 +11,6 @@ import Foundation @Suite("Path row titles") struct PathTitleTests { - @available(iOS 15.0, *) private func title( _ type: CustomerCenterConfiguration.PathType, pathTitle: String? = nil, @@ -26,7 +25,6 @@ struct PathTitleTests { /// The bug the required URL title exists to prevent: the host is identical across an app's own /// links, so three distinct destinations would render as three identical rows. - @available(iOS 15.0, *) @Test("URL rows on one host still read differently") func urlRowsOnTheSameHostAreDistinct() { let paths: [CustomerCenterConfiguration.Path] = [ @@ -42,14 +40,12 @@ struct PathTitleTests { #expect(Set(rows).count == 3, "a shared host must not collapse three rows into one label") } - @available(iOS 15.0, *) @Test("a URL row without a title falls back to its host") func untitledURLRowShowsHost() { #expect(title(.url(URL(string: "https://acme.com/faq")!)) == "acme.com") } /// Every other type names itself, so a title is optional there and overrides the default. - @available(iOS 15.0, *) @Test("built-in types fall back to their localized label") func builtInTypesNameThemselves() { #expect(title(.restore) == CustomerCenterStrings.english.string("customer_center_path_restore")) @@ -57,7 +53,6 @@ struct PathTitleTests { #expect(title(.contactSupport) == CustomerCenterStrings.english.string("customer_center_path_contact_support")) } - @available(iOS 15.0, *) @Test("an explicit title wins over every default", arguments: [ CustomerCenterConfiguration.PathType.restore, .contactSupport, @@ -69,7 +64,6 @@ struct PathTitleTests { /// The manage row is the one built-in whose label depends on where it leads: a web management /// page does more than cancel, so calling it "Cancel subscription" there undersells it. - @available(iOS 15.0, *) @Test("the manage row is named for the store it leads to") func manageRowNamedForItsDestination() { let appStore = title(.manageSubscription, destination: .appleManageSheet(subscriptionGroupId: nil)) @@ -80,7 +74,6 @@ struct PathTitleTests { #expect(appStore != web) } - @available(iOS 15.0, *) @Test("an untitled survey asks why you're cancelling only on the manage-subscription path") func untitledSurveyTitle() { let untitled = CustomerCenterConfiguration.FeedbackSurvey.cancellation From a37ca1fda6eb83e32d14535f43294b26e5e46602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:48:37 +0200 Subject: [PATCH 06/11] Stop declaring tvOS, which the SDK can't build for Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- Package.swift | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c11df99b5..e15def5958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup ## 4.18.0 ### Breaking Changes -- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS, tvOS and watchOS to macOS 12, tvOS 15 and watchOS 8. +- Raises the minimum iOS deployment target from iOS 13 to iOS 15, and the Swift Package Manager minimums for macOS and watchOS to macOS 12 and watchOS 8. - Requires Xcode 26 (Swift 6.2) or later to build the SDK. Apple has required the iOS 26 SDK for App Store Connect uploads since April 28, 2026. The SDK itself still compiles in the Swift 5 language mode. - This ships as a minor release because every App Store Connect upload already needs Xcode 26, so any app that can ship an update already meets the new Xcode requirement. If your app still supports iOS 13 or 14, Swift Package Manager will fail to resolve this version for it: pin SuperwallKit to `4.17.x` (`.upToNextMinor(from: "4.17.0")`, or `~> 4.17.0` in CocoaPods) until you raise your deployment target. diff --git a/CLAUDE.md b/CLAUDE.md index c7bb7baa9d..b65e539648 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ SuperwallKit is an iOS SDK for remote paywall configuration and A/B testing. The ### Minimum Toolchain and Platforms -- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12, tvOS 15, watchOS 8. CI builds iOS (tests), Mac Catalyst and visionOS (`build-platforms.yml`). Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. +- Xcode 26 / Swift 6.2 (`swift-tools-version:6.2`), iOS 15, macOS 12, watchOS 8. tvOS isn't supported (WebKit, SafariServices and Superscript have no tvOS build). CI builds iOS (tests), Mac Catalyst and visionOS (`build-platforms.yml`). Apple requires the iOS 26 SDK for App Store Connect uploads, so there's no reason to support older Xcodes. - Don't add `#if compiler(...)` checks for anything below 6.2; only gate APIs newer than that (e.g. `compiler(>=6.3.2)` for iOS 26.4 StoreKit APIs). - Don't add `@available`/`#available` checks or fallback branches for iOS 15 or older (StoreKit 2, sheet detents, `UIMenu` buttons and friends are always there). - The package pins `swiftLanguageModes: [.v5]`. Moving to Swift 6 language mode is a separate change. diff --git a/Package.swift b/Package.swift index 1d8df3089a..fe030e19d6 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,6 @@ let package = Package( platforms: [ .iOS(.v15), .macOS(.v12), - .tvOS(.v15), .watchOS(.v8) ], products: [ From 5059969e9a3f4d545264b4ea81431564f8972633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:56:44 +0200 Subject: [PATCH 07/11] Test computed properties, the dev server loader and version reporting Pins current behaviour before moving these onto iOS 15 and Swift 6.2 APIs. Co-Authored-By: Claude Opus 5.5 --- .../DevServer/DevServerLocator.swift | 2 +- SuperwallKit.xcodeproj/project.pbxproj | 8 ++ .../DevServerLocatorURLSessionTests.swift | 93 +++++++++++++++++++ .../Network/DeviceHelperTests.swift | 11 +++ ...CoreDataManagerComputedPropertyTests.swift | 80 ++++++++++++++++ 5 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 Tests/SuperwallKitTests/DevServer/DevServerLocatorURLSessionTests.swift create mode 100644 Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerComputedPropertyTests.swift diff --git a/Sources/SuperwallKit/DevServer/DevServerLocator.swift b/Sources/SuperwallKit/DevServer/DevServerLocator.swift index e7d6bcd174..e1c5df5dfd 100644 --- a/Sources/SuperwallKit/DevServer/DevServerLocator.swift +++ b/Sources/SuperwallKit/DevServer/DevServerLocator.swift @@ -186,7 +186,7 @@ actor DevServerLocator { } } - private static func loadWithURLSession(_ request: URLRequest) async throws -> (Data, URLResponse?) { + static func loadWithURLSession(_ request: URLRequest) async throws -> (Data, URLResponse?) { return try await withCheckedThrowingContinuation { continuation in let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index cd73864696..e58fedaae6 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ 2653909358966BE9AC9894F1 /* EvaluationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911CD5859EC1BE7E428F06C4 /* EvaluationResult.swift */; }; 2698874EEAE37BAECE7B8FD8 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79E2143AD65D151AE7A4BF0F /* Network.swift */; }; 2743143ED664F942D5D758B1 /* DevicePreloadScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AC1F76564A6EC47EE696F9 /* DevicePreloadScriptTests.swift */; }; + 27C9585AF7E12C7280FDC51C /* DevServerLocatorURLSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8684B1F9A4CAE200794C2689 /* DevServerLocatorURLSessionTests.swift */; }; 27DC2F109FAE3357DC8418F6 /* AutomaticPurchaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1F47410CBC84D9ABD2F14 /* AutomaticPurchaseController.swift */; }; 27E396F717A62BA4E0D98086 /* PaywallCacheLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA95995DE57E811FD65C02 /* PaywallCacheLogicTests.swift */; }; 28A1E7AFF222A9C36F4AE019 /* Superwall+CustomerCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F0D37FD3B2213A296DEF4 /* Superwall+CustomerCenter.swift */; }; @@ -551,6 +552,7 @@ CF2064883604B915C8768FC5 /* ASIdManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF989B3D90DC3D88FACC4D45 /* ASIdManagerProxy.swift */; }; CF3683E2AD703237EC0CE22E /* PaywallProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95DABA23C6CBEF0AAA63C0 /* PaywallProducts.swift */; }; CFEB0D797815E8EDFB059767 /* Superwall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7EDB6D68D0AEDD332E40BB /* Superwall.swift */; }; + D0580483B7A26B0FD912D7E4 /* CoreDataManagerComputedPropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C3C6C9F66BF8EC45CFADF2 /* CoreDataManagerComputedPropertyTests.swift */; }; D0D4568B72D20652C12AA89B /* GrantedEntitlementsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1099A063D46DE7373CFABC4C /* GrantedEntitlementsTests.swift */; }; D0E19F665C7B230BF3FA122D /* TrackingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85ED994DEC92BB90ACC6AC2 /* TrackingResult.swift */; }; D11EE875D4F3B3AC212526CD /* PurchasePresentationBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4032D6E844683EBEFB6FF619 /* PurchasePresentationBuilderTests.swift */; }; @@ -937,6 +939,7 @@ 58EA2C2A9FFC16FA7B90A31B /* CustomerCenterStringsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterStringsTests.swift; sourceTree = ""; }; 59A767F107FB1FBBC2F22DB3 /* AppSessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSessionManagerTests.swift; sourceTree = ""; }; 59AADECFD10DA2D0BC3FFF1D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = ""; }; + 59C3C6C9F66BF8EC45CFADF2 /* CoreDataManagerComputedPropertyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManagerComputedPropertyTests.swift; sourceTree = ""; }; 59C73BC10AC2F6DE8AB1074A /* SuperwallKit_AppleIncRootCertificate.cer */ = {isa = PBXFileReference; path = SuperwallKit_AppleIncRootCertificate.cer; sourceTree = ""; }; 5A413B6FF46B130D90A428B4 /* ProductPurchaserLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPurchaserLogic.swift; sourceTree = ""; }; 5AC35B7D7641BEB17798C199 /* SupportEmailComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportEmailComposer.swift; sourceTree = ""; }; @@ -1063,6 +1066,7 @@ 862888AB2869D09AA55A017D /* InAppReceiptPayloadContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppReceiptPayloadContainer.swift; sourceTree = ""; }; 865262BC55BAEA135E51698D /* CacheKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheKeys.swift; sourceTree = ""; }; 866F99509EDFBE8BAE10E575 /* LoadingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingModel.swift; sourceTree = ""; }; + 8684B1F9A4CAE200794C2689 /* DevServerLocatorURLSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevServerLocatorURLSessionTests.swift; sourceTree = ""; }; 8708F74D80CB9A440F34A556 /* FakeContactsAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeContactsAuthorizationStatus.swift; sourceTree = ""; }; 8719AC2E83128EE469E58C36 /* RedeemRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemRequest.swift; sourceTree = ""; }; 87AD727C5A8639E704F7BE98 /* PaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = ""; }; @@ -3057,6 +3061,7 @@ children = ( 577D25646DA26238881BF6AB /* DevModeTests.swift */, 21C1CF3FA0D1266FFF8A29FF /* DevServerLocatorTests.swift */, + 8684B1F9A4CAE200794C2689 /* DevServerLocatorURLSessionTests.swift */, A349A124DD1DF28EEF04592C /* DevServerManifestTests.swift */, A4FD16729844D83A3EAA02FC /* DevServerPaywallTests.swift */, 6E0A1ED94DE7737BCB7D4D8C /* DevServerPreviewTests.swift */, @@ -3268,6 +3273,7 @@ CE732EAB1FB2F1C6395243FE /* Core Data */ = { isa = PBXGroup; children = ( + 59C3C6C9F66BF8EC45CFADF2 /* CoreDataManagerComputedPropertyTests.swift */, E74C7DE0FAFE0C01F374DDF0 /* CoreDataManagerFakeDataMock.swift */, EB1B228FB279EA92C35C7394 /* CoreDataManagerMock.swift */, 0D9CC1B947A08633E1C7BAE3 /* CoreDataManagerTests.swift */, @@ -3768,6 +3774,7 @@ AE1D15070BC159212967CAD4 /* ConfirmHoldoutAssignmentTests.swift in Sources */, 61EC2A032D127B323D4A8124 /* ConfirmPaywallAssignmentOperatorTests.swift in Sources */, CBC90EC17DC1EC4C2FE9DFC8 /* ContactStoreProxyTests.swift in Sources */, + D0580483B7A26B0FD912D7E4 /* CoreDataManagerComputedPropertyTests.swift in Sources */, AEAB0C0C168B0B3D864109EA /* CoreDataManagerFakeDataMock.swift in Sources */, A1621A749D8F05959A486ACE /* CoreDataManagerMock.swift in Sources */, C7AB21123540550E513AD28A /* CoreDataManagerTests.swift in Sources */, @@ -3797,6 +3804,7 @@ 8E04DDF7FA8D6A76E96378F3 /* DesignReviewSnapshots.swift in Sources */, FEA3AED0B70D730993A16B2C /* DevModeTests.swift in Sources */, EA21B417513519FEDE6232D4 /* DevServerLocatorTests.swift in Sources */, + 27C9585AF7E12C7280FDC51C /* DevServerLocatorURLSessionTests.swift in Sources */, 069391992F6191874022F2BA /* DevServerManifestTests.swift in Sources */, 669B86B82B4CCD7BC7D02B55 /* DevServerPaywallTests.swift in Sources */, EE1A7003F266DF3C1481EEBA /* DevServerPreviewTests.swift in Sources */, diff --git a/Tests/SuperwallKitTests/DevServer/DevServerLocatorURLSessionTests.swift b/Tests/SuperwallKitTests/DevServer/DevServerLocatorURLSessionTests.swift new file mode 100644 index 0000000000..2c81fdf8c5 --- /dev/null +++ b/Tests/SuperwallKitTests/DevServer/DevServerLocatorURLSessionTests.swift @@ -0,0 +1,93 @@ +// +// DevServerLocatorURLSessionTests.swift +// SuperwallKitTests +// + +import Foundation +import Testing +@testable import SuperwallKit + +/// Answers requests to one made-up host, so registering it globally leaves every +/// other request in the suite alone. +private final class StubURLProtocol: URLProtocol { + static let host = "devserver-loader-stub.invalid" + static let body = Data(#"{"surfaces":[]}"#.utf8) + static let registered: Void = { + URLProtocol.registerClass(StubURLProtocol.self) + }() + + override class func canInit(with request: URLRequest) -> Bool { + return request.url?.host == host + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let url = request.url else { + return + } + switch url.path { + case "/ok": + respond(url: url, status: 200, body: Self.body) + case "/missing": + respond(url: url, status: 404, body: Data("not found".utf8)) + case "/empty": + respond(url: url, status: 200, body: nil) + default: + client?.urlProtocol(self, didFailWithError: URLError(.cannotConnectToHost)) + } + } + + override func stopLoading() {} + + private func respond(url: URL, status: Int, body: Data?) { + let response = HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: nil)! + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + if let body = body { + client?.urlProtocol(self, didLoad: body) + } + client?.urlProtocolDidFinishLoading(self) + } +} + +struct DevServerLocatorURLSessionTests { + init() { + StubURLProtocol.registered + } + + private func load(_ path: String) async throws -> (Data, URLResponse?) { + let url = URL(string: "http://\(StubURLProtocol.host)\(path)")! + return try await DevServerLocator.loadWithURLSession(URLRequest(url: url)) + } + + @Test func success_returnsBodyAndResponse() async throws { + let (data, response) = try await load("/ok") + + #expect(data == StubURLProtocol.body) + #expect((response as? HTTPURLResponse)?.statusCode == 200) + } + + /// The locator reads the status itself, so an error status must come back as a + /// response rather than a thrown error. + @Test func errorStatus_returnsResponseWithoutThrowing() async throws { + let (data, response) = try await load("/missing") + + #expect(data == Data("not found".utf8)) + #expect((response as? HTTPURLResponse)?.statusCode == 404) + } + + @Test func emptyBody_returnsEmptyData() async throws { + let (data, response) = try await load("/empty") + + #expect(data.isEmpty) + #expect((response as? HTTPURLResponse)?.statusCode == 200) + } + + @Test func connectionFailure_throwsTheURLError() async { + await #expect(throws: URLError.self) { + _ = try await load("/unreachable") + } + } +} diff --git a/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift b/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift index 6298bf67b2..0be77a73ad 100644 --- a/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift +++ b/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift @@ -26,6 +26,17 @@ struct DeviceHelperTests { ) } + /// The SDK needs Swift 6.2 to build. In the Swift 5 language mode it builds in, a + /// Swift 6 compiler reports its language version as 5.10. + @Test func swiftAndCompilerVersions_matchTheToolchainFloor() { + let (deviceHelper, dependencyContainer) = makeDeviceHelper(gate: Gate(false)) + withExtendedLifetime(dependencyContainer) { + #expect(["5.10", "6.2"].contains(deviceHelper.currentSwiftVersion())) + let compilerVersion = deviceHelper.currentCompilerVersion() + #expect(compilerVersion.compare("6.2", options: .numeric) != .orderedAscending) + } + } + @Test func makePaddedSdkVersion_withBeta() { let version = "3.0.0-beta.1" let paddedVersion = DeviceHelper.makePaddedVersion(using: version) diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerComputedPropertyTests.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerComputedPropertyTests.swift new file mode 100644 index 0000000000..773e9cebb0 --- /dev/null +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerComputedPropertyTests.swift @@ -0,0 +1,80 @@ +// +// CoreDataManagerComputedPropertyTests.swift +// SuperwallKitTests +// + +import Foundation +import Testing +@testable import SuperwallKit + +struct CoreDataManagerComputedPropertyTests { + private let coreDataManager = CoreDataManager(coreDataStack: CoreDataStackMock()) + + private func save(_ name: String, secondsAgo: TimeInterval) async -> PlacementData { + let placementData = PlacementData.stub() + .setting(\.name, to: name) + .setting(\.createdAt, to: Date().addingTimeInterval(-secondsAgo)) + await withCheckedContinuation { continuation in + coreDataManager.savePlacementData(placementData) { _ in + continuation.resume() + } + } + return placementData + } + + private func hoursSince( + _ name: String, + after placement: PlacementData? = nil + ) async -> Int? { + return await coreDataManager.getComputedPropertySincePlacement( + placement, + request: ComputedPropertyRequest(type: .hoursSince, placementName: name) + ) + } + + @Test func noSavedPlacement_returnsNil() async { + #expect(await hoursSince("missing") == nil) + } + + @Test func savedPlacement_returnsTimeSinceIt() async { + _ = await save("opened", secondsAgo: 3 * 3600 + 60) + + #expect(await hoursSince("opened") == 3) + } + + @Test func usesTheMostRecentPlacement() async { + _ = await save("opened", secondsAgo: 8 * 3600 + 60) + _ = await save("opened", secondsAgo: 2 * 3600 + 60) + + #expect(await hoursSince("opened") == 2) + } + + @Test func ignoresOtherPlacementNames() async { + _ = await save("opened", secondsAgo: 4 * 3600 + 60) + _ = await save("closed", secondsAgo: 60) + + #expect(await hoursSince("opened") == 4) + } + + /// The placement being evaluated has just been saved, so it's skipped and the one + /// before it is measured instead. + @Test func samePlacementBeingEvaluated_measuresTheOneBeforeIt() async { + _ = await save("opened", secondsAgo: 5 * 3600 + 60) + let current = await save("opened", secondsAgo: 0) + + #expect(await hoursSince("opened", after: current) == 5) + } + + @Test func differentPlacementBeingEvaluated_measuresTheLatest() async { + _ = await save("opened", secondsAgo: 6 * 3600 + 60) + let current = await save("closed", secondsAgo: 0) + + #expect(await hoursSince("opened", after: current) == 6) + } + + @Test func onlyPlacementIsTheOneBeingEvaluated_returnsNil() async { + let current = await save("opened", secondsAgo: 0) + + #expect(await hoursSince("opened", after: current) == nil) + } +} From 7e372aa81229aee1362b929ffbc1abd7c24636e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 29 Sep 2026 21:54:44 +0200 Subject: [PATCH 08/11] Use iOS 15 and Swift 6.2 APIs now that they're the floor - Core Data reads use the async perform, and a computed property reads the placement's date on the context's queue instead of after it. - The dev server probe uses URLSession's async data(for:). - Window lookups use UIWindowScene.keyWindow, and test mode reuses the existing top view controller helper instead of the deprecated windows list. - Superscript is imported internally so it stays out of the SDK's interface. - Drop Swift and compiler version branches below 6.2. Co-Authored-By: Claude Opus 5.5 --- .../DevServer/DevServerLocator.swift | 12 +- .../UIApplication+ActiveWindow.swift | 4 +- .../Network/Device Helper/SwiftVersion.swift | 172 +----------------- .../Expression Evaluator/CELEvaluator.swift | 2 +- .../EvaluationContext.swift | 2 +- .../Loading/Listeners/HiddenListener.swift | 1 + .../Loading/Listeners/PaddingListener.swift | 1 + .../Storage/Core Data/CoreDataManager.swift | 54 ++---- .../Storage/Core Data/CoreDataStack.swift | 35 ++-- .../TestMode/TestModeTransactionHandler.swift | 16 +- .../Core Data/CoreDataManagerMock.swift | 6 +- 11 files changed, 42 insertions(+), 263 deletions(-) diff --git a/Sources/SuperwallKit/DevServer/DevServerLocator.swift b/Sources/SuperwallKit/DevServer/DevServerLocator.swift index e1c5df5dfd..fb966186af 100644 --- a/Sources/SuperwallKit/DevServer/DevServerLocator.swift +++ b/Sources/SuperwallKit/DevServer/DevServerLocator.swift @@ -187,15 +187,7 @@ actor DevServerLocator { } static func loadWithURLSession(_ request: URLRequest) async throws -> (Data, URLResponse?) { - return try await withCheckedThrowingContinuation { continuation in - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { - continuation.resume(returning: (data, response)) - } else { - continuation.resume(throwing: error ?? URLError(.badServerResponse)) - } - } - task.resume() - } + let (data, response) = try await URLSession.shared.data(for: request) + return (data, response) } } diff --git a/Sources/SuperwallKit/Misc/Extensions/UIApplication+ActiveWindow.swift b/Sources/SuperwallKit/Misc/Extensions/UIApplication+ActiveWindow.swift index 80b6323ec3..5e020d7474 100644 --- a/Sources/SuperwallKit/Misc/Extensions/UIApplication+ActiveWindow.swift +++ b/Sources/SuperwallKit/Misc/Extensions/UIApplication+ActiveWindow.swift @@ -16,13 +16,13 @@ extension UIApplication { // First, try to find a key window in the foreground active scene if let windowScene = sharedApplication.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - return windowScene.windows.first { $0.isKeyWindow } ?? windowScene.windows.first + return windowScene.keyWindow ?? windowScene.windows.first } // Then try to find a key window in the foreground inactive scene if let windowScene = sharedApplication.connectedScenes .first(where: { $0.activationState == .foregroundInactive }) as? UIWindowScene { - return windowScene.windows.first { $0.isKeyWindow } ?? windowScene.windows.first + return windowScene.keyWindow ?? windowScene.windows.first } // Fallback: search across all scenes for a key window diff --git a/Sources/SuperwallKit/Network/Device Helper/SwiftVersion.swift b/Sources/SuperwallKit/Network/Device Helper/SwiftVersion.swift index 89a92fb2cc..ddb4c63081 100644 --- a/Sources/SuperwallKit/Network/Device Helper/SwiftVersion.swift +++ b/Sources/SuperwallKit/Network/Device Helper/SwiftVersion.swift @@ -4,108 +4,14 @@ // // Created by Yusuf Tör on 05/08/2025. // -// swiftlint:disable function_body_length extension DeviceHelper { func currentSwiftVersion() -> String { #if swift(>=6.2) return "6.2" - #elseif swift(>=6.1) - return "6.1" - #elseif swift(>=6.0) - return "6.0" - #elseif swift(>=5.10) - return "5.10" - #elseif swift(>=5.9.2) - return "5.9.2" - #elseif swift(>=5.9.1) - return "5.9.1" - #elseif swift(>=5.9) - return "5.9" - #elseif swift(>=5.8.1) - return "5.8.1" - #elseif swift(>=5.8) - return "5.8" - #elseif swift(>=5.7.3) - return "5.7.3" - #elseif swift(>=5.7.2) - return "5.7.2" - #elseif swift(>=5.7.1) - return "5.7.1" - #elseif swift(>=5.7) - return "5.7" - #elseif swift(>=5.6.1) - return "5.6.1" - #elseif swift(>=5.6) - return "5.6" - #elseif swift(>=5.5.2) - return "5.5.2" - #elseif swift(>=5.5.1) - return "5.5.1" - #elseif swift(>=5.5) - return "5.5" - #elseif swift(>=5.4.2) - return "5.4.2" - #elseif swift(>=5.4) - return "5.4" - #elseif swift(>=5.3.2) - return "5.3.2" - #elseif swift(>=5.3.1) - return "5.3.1" - #elseif swift(>=5.3) - return "5.3" - #elseif swift(>=5.2.4) - return "5.2.4" - #elseif swift(>=5.2.2) - return "5.2.2" - #elseif swift(>=5.2) - return "5.2" - #elseif swift(>=5.1.3) - return "5.1.3" - #elseif swift(>=5.1.2) - return "5.1.2" - #elseif swift(>=5.1) - return "5.1" - #elseif swift(>=5.0.1) - return "5.0.1" - #elseif swift(>=5.0) - return "5.0" - #elseif swift(>=4.2.1) - return "4.2.1" - #elseif swift(>=4.2) - return "4.2" - #elseif swift(>=4.1.2) - return "4.1.2" - #elseif swift(>=4.1) - return "4.1" - #elseif swift(>=4.0.3) - return "4.0.3" - #elseif swift(>=4.0.2) - return "4.0.2" - #elseif swift(>=4.0) - return "4.0" - #elseif swift(>=3.1) - return "3.1" - #elseif swift(>=3.0.2) - return "3.0.2" - #elseif swift(>=3.0.1) - return "3.0.1" - #elseif swift(>=3.0) - return "3.0" - #elseif swift(>=2.2) - return "2.2" - #elseif swift(>=2.1) - return "2.1" - #elseif swift(>=2.0) - return "2.0" - #elseif swift(>=1.2) - return "1.2" - #elseif swift(>=1.1) - return "1.1" - #elseif swift(>=1.0) - return "1.0" #else - return "Unknown" + // A Swift 6 compiler reports this in the Swift 5 language mode. + return "5.10" #endif } @@ -124,80 +30,6 @@ extension DeviceHelper { return "6.2.3" #elseif compiler(>=6.2) return "6.2" - #elseif compiler(>=6.1) - return "6.1" - #elseif compiler(>=6.0) - return "6.0" - #elseif compiler(>=5.10) - return "5.10" - #elseif compiler(>=5.9.2) - return "5.9.2" - #elseif compiler(>=5.9.1) - return "5.9.1" - #elseif compiler(>=5.9) - return "5.9" - #elseif compiler(>=5.8.1) - return "5.8.1" - #elseif compiler(>=5.8) - return "5.8" - #elseif compiler(>=5.7.3) - return "5.7.3" - #elseif compiler(>=5.7.2) - return "5.7.2" - #elseif compiler(>=5.7.1) - return "5.7.1" - #elseif compiler(>=5.7) - return "5.7" - #elseif compiler(>=5.6.1) - return "5.6.1" - #elseif compiler(>=5.6) - return "5.6" - #elseif compiler(>=5.5.2) - return "5.5.2" - #elseif compiler(>=5.5.1) - return "5.5.1" - #elseif compiler(>=5.5) - return "5.5" - #elseif compiler(>=5.4.2) - return "5.4.2" - #elseif compiler(>=5.4) - return "5.4" - #elseif compiler(>=5.3.2) - return "5.3.2" - #elseif compiler(>=5.3.1) - return "5.3.1" - #elseif compiler(>=5.3) - return "5.3" - #elseif compiler(>=5.2.4) - return "5.2.4" - #elseif compiler(>=5.2.2) - return "5.2.2" - #elseif compiler(>=5.2) - return "5.2" - #elseif compiler(>=5.1.3) - return "5.1.3" - #elseif compiler(>=5.1.2) - return "5.1.2" - #elseif compiler(>=5.1) - return "5.1" - #elseif compiler(>=5.0.1) - return "5.0.1" - #elseif compiler(>=5.0) - return "5.0" - #elseif compiler(>=4.2.1) - return "4.2.1" - #elseif compiler(>=4.2) - return "4.2" - #elseif compiler(>=4.1.2) - return "4.1.2" - #elseif compiler(>=4.1) - return "4.1" - #elseif compiler(>=4.0.3) - return "4.0.3" - #elseif compiler(>=4.0.2) - return "4.0.2" - #elseif compiler(>=4.0) - return "4.0" #else return "Unknown" #endif diff --git a/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/CELEvaluator.swift b/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/CELEvaluator.swift index 966af3efb0..a1aaad1b23 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/CELEvaluator.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/CELEvaluator.swift @@ -7,7 +7,7 @@ // swiftlint:disable function_body_length import Foundation -import Superscript +internal import Superscript protocol ExpressionEvaluating { func evaluateExpression( diff --git a/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/EvaluationContext.swift b/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/EvaluationContext.swift index a1f996b008..42fd00eec2 100644 --- a/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/EvaluationContext.swift +++ b/Sources/SuperwallKit/Paywall/Presentation/Audience Logic/Expression Evaluator/EvaluationContext.swift @@ -7,7 +7,7 @@ // swiftlint:disable function_body_length import Foundation -import Superscript +internal import Superscript final class EvaluationContext: HostContext { let storage: Storage diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift index 0e43e65128..d920c3805f 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/HiddenListener.swift @@ -5,6 +5,7 @@ // Created by Yusuf Tör on 12/10/2022. // +import Combine import SwiftUI struct HiddenListenerViewModifier: ViewModifier { diff --git a/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift index 911736e853..5b7cf5c0cb 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Loading/Listeners/PaddingListener.swift @@ -5,6 +5,7 @@ // Created by Yusuf Tör on 01/02/2023. // +import Combine import SwiftUI struct PaddingListenerViewModifier: ViewModifier { diff --git a/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift b/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift index 9b46cc148e..92d75bfb5d 100644 --- a/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift +++ b/Sources/SuperwallKit/Storage/Core Data/CoreDataManager.swift @@ -174,26 +174,18 @@ class CoreDataManager { lastEventDate = placement.name == request.placementName ? placement.createdAt : nil } - return await withCheckedContinuation { continuation in - coreDataStack.getLastSavedPlacement( - name: request.placementName, - before: lastEventDate - ) { placement in - guard let placement = placement else { - return continuation.resume(returning: nil) - } - let createdAt = placement.createdAt - let calendar = Calendar.current - let currentDate = Date() - let components = calendar.dateComponents( - [request.type.calendarComponent], - from: createdAt, - to: currentDate - ) - - continuation.resume(returning: request.type.dateComponent(from: components)) - } + guard let createdAt = await coreDataStack.getLastSavedPlacementDate( + name: request.placementName, + before: lastEventDate + ) else { + return nil } + let components = Calendar.current.dateComponents( + [request.type.calendarComponent], + from: createdAt, + to: Date() + ) + return request.type.dateComponent(from: components) } func countAudienceOccurrences( @@ -223,18 +215,10 @@ class CoreDataManager { audienceOccurrence.key ) - return await withCheckedContinuation { continuation in - coreDataStack.count(for: fetchRequest) { count in - continuation.resume(returning: count) - } - } + return await coreDataStack.count(for: fetchRequest) case .infinity: fetchRequest.predicate = NSPredicate(format: "occurrenceKey == %@", audienceOccurrence.key) - return await withCheckedContinuation { continuation in - coreDataStack.count(for: fetchRequest) { count in - continuation.resume(returning: count) - } - } + return await coreDataStack.count(for: fetchRequest) } } @@ -265,18 +249,10 @@ class CoreDataManager { placementName ) - return await withCheckedContinuation { continuation in - coreDataStack.count(for: fetchRequest) { count in - continuation.resume(returning: count) - } - } + return await coreDataStack.count(for: fetchRequest) case .infinity: fetchRequest.predicate = NSPredicate(format: "name == %@", placementName) - return await withCheckedContinuation { continuation in - coreDataStack.count(for: fetchRequest) { count in - continuation.resume(returning: count) - } - } + return await coreDataStack.count(for: fetchRequest) } } } diff --git a/Sources/SuperwallKit/Storage/Core Data/CoreDataStack.swift b/Sources/SuperwallKit/Storage/Core Data/CoreDataStack.swift index 3e2c4cf468..e016d601c1 100644 --- a/Sources/SuperwallKit/Storage/Core Data/CoreDataStack.swift +++ b/Sources/SuperwallKit/Storage/Core Data/CoreDataStack.swift @@ -78,20 +78,16 @@ class CoreDataStack { self.mainContext = mainContext } - func count( - for fetchRequest: NSFetchRequest, - completion: @escaping ((Int) -> Void) - ) { + func count(for fetchRequest: NSFetchRequest) async -> Int { guard let backgroundContext = backgroundContext, persistentContainer != nil else { - return completion(0) + return 0 } - backgroundContext.perform { + return await backgroundContext.perform { do { - let count = try backgroundContext.count(for: fetchRequest) - completion(count) + return try backgroundContext.count(for: fetchRequest) } catch let error as NSError { Logger.debug( logLevel: .error, @@ -100,21 +96,22 @@ class CoreDataStack { info: error.userInfo, error: error ) - completion(0) + return 0 } } } - func getLastSavedPlacement( + /// When the most recent placement with this name was saved, optionally only looking + /// before `date`. + func getLastSavedPlacementDate( name: String, - before date: Date?, - completion: @escaping ((ManagedEventData?) -> Void) - ) { + before date: Date? + ) async -> Date? { guard let backgroundContext = backgroundContext else { - return completion(nil) + return nil } - backgroundContext.perform { + return await backgroundContext.perform { let fetchRequest = ManagedEventData.fetchRequest() if let date = date { fetchRequest.predicate = NSPredicate(format: "name == %@ AND createdAt < %@", name, date as NSDate) @@ -125,11 +122,7 @@ class CoreDataStack { fetchRequest.fetchLimit = 1 do { - let results = try backgroundContext.fetch(fetchRequest) - guard let placement = results.first else { - return completion(nil) - } - completion(placement) + return try backgroundContext.fetch(fetchRequest).first?.createdAt } catch { Logger.debug( logLevel: .error, @@ -138,7 +131,7 @@ class CoreDataStack { info: ["placement": name], error: error ) - completion(nil) + return nil } } } diff --git a/Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift b/Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift index b422adec90..21fa50a19c 100644 --- a/Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift +++ b/Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift @@ -25,7 +25,7 @@ final class TestModeTransactionHandler { product: StoreProduct, purchaseSource: PurchaseSource ) async -> PurchaseResult { - guard let viewController = topViewController() else { + guard let viewController = UIViewController.topMostViewController else { return .failed(PurchaseError.productUnavailable) } @@ -109,7 +109,7 @@ final class TestModeTransactionHandler { /// or `.failed(nil)` when the user cancels. @MainActor func handleRestore() async -> RestorationResult { - guard let viewController = topViewController() else { + guard let viewController = UIViewController.topMostViewController else { return .failed(nil) } @@ -177,16 +177,4 @@ final class TestModeTransactionHandler { return .failed(nil) } } - - @MainActor - private func topViewController() -> UIViewController? { - guard let window = UIApplication.sharedApplication?.windows.first(where: { $0.isKeyWindow }), - var topVC = window.rootViewController else { - return nil - } - while let presented = topVC.presentedViewController { - topVC = presented - } - return topVC - } } diff --git a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift index 565db7e8de..7f8a6f9599 100644 --- a/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift +++ b/Tests/SuperwallKitTests/Storage/Core Data/CoreDataManagerMock.swift @@ -19,10 +19,6 @@ final class CoreDataManagerMock: CoreDataManager { func countAllEvents() async -> Int { let fetchRequest = ManagedEventData.fetchRequest() - return await withCheckedContinuation { continuation in - internalDataStack.count(for: fetchRequest) { count in - continuation.resume(returning: count) - } - } + return await internalDataStack.count(for: fetchRequest) } } From dd25776c0fe4b93582d239c6aa8f81d561348197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:25:05 +0200 Subject: [PATCH 09/11] Stop using the app-wide windows list and the scene-less review request The landscape check reads the active scene instead of UIApplication.windows, and a review request with no window of its own falls back to the active scene. Co-Authored-By: Claude Opus 5.5 --- .../Extensions/UIWindow/UIWindow+Landscape.swift | 9 +-------- .../View Controller/PaywallViewController.swift | 14 +++++--------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/Sources/SuperwallKit/Misc/Extensions/UIWindow/UIWindow+Landscape.swift b/Sources/SuperwallKit/Misc/Extensions/UIWindow/UIWindow+Landscape.swift index 2b03c193a8..2b8275406b 100644 --- a/Sources/SuperwallKit/Misc/Extensions/UIWindow/UIWindow+Landscape.swift +++ b/Sources/SuperwallKit/Misc/Extensions/UIWindow/UIWindow+Landscape.swift @@ -9,13 +9,6 @@ import UIKit extension UIWindow { static var isLandscape: Bool { - guard let sharedApplication = UIApplication.sharedApplication else { - return false - } - return sharedApplication.windows - .first? - .windowScene? - .interfaceOrientation - .isLandscape ?? false + return UIApplication.sharedApplication?.activeWindowScene?.interfaceOrientation.isLandscape ?? false } } diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index 4b8a06500b..8ca32fe45e 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -1439,21 +1439,17 @@ extension PaywallViewController: PaywallMessageHandlerDelegate { func requestReview(type: ReviewType) { switch type { case .inApp: - #if os(visionOS) - if let scene = view.window?.windowScene { + if let scene = view.window?.windowScene ?? UIApplication.sharedApplication?.activeWindowScene { + #if os(visionOS) AppStore.requestReview(in: scene) - } - #else - if let scene = view.window?.windowScene { + #else if #available(iOS 16.0, *) { AppStore.requestReview(in: scene) } else { SKStoreReviewController.requestReview(in: scene) } - } else { - SKStoreReviewController.requestReview() - } - #endif + #endif + } trackReviewRequest(type: .inApp) case .external: let appId: String From 572f24dea7c526c5725cea5ac4575b2a7b8fb89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:07:35 +0200 Subject: [PATCH 10/11] Drop tvOS from platform checks now that it isn't supported Also reword two test comments that still talked about iOS 14. Co-Authored-By: Claude Opus 5.5 --- .../Analytics/Attribution/AttributionFetcher.swift | 4 ++-- .../Analytics/Attribution/AttributionPoster.swift | 1 - .../Misc/Extensions/SystemInfo+NotificationName.swift | 6 +++--- .../Products/Receipt Manager/EntitlementProcessor.swift | 6 +++--- .../Products/Receipt Manager/Validation/Validation.swift | 4 ++-- .../StoreKit/Products/StoreProduct/SK2StoreProduct.swift | 4 ++-- .../Purchasing/StoreKit 2/ProductPurchaserSK2.swift | 2 +- .../Attribution/AttributionDeviceIdentifiersTests.swift | 5 ++--- .../Location/LocationPermissionDelegateTests.swift | 2 +- .../Receipt Manager/EntitlementProcessorTests.swift | 2 +- 10 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift index 97b35ee878..ff1f6aa1a8 100644 --- a/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift +++ b/Sources/SuperwallKit/Analytics/Attribution/AttributionFetcher.swift @@ -38,7 +38,7 @@ final class AttributionFetcher { var identifierForAdvertisers: String? { // should match available platforms here: // https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier - #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) + #if os(iOS) || os(macOS) || os(visionOS) let identifierManagerProxy = AttributionTypeFactory.asIdProxy() guard let identifierManagerProxy = identifierManagerProxy else { Logger.debug( @@ -302,7 +302,7 @@ extension AttributionFetcher { if let attStatusProvider { return attStatusProvider() } - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || os(macOS) || os(visionOS) + #if os(iOS) || targetEnvironment(macCatalyst) || os(macOS) || os(visionOS) return TrackingManagerProxy().trackingAuthorizationStatus() #else return nil diff --git a/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift b/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift index c6207fcb03..a8d25878c8 100644 --- a/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift +++ b/Sources/SuperwallKit/Analytics/Attribution/AttributionPoster.swift @@ -138,7 +138,6 @@ final class AttributionPoster { } // Should match OS availability in https://developer.apple.com/documentation/ad_services - @available(tvOS, unavailable) @available(watchOS, unavailable) func getAdServicesTokenIfNeeded() async { // Single-flight: only one collection at a time. Synchronous check on the diff --git a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift index aa8f41e3a3..8794bf1af5 100644 --- a/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift +++ b/Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift @@ -6,7 +6,7 @@ // // swiftlint:disable identifier_name -#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(visionOS) || targetEnvironment(macCatalyst) import UIKit #elseif os(watchOS) import UIKit @@ -17,7 +17,7 @@ import AppKit enum SystemInfo { static var applicationWillEnterForegroundNotification: Notification.Name { - #if os(iOS) || os(tvOS) || os(visionOS) + #if os(iOS) || os(visionOS) UIApplication.willEnterForegroundNotification #elseif os(macOS) NSApplication.willBecomeActiveNotification @@ -27,7 +27,7 @@ enum SystemInfo { } static var applicationDidBecomeActiveNotification: Notification.Name? { - #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) + #if os(iOS) || os(visionOS) || targetEnvironment(macCatalyst) return UIApplication.didBecomeActiveNotification #elseif os(macOS) return NSApplication.didBecomeActiveNotification diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift index 7a712cf7f9..2624bce3c5 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/EntitlementProcessor.swift @@ -61,7 +61,7 @@ protocol SubscriptionStatusProvider { func getSubscriptionStatus(for transaction: Transaction) async -> StoreKit.Product.SubscriptionInfo.Status? func getWillAutoRenew(from status: StoreKit.Product.SubscriptionInfo.Status?) -> Bool func getSubscriptionState(from status: StoreKit.Product.SubscriptionInfo.Status?) -> LatestSubscription.State? - @available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) + @available(iOS 17.2, macOS 14.2, watchOS 10.2, visionOS 1.1, *) func getOfferType(from transaction: Transaction) -> LatestSubscription.OfferType? } @@ -95,7 +95,7 @@ struct StoreKitSubscriptionStatusProvider: SubscriptionStatusProvider { } } - @available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) + @available(iOS 17.2, macOS 14.2, watchOS 10.2, visionOS 1.1, *) func getOfferType(from transaction: Transaction) -> LatestSubscription.OfferType? { if transaction.offer?.type == .winBack { return .winback @@ -469,7 +469,7 @@ extension Transaction: EntitlementTransaction { } var offerType: LatestSubscription.OfferType? { - if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) { + if #available(iOS 17.2, macOS 14.2, watchOS 10.2, visionOS 1.1, *) { if offer?.type == .winBack { return .winback } diff --git a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift index 9bb179f4f6..4a93c89e41 100644 --- a/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift +++ b/Sources/SuperwallKit/StoreKit/Products/Receipt Manager/Validation/Validation.swift @@ -6,7 +6,7 @@ // Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved. // -#if os(iOS) || os(tvOS) +#if os(iOS) import UIKit #elseif os(watchOS) import UIKit @@ -253,7 +253,7 @@ private func guid() -> Data { return data } return Data() -#elseif !targetEnvironment(macCatalyst) && (os(iOS) || os(tvOS) || os(visionOS)) +#elseif !targetEnvironment(macCatalyst) && (os(iOS) || os(visionOS)) if let identifierForVendor = UIDevice.current.identifierForVendor { var rawUUID = identifierForVendor.uuid let count = MemoryLayout.size(ofValue: rawUUID) diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index 299c9253a5..10213a40dd 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -39,7 +39,7 @@ struct SK2StoreProduct: StoreProductType { self.billingPlanType = billingPlanType #if compiler(>=6.3.2) - if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *), + if #available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *), let term = Self.findPricingTerm(for: billingPlanType, in: sk2Product) { // Use the commitment *period* (= year for an annual MONTHLY product) // so the paywall reads as the underlying product (not its billing @@ -225,7 +225,7 @@ struct SK2StoreProduct: StoreProductType { } #if compiler(>=6.3.2) - @available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) + @available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *) private static func findPricingTerm( for billingPlanType: AppStoreProduct.BillingPlanType?, in sk2Product: SK2Product diff --git a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift index f420add380..2944089168 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/Purchasing/StoreKit 2/ProductPurchaserSK2.swift @@ -99,7 +99,7 @@ final class ProductPurchaserSK2: Purchasing { } #if compiler(>=6.3.2) - if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *), + if #available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *), let plan = product.billingPlanType { let sk2Plan: StoreKit.Product.SubscriptionInfo.BillingPlanType switch plan { diff --git a/Tests/SuperwallKitTests/Analytics/Attribution/AttributionDeviceIdentifiersTests.swift b/Tests/SuperwallKitTests/Analytics/Attribution/AttributionDeviceIdentifiersTests.swift index ae070b2df1..408522dd93 100644 --- a/Tests/SuperwallKitTests/Analytics/Attribution/AttributionDeviceIdentifiersTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Attribution/AttributionDeviceIdentifiersTests.swift @@ -102,9 +102,8 @@ struct AttributionDeviceIdentifiersTests { ) defer { fetcher.cancelPendingOperations() } - // Before iOS 14.5 the IDFA is readable while ATT still reads - // `notDetermined`, and a build that can't resolve `ATTrackingManager` - // reports the same status, so the status mustn't gate the IDFA. + // A build that can't resolve `ATTrackingManager` reports `notDetermined` + // while the IDFA is still readable, so the status mustn't gate the IDFA. fetcher.mergeIntegrationAttributes(attributes: ["appsflyerId": "af-1"]) #expect(fetcher.integrationAttributes["attStatus"] == "0") #expect(fetcher.integrationAttributes["idfa"] == "advertiser-1") diff --git a/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift b/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift index a111b621f0..93c67c605d 100644 --- a/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift +++ b/Tests/SuperwallKitTests/Permissions/Location/LocationPermissionDelegateTests.swift @@ -17,7 +17,7 @@ struct LocationPermissionDelegateTests { receivedStatus = status } - // Simulate iOS 14+ delegate call with a mock manager + // Simulate the authorization change delegate call with a mock manager let mockManager = MockLocationManager() mockManager.mockAuthorizationStatus = FakeLocationAuthorizationStatus.authorizedWhenInUse.rawValue delegate.locationManagerDidChangeAuthorization(mockManager) diff --git a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift index 741c1faa96..8265002612 100644 --- a/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift +++ b/Tests/SuperwallKitTests/StoreKit/Products/Receipt Manager/EntitlementProcessorTests.swift @@ -1362,7 +1362,7 @@ struct MockSubscriptionStatusProvider: SubscriptionStatusProvider { return mockState } - @available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) + @available(iOS 17.2, macOS 14.2, watchOS 10.2, visionOS 1.1, *) func getOfferType(from transaction: Transaction) -> LatestSubscription.OfferType? { return mockOfferType } From 02d97c33b6c94663d042dcaad67fde6d9bf3140c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:09:53 +0200 Subject: [PATCH 11/11] Make the compiler version test fail on "Unknown" Co-Authored-By: Claude Opus 5.5 --- Tests/SuperwallKitTests/Network/DeviceHelperTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift b/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift index 0be77a73ad..34b99846a5 100644 --- a/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift +++ b/Tests/SuperwallKitTests/Network/DeviceHelperTests.swift @@ -33,6 +33,7 @@ struct DeviceHelperTests { withExtendedLifetime(dependencyContainer) { #expect(["5.10", "6.2"].contains(deviceHelper.currentSwiftVersion())) let compilerVersion = deviceHelper.currentCompilerVersion() + #expect(compilerVersion != "Unknown") #expect(compilerVersion.compare("6.2", options: .numeric) != .orderedAscending) } }