Skip to content

Require iOS 15 and Xcode 26 (Swift 6.2) - #532

Merged
yusuftor merged 12 commits into
developfrom
chore/min-ios15-swift62
Oct 7, 2026
Merged

yusuftor merged 12 commits into
developfrom
chore/min-ios15-swift62

Conversation

@yusuftor

@yusuftor yusuftor commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Changes in this pull request

Since April 28, 2026, Apple has required App Store Connect uploads to be built with the iOS 26 SDK, so any app picking up a new SDK release is already on Xcode 26. This PR raises the SDK's minimums to match, then removes the code that only existed to support older versions.

Minimums (commit 1)

  • Toolchain: swift-tools-version goes from 5.5 to 6.2 (Xcode 26+). The package pins swiftLanguageModes: [.v5], so strict concurrency stays off; moving to Swift 6 language mode can be a separate change.
  • Deployment targets: iOS goes from 13 to 15 in Package.swift, the podspec, project.yml and the Xcode project.
  • Other SwiftPM platforms: macOS goes from 10.12 to 12, because SwiftPM 6.2 deprecates anything older. watchOS goes from 6.2 to 8, to pair with iOS 15. tvOS isn't declared, because the SDK can't build for it (WebKit, SafariServices and Superscript have no tvOS build).
  • Manifest syntax: the Superscript dependency uses exact:, because .exact(...) isn't available in the 6.2 manifest.
  • Podspec language version: swift_versions stays at 5.5. In CocoaPods it sets the Swift language version, not the compiler, so setting it to 6.2 would force Swift 6 language mode on pod users.

Dead code removed

  • Compiler checks: #if compiler(...) checks for versions below 6.2, the swift(<5.7) workaround that stored the SK2 Product as Any, xcode12.py and its // ignore-xcode-12 markers. The compiler(>=6.3.2) checks for the iOS 26.4 StoreKit APIs stay. (commit 1)
  • Availability checks (commit 2): every @available/#available check at or below iOS 15 / Mac Catalyst 15 / macOS 12 / watchOS 8, across 71 files, along with the fallback branches that only ran on older OSes:
    • The default storeKitVersion is now always StoreKit 2. Before, it was StoreKit 1 on iOS 13/14.
    • Receipt validation no longer has a SecTrustEvaluate fallback.
    • The pre-iOS 14 photo-permission APIs are gone, and camera/tracking permissions are no longer reported as "unsupported" on old OSes.
    • The Test Mode action-sheet fallbacks for the UIMenu buttons are gone, along with their helpers.
    • The guard #available(...) else { return } early returns in the tests are gone.
    • Checks for iOS 16 and later are unchanged.
  • Small fixes:
    • interfaceType handles .vision inside the switch, which clears the existing "switch must be exhaustive" warning.
    • keyboardDismissMode in the debugger's paywall picker is now guarded with #if !os(visionOS). It was already breaking the visionOS build on develop.

Customer Center and tvOS leftovers (after merging develop)

  • Removed the iOS 14/15 availability checks that came in with the Customer Center. DependencyContainer now stores the Customer Center manager as its real type instead of Any?.
  • Removed os(tvOS) from #if conditions and tvOS from availability lists, since tvOS is no longer declared.
  • The storeKitVersion doc no longer says the SDK falls back to StoreKit 1.

Using iOS 15 and Swift 6.2 APIs (folded in from #536). Tests went in first and passed against the old code:

  • Core Data: counting and the last-placement lookup use iOS 15's async perform, removing five hand-written async wrappers. The lookup now reads the placement's date on the right queue; before, it read it from another thread, which Core Data doesn't allow. New CoreDataManagerComputedPropertyTests cover it.
  • Dev server probe: uses URLSession.data(for:). New DevServerLocatorURLSessionTests run the real loader against a stub that only answers a made-up host.
  • Windows: activeWindow uses UIWindowScene.keyWindow; the test mode alerts and UIWindow.isLandscape no longer use the deprecated UIApplication.windows.
  • Review request: with no window of its own, the paywall asks in the active scene instead of calling the scene-less SKStoreReviewController.requestReview() (deprecated in iOS 14).
  • Superscript: imported with internal import, so it's no longer part of SuperwallKit's interface.
  • Version reporting: removed the Swift and compiler version branches below 6.2. A new test pins what they can still return.
  • The window lookups, review request and internal import can't be unit tested because the test target has no host app; the build is the check for those.

Still deprecated, left for later: the debugger's buttons use titleEdgeInsets, contentEdgeInsets and adjustsImageWhenHighlighted (iOS 15). Moving them to UIButton.Configuration changes how they draw, so it needs a visual check. Cache.swift uses NSKeyedUnarchiver.unarchiveObject (iOS 12); replacing it means listing which classes the cache may decode, which should be tested against data saved by the current SDK.

Release: version bumped to 4.18.0, with a CHANGELOG "Breaking Changes" entry and a toolchain section in CLAUDE.md, including a rule not to reintroduce these checks.

Usage data behind the decision (Superwall events, last 24h, iOS apps on SDK 4.10.6+, grouped by the compilerVersion device attribute):

  • Swift 6.2 or later (Xcode 26+): about 1,650+ apps and about 19M users.
  • Swift 6.0–6.1 (Xcode 16): 173 apps and about 43k users. These are older builds already installed on phones, and they can't ship an update without moving to Xcode 26.
  • iOS versions below 15 account for about 0.005% of users.

Checklist

  • All unit tests pass. (1,091 tests in 110 suites, on an iOS 27 simulator. Note: scripts/test.sh targets an "iPhone 16" simulator, which fails to launch because the test target defaults to iOS 27. That was already the case before this PR.)
  • All UI tests pass.
  • Demo project builds and runs on iOS. (The SDK package builds for iOS Simulator; I didn't run the demo app.)
  • Demo project builds and runs on Mac Catalyst. (The SDK package builds for Mac Catalyst; I didn't run the demo app.)
  • Demo project builds and runs on visionOS. (The SDK package builds for visionOS Simulator; I didn't run the demo app.)
  • I added/updated tests or detailed why my change isn't tested. (This only changes build configuration and removes branches that can no longer run; the existing suite covers the remaining paths.)
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues. (The 2 remaining warnings were already there.)
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because a rejected Customer Center presentation can permanently block all later Customer Center presentations and retain the supplied delegate.

Summary

This PR raises the minimum supported toolchain and deployment targets, removes obsolete availability paths, adopts newer iOS APIs, and incorporates a substantial Customer Center feature set.

  • Requires Swift 6.2/Xcode 26 while retaining Swift 5 language mode.
  • Raises the iOS, macOS, and watchOS deployment floors and explicitly drops tvOS support.
  • Adds Customer Center presentation, configuration, navigation, purchase-management actions, analytics, localization, documentation, and tests.
  • Modernizes Core Data, URLSession, active-window, review-request, and platform-specific code.
  • One presentation-lifecycle failure can leave the Customer Center manager permanently stuck after UIKit rejects a presentation.

Diagram

sequenceDiagram
  participant Host
  participant Manager as CustomerCenterManager
  participant UIKit
  participant Controller as CustomerCenterViewController

  Host->>Manager: presentCustomerCenter(...)
  Manager->>Manager: retain delegate and controller
  Manager->>UIKit: presenter.present(controller)
  alt Presentation succeeds
    UIKit->>Controller: presentation lifecycle
    Controller->>Manager: onDismiss
    Manager->>Manager: clear controller and delegate
  else Presentation rejected
    UIKit-->>Manager: no completion or error callback
    Note over Manager: controller and delegate remain retained
    Host->>Manager: presentCustomerCenter(...)
    Manager-->>Host: rejected as already presented
  end
Loading

Reviews (3) · Last reviewed commit: "Make the compiler version test fail on "..." · Reviewed by Greptile

yusuftor and others added 2 commits September 23, 2026 14:59
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@yusuftor yusuftor added the docs-required Ships a customer-facing change that needs a superwall/docs update label Sep 23, 2026
@yusuftor

Copy link
Copy Markdown
Collaborator Author

📚 Docs required

What changed for customers: SuperwallKit 4.18.0 needs iOS 15+ (was iOS 13) and Xcode 26 / Swift 6.2 to build (was swift-tools 5.5). SwiftPM also needs macOS 12+ and watchOS 8+. On every supported OS, storeKitVersion now defaults to StoreKit 2.

Why this needs docs: Customers set their deployment target and toolchain against these supported versions (rule 6), and the new floor is a prerequisite they must meet before upgrading (rule 4). The docs also still describe a StoreKit 1 fallback on older iOS versions, and that default has changed (rule 3).

Coverage today:

Without a docs page this change also gets no changelog entry: superwall/docs publishes a daily GitHub Release that superwall.com renders at /changelog, and every entry must link to a live docs page. Undocumented means invisible to customers.

Prompt for the docs agent. Run in superwall/docs
Document a change that shipped in superwall/Superwall-iOS#532: raise iOS SDK minimums to iOS 15 / Xcode 26 (SuperwallKit 4.18.0).

What shipped, in customer terms:
SuperwallKit 4.18.0 raises the minimum deployment target from iOS 13 to iOS 15, for both SwiftPM and CocoaPods.
Building it now needs Xcode 26 or later, because the package manifest uses swift-tools-version 6.2. Apple has required the iOS 26 SDK for App Store uploads since April 28, 2026.
The package stays in Swift 5 language mode, so customers do not have to adopt Swift 6 strict concurrency.
The SwiftPM minimums for other platforms are now macOS 12 and watchOS 8.
The public API is unchanged. Because iOS 13/14 are no longer supported, the default `storeKitVersion` is now always StoreKit 2. There is no StoreKit 1 fallback on older OS versions any more, though customers can still opt in to `.storeKit1` explicitly.

Setup or prerequisites a customer must complete:
- The app's deployment target must be iOS 15.0 or higher. For CocoaPods, that means `platform :ios, '15.0'` or higher in the Podfile.
- Build with Xcode 26 or later.
- Apps that must keep supporting iOS 13/14 stay on SuperwallKit 4.17.x.

Where it belongs:
- content/docs/ios/quickstart/install.mdx: add a "Requirements" section near the top, after Overview. List iOS 15.0+, Xcode 26+ (Swift 6.2 toolchain), and for SwiftPM macOS 12+ / watchOS 8+. Note that 4.18.0 raised these minimums and that apps needing iOS 13/14 should pin 4.17.x.
- content/docs/ios/sdk-reference/SuperwallOptions.mdx:
  - L15: remove the "falls back to StoreKit 1 on older versions" wording.
  - L52: change the storeKitVersion default to "StoreKit 2".
  - Also check the ~L212 example comment "for better performance on iOS 15+".
- content/docs/ios/guides/migrations/migrating-to-v4.mdx L34: optionally clarify that "on iOS 15+" now covers every supported OS as of 4.18.0.
- content/docs/ios/changelog.mdx: add the 4.18.0 entry, if it is not already synced from the SDK CHANGELOG.

Source of truth. Read these before writing:
- superwall/Superwall-iOS#532 diff: gh pr diff 532 --repo superwall/Superwall-iOS --patch
- Key files: Package.swift, SuperwallKit.podspec, CHANGELOG.md, Sources/SuperwallKit/Config/Options/SuperwallOptions.swift, README.md

Constraints:
- Match the voice and structure of neighbouring pages under content/docs/**.
- Use the Fumadocs TypeTable for parameters and types; never <ParamTable>.
- If you add a page, add it to the folder's meta.json.
- Run `bun run build:cf` and `bun test` before opening the PR.
- Open a PR against superwall/docs referencing superwall/Superwall-iOS#532. Do not deploy.

Flagged by the docs-required skill. If this is wrong, remove the label and say why in a reply so the skill's calibration set can be corrected.

Comment thread Package.swift
Comment thread Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The code cleanup is thorough and correct — a full sweep of Sources/ finds no remaining @available/#available below the new floor. Two things to settle before merging: the version number chosen for a breaking deployment-target raise, and a dead-code leftover in the Mac Catalyst camera path.

Reviewed changes — both commits on chore/min-ios15-swift62, covering the manifest/toolchain bump and the 71-file availability cleanup.

  • Toolchain floor — swift-tools-version 5.5 → 6.2 with swiftLanguageModes: [.v5] pinned, so strict concurrency stays off; the Superscript dependency moves to the exact: label form required by the 6.2 manifest API.
  • Platform floor — iOS 13 → 15, macOS 10.12 → 12, watchOS 6.2 → 8, kept in sync across Package.swift, SuperwallKit.podspec, project.yml and project.pbxproj.
  • Compiler-gate removal — #if compiler(...) checks below 6.2, the swift(<5.7) Any-boxing workaround in SK2StoreProduct, and xcode12.py with its // ignore-xcode-12 markers are gone; compiler(>=6.3.2) gates for the iOS 26.4 StoreKit APIs are correctly retained.
  • Availability cleanup — every @available/#available at or below the new floor, plus the fallback branches only reachable on older OSes: StoreKit 1 default on iOS 13/14, the SecTrustEvaluate receipt path, pre-iOS-14 photo permissions, and the Test Mode action-sheet fallbacks for UIMenu buttons.
  • Small fixes — interfaceType handles .vision inside the switch, and keyboardDismissMode in the debugger picker is now #if !os(visionOS)-guarded.
  • Release plumbing — 4.17.0 → 4.18.0 across Constants.swift, the podspec and CHANGELOG.md, plus a new "Minimum Toolchain and Platforms" section in CLAUDE.md.

I verified the manifest parses (swift package dump-package reports ios 15.0 / macos 12.0 / watchos 8.0, swiftLanguageVersions: ["5"], toolsVersion 6.2.0) and that no CI workflow pins an Xcode below 26 — every macOS job is macos-26 with latest-stable.

⚠️ A breaking deployment-target raise is shipping as a minor version

4.17.0 → 4.18.0 for a change whose own CHANGELOG.md entry is filed under "### Breaking Changes". SwiftPM's default requirement is .upToNextMajor, so every integrator pinned at from: "4.x" resolves into this release automatically — and any of them still targeting iOS 13 or 14 gets a hard resolution failure (requires minimum platform version 15.0) from a package update they didn't ask for. A major tag is the mechanism that stops that. Repo precedent agrees: the last minimum-iOS raise shipped in 3.0.0, also under "### Breaking Changes".

Technical details
# Version number for the iOS 15 / Xcode 26 floor

## Affected sites
- `Sources/SuperwallKit/Misc/Constants.swift:21` — `4.18.0`
- `SuperwallKit.podspec:4` — `s.version = "4.18.0"`
- `CHANGELOG.md:5-9` — `## 4.18.0` with a `### Breaking Changes` heading

## Context already verified
- `master` and `develop` both carried `4.17.0` before this PR, so per `CLAUDE.md`
  opening a new version section (rather than appending to a staged one) was correct.
  The only question is which number.
- `CHANGELOG.md:1368` — "Sets the minimum iOS version to iOS 13." shipped in `3.0.0`.

## Required outcome
- A deliberate, recorded decision on major vs minor. If `5.0.0`, bump all three
  files together per `CLAUDE.md`. If `4.18.0` stands, say in the changelog entry
  why a minor was chosen so integrators reading it understand the resolution
  failure they may hit.

## Open questions for the human
- Is there a separate v5 milestone this would collide with?
- Does the release process publish a migration note, and does the online
  documentation's stated minimum get updated alongside the tag? The PR checklist
  leaves "I have updated the SDK documentation as well as the online docs" unchecked.

ℹ️ One iOS 13 fallback survived the sweep, outside the diff

Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift still declares locationManager(_:didChangeAuthorization:) — the pre-iOS 14 CoreLocation delegate selector — alongside its doc comments "Implements both iOS 14+ and iOS 13 delegate methods dynamically" and "iOS 13 and earlier delegate method". At an iOS 15 floor CLLocationManager only ever dispatches locationManagerDidChangeAuthorization(_:), so the older selector is unreachable. Worth noting that the file's header comment explains these @objc selectors are deliberately present in the binary's Objective-C metadata, so this is a considered removal rather than a blind delete.

Technical details
# Residual pre-iOS 14 CoreLocation delegate path

## Affected sites
- `Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift:11`
  — class doc comment "Implements both iOS 14+ and iOS 13 delegate methods dynamically"
- `Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift:37-45`
  — `/// iOS 13 and earlier delegate method` + the `#if !os(visionOS)` guarded
    `@objc func locationManager(_:didChangeAuthorization:)`
- `Sources/SuperwallKit/Permissions/Handlers/Location/LocationPermissionDelegate.swift:49`
  — comment "Try instance property first (iOS 14+)", whose "first" framing implies a
    fallback that no longer needs to exist

## Required outcome
- Either drop the iOS 13 selector and realign the comments to the iOS 15 floor, or
  leave it and record why (the header comment's `scan-privacy-signatures.sh` rationale
  is about which selectors are *acceptable* to expose, not about which are still needed).

## Open questions for the human
- Does removing an `@objc` selector from the binary's metadata interact with the
  privacy-signature scanning this file's header describes?

ℹ️ Nitpicks

  • Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SK2PriceFormatRoundingTests.swift:23-25 — the comment still says "SuperwallKit ships with an iOS 13 minimum so we guard at runtime" after the guard was removed.
  • Tests/SuperwallKitTests/StoreKit/Products/SK2StoreProductCyclesTests.swift:17-19 — "each test guards with #available before calling it" no longer describes the tests.
  • Sources/SuperwallKit/StoreKit/Transactions/Purchasing/PurchasingCoordinator.swift:69 — "If on iOS 15+, try and get latest transaction using SK2" is now unconditional.
  • Sources/SuperwallKit/Misc/Extensions/SystemInfo+NotificationName.swift:38 — the dyld-crash workaround comment says "watchOS 7.0..<9.0"; the reachable range is now 8.0..<9.0.
  • Tests/SuperwallKitTests/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandlerDelegateMock.swift:29-39 — a #if compiler(>=6.0) with a dead #else branch escaped the compiler-gate sweep, which the new CLAUDE.md rule now forbids.

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

Comment thread Sources/SuperwallKit/Permissions/Handlers/PermissionHandler+Camera.swift Outdated
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 <noreply@anthropic.com>
@yusuftor

Copy link
Copy Markdown
Collaborator Author

Re the version question in the Pullfrog review: 4.18.0 is intentional. Every App Store Connect upload already needs Xcode 26 / the iOS 26 SDK, so no shipping app can take this release without already meeting its minimums. Apps still on iOS 13/14 can stay on 4.17.x.

The other review findings (tvOS floor, dead Catalyst camera code) are fixed in 8fc6fce, which also adds a visionOS build to build-platforms.yml.

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 <noreply@anthropic.com>
@yusuftor

Copy link
Copy Markdown
Collaborator Author

Addressed the rest of the Pullfrog review in 6e264da:

  • Version: 4.18.0 stays. The changelog entry now explains why it's a minor release, and tells apps that still support iOS 13/14 to pin 4.17.x (.upToNextMinor(from: "4.17.0") in SwiftPM, ~> 4.17.0 in CocoaPods).
  • iOS 13 location callback: removed locationManager(_:didChangeAuthorization:) and its test, since CLLocationManager only dispatches locationManagerDidChangeAuthorization(_:) on iOS 15+. The class doc comment and the scan-privacy-signatures.sh note now name only the remaining selector. Removing a selector only takes a name out of the binary's metadata, so the privacy scan isn't affected.
  • Nitpicks: removed the stale comments in SK2PriceFormatRoundingTests/SK2StoreProductCyclesTests and updated the ones in PurchasingCoordinator and the watchOS range, removed the test-only #if compiler(>=6.0) in FakeWebView, and updated two other comments that still referred to iOS 14.

All 1,090 tests pass. On the previous push, CI's Mac Catalyst and visionOS builds both passed.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No blocking issues — the prior review's camera dead-code comment is fully addressed and CI is green on all three platform jobs. One observation on the tvOS floor inline.

Reviewed changes — the single new commit 8fc6fce since the prior pullfrog review at 4136dc8, a 5-file response to review feedback.

  • Collapsed the Mac Catalyst camera conditionals — PermissionHandler+Camera.swift now has no #if targetEnvironment(macCatalyst) at all; the unreachable return .unsupported and the empty #if/#endif pair are both gone, exactly as suggested.
  • Declared a tvOS 15 floor — .tvOS(.v15) in Package.swift, echoed in the CHANGELOG.md breaking-changes entry and the CLAUDE.md toolchain section.
  • Added a build-visionos CI job — macos-26 + latest-stable, a conditional xcodebuild -downloadPlatform visionOS, then rm -r SuperwallKit.xcodeproj so the build goes through Package.swift (the xcodegen project is iOS-only). Package.swift was also added to the workflow's push: paths filter.

I confirmed build-visionos, build-maccatalyst and Package-SwiftLint all pass on 8fc6fce, which also proves the libcel xcframework behind the Superscript dependency ships visionOS slices.

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

Comment thread Package.swift Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No new issues in this delta — every remaining item from the previous review is addressed. One changelog-prose nitpick below.

Reviewed changes — the single new commit 6e264da since the prior pullfrog review at 8fc6fce, an 11-file cleanup pass responding to review feedback.

  • Dropped the pre-iOS 14 CoreLocation selector — LocationPermissionDelegate.locationManager(_:didChangeAuthorization:), its test, and the class/scan-privacy-signatures.sh notes that described two selectors now describe one. At the iOS 15 floor CLLocationManager only ever dispatches locationManagerDidChangeAuthorization(_:), and no reference to the old selector survives anywhere in Sources/, Tests/ or scripts/.
  • Removed the last compiler gate below the floor — PaywallMessageHandlerDelegateMock's #if compiler(>=6.0) and its dead #else branch are gone, keeping the @MainActor completion-handler signature.
  • Refreshed six stale comments — AttributionFetcher (pre-14.5 IDFA/ATT clause), DebugPaywallPickerViewController ("on iOS 14+"), SystemInfo+NotificationName ("watchOS 7.0..<9.0" → "8.0..<9.0"), PurchasingCoordinator ("If on iOS 15+"), SK2StoreProductCyclesTests and SK2PriceFormatRoundingTests (both described #available guards that no longer exist).
  • Recorded the minor-version decision in the changelog — CHANGELOG.md:10 now explains why 4.18.0 rather than 5.0.0 and tells iOS 13/14 apps to pin 4.17.x. I verified both forms resolve the intended range: .upToNextMinor(from: "4.17.0") and ~> 4.17.0 each give 4.17.0..<4.18.0.

MockLocationManager is still exercised by the five surviving location tests, so the test-file deletion left nothing dangling.

ℹ️ Nitpicks

  • CHANGELOG.md:10 — "every App Store Connect upload already needs Xcode 26, so any app that can ship an update already meets the new Xcode requirement" argues for the toolchain bump, but the resolution failure described in the very next sentence comes from the deployment-target bump, which an Xcode 26 app can still fail. The escape-hatch guidance that follows is accurate and complete; only the lead-in reasoning skips a step.

Pullfrog  | Fix it ➔ | View workflow run | Using Claude Opus | 𝕏

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No blocking issues. The merge resolved cleanly. It did bring in Customer Center code that re-adds the kind of availability checks this PR sets out to remove.

Reviewed changes: the single new commit 542f143 since the prior pullfrog review at 6e264da, a merge of develop that brought in the Customer Center feature, which develop had staged under its own 4.18.0.

  • Merged the two 4.18.0 changelog sections: one ## 4.18.0 heading, "Breaking Changes" first, then the Customer Center Enhancements and Fixes. The Customer Center entry's "Requires iOS 15+." was dropped because the SDK floor now makes it redundant.
  • Kept the version consistent: Constants.swift, the podspec and CHANGELOG.md all read 4.18.0, so nothing needed bumping again.

build-visionos and both SwiftLint jobs are green on 542f143, so the merged Customer Center code builds under the new manifest. run-tests and build-maccatalyst were still pending when I wrote this.

ℹ️ The merge brings back sub-floor availability checks via Customer Center

The branch no longer matches the PR description ("every @available/#available check at or below iOS 15 … removed") or the new CLAUDE.md rule. develop's Customer Center code adds about 70 @available(iOS 15.0, *) / #available(iOS 15.0, *) checks under Sources/, one @available(iOS 14.0, *), and 94 more in tests. None of these lines are in the PR diff because they live on the base branch, so they can't be anchored inline. At the new floor they're all no-ops, and DependencyContainer carries an Any? boxing workaround with the same shape as the swift(<5.7) hack this PR deleted from SK2StoreProduct.

Technical details
# Customer Center carries availability checks below the iOS 15 floor

## Affected sites
- `Sources/SuperwallKit/Dependencies/DependencyContainer.swift:51-71` — `private var _customerCenterManager: Any?` plus an `@available(iOS 15.0, *)` typed accessor that casts it back; the comment justifies it purely by availability, which the floor now guarantees
- `Sources/SuperwallKit/Superwall+CustomerCenter.swift:25,53,70` — `@available(iOS 15.0, *)` on the public `presentCustomerCenter` / `dismissCustomerCenter` / ObjC entry points
- `Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterDependencies.swift:87,96` — `if #available(iOS 15.0, *), let …` conditions
- `Sources/SuperwallKit/CustomerCenter/Logic/AppStoreVersionLookup.swift:76-79` — `if #available(iOS 15.0, *) { … }` followed by an unreachable `return nil`
- `Sources/SuperwallKit/CustomerCenter/Models/CustomerCenterConfiguration+Appearance.swift:61` — `@available(iOS 14.0, *)`
- ~30 other files under `Sources/SuperwallKit/CustomerCenter/**` with type-level `@available(iOS 15.0, *)`
- 11 files under `Tests/SuperwallKitTests/CustomerCenter/**` (94 annotations)

## Required outcome
- Either sweep these in this PR so the branch matches its description and the `CLAUDE.md` rule, or explicitly defer the sweep to a follow-up. Checks for iOS 16 and later (e.g. `AppStoreVersionLookup.swift:69`, `CountryCode.swift:23`, the iOS 17 ones in `CustomerCenterSheets.swift` / `CustomerCenterViewModel.swift`) stay.

## Suggested approach (optional)
- `DependencyContainer`: type the backing store as `CustomerCenterManager?` and drop the cast and the availability comment.
- Drop the `#available(iOS 15.0, *)` clause from compound conditions, and collapse `if #available(iOS 15.0, *) { X }; return nil` to `X`.

## Open questions for the human
- Sweep here, or in a follow-up? The Customer Center PR is fresh on `develop`, so a separate commit may be easier to review.

ℹ️ Nitpicks

  • Sources/SuperwallKit/Config/Options/SuperwallOptions.swift:129-130: the public doc on storeKitVersion still says "StoreKit 2 by default if the app is running on iOS 15+, otherwise it will fallback to StoreKit 1". The default has been unconditionally StoreKit 2 since 4136dc8. This comment predates this delta, but it's customer-facing and was missed by earlier sweeps.

Pullfrog  | Fix it ➔ | View workflow run | Using Claude Opus | 𝕏

yusuftor and others added 2 commits September 29, 2026 16:48
Also stop saying the SDK falls back to StoreKit 1 below iOS 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found. Both open items from earlier reviews are fixed, and the branch matches its description again.

Reviewed changes: the two commits since the prior pullfrog review at 542f143.

  • Removed the Customer Center availability checks below iOS 15: every @available(iOS 15.0, *) / #available(iOS 15.0, *) and the one @available(iOS 14.0, *) is gone from Sources/SuperwallKit/CustomerCenter/**, Superwall+CustomerCenter.swift and the 11 Customer Center test files. A grep of Sources/ and Tests/ for checks at or below the floor now only finds #available(watchOS 9, *), which is above it.
  • Simplified the code those checks forced: DependencyContainer._customerCenterManager is typed as CustomerCenterManager? now, not Any? with a cast. currentStorefrontCountryCode() is a single return with no dead return nil. The two compound if #available(...), let ... conditions in ProductDisplayInfo.init keep only their optional bindings. The @available(swift, obsoleted: 1.0) on the Objective-C entry point stays, which is correct. Checks for iOS 16 and 17 are unchanged.
  • Fixed the public storeKitVersion doc: it no longer says the SDK falls back to StoreKit 1.
  • Removed tvOS: .tvOS(.v15) is out of Package.swift and tvOS is out of the changelog entry. CLAUDE.md now says tvOS isn't supported and gives the reason.

At a37ca1f, build-maccatalyst and Package-SwiftLint pass. build-visionos and run-tests were still pending when I wrote this.

ℹ️ Nitpicks

  • Sources/SuperwallKit/Documentation.docc/CustomerCenter.md:241: the Limitations section still says "Requires iOS 15.0+. On earlier versions, presentation calls are unavailable at compile time." The Customer Center APIs have no availability attributes now, and the SDK can't be built for earlier versions at all, so the second sentence is stale.
  • Sources/SuperwallKit/CustomerCenter/ViewModel/CustomerCenterDependencies.swift:282: "iOS 15 — the Customer Center's own floor" should now say the SDK's floor. The reasoning around it still holds.

Pullfrog  | View workflow run | Using Claude Opus | 𝕏

yusuftor and others added 2 commits September 29, 2026 16:56
Pins current behaviour before moving these onto iOS 15 and Swift 6.2 APIs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@maple-review-bot

maple-review-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Note

A newer push replaced dd25776 before its review finished. The latest commit is reviewed in a new comment.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No issues in the source changes. There's one small suggestion inline about a test assertion that's looser than it looks.

Reviewed changes: I reviewed the three commits since the prior pullfrog review at a37ca1f. They switch to iOS 15 and Swift 6.2 APIs now that those are the floor, and add tests for the code paths that changed.

  • Switched Core Data reads to async perform: CoreDataStack.count(for:) and getLastSavedPlacementDate(name:before:) now return values through the iOS 15 async NSManagedObjectContext.perform, so the continuation wrappers in CoreDataManager are gone. The new CoreDataManagerComputedPropertyTests check the before: exclusion against a fresh in-memory stack in each test.
  • Moved the dev server loader to URLSession.data(for:): it replaces the hand-written continuation. The function is now internal so the new DevServerLocatorURLSessionTests can exercise it through a URLProtocol stub that only answers one host.
  • Dropped deprecated window APIs: activeWindow uses windowScene.keyWindow, UIWindow.isLandscape reads activeWindowScene instead of UIApplication.windows.first, and Test Mode uses the shared UIViewController.topMostViewController instead of its own key-window helper.
  • Made the in-app review request always scene-based: it falls back from view.window?.windowScene to activeWindowScene, and the scene-less SKStoreReviewController.requestReview() is gone.
  • Cut the version-reporting ladders back to the floor: currentSwiftVersion() returns "6.2" or "5.10", which is what a 6.x compiler reports in Swift 5 language mode. currentCompilerVersion() stops at 6.2.
  • Made the Superscript imports internal: CELEvaluator.swift and EvaluationContext.swift are the only files in Sources/ that import it, so changing both to internal import can't trigger a mixed-access-level warning. HiddenListener and PaddingListener now import Combine explicitly.

At dd25776, build-maccatalyst and Package-SwiftLint pass. build-visionos and run-tests were still pending when I wrote this.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5.5 | 𝕏

Comment thread Tests/SuperwallKitTests/Network/DeviceHelperTests.swift
Also reword two test comments that still talked about iOS 14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maple-review-bot

maple-review-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Note

A newer push replaced 572f24d before its review finished. The latest commit is reviewed in a new comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maple-review-bot

maple-review-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 4/10 · risky as written
Two scene-less app regressions remain untested; the removed xcode12.py diff was not read.
quality 80/100 · 2 warnings · tests partial · risk high

Warning

This review ended early; what follows is what it established.

Raises the minimums to iOS 15 and Swift 6.2, removes compatibility branches, and adopts newer async and scene APIs. The scene-based replacements regress review requests and test transactions in apps without scenes; fix those before merging.

  • Package.swift requires Swift 6.2 while retaining Swift 5 language mode.
  • CoreDataStack returns placement dates on its context queue.
  • DevServerLocator uses the async URLSession loader.

Findings

🟠 Warning · F1 · requestReview drops requests from apps without scenes

correctness · Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift:1442-1452

A UIKit app using the app-delegate window lifecycle can present the controller returned by getPaywall, but neither its window nor activeWindowScene supplies a scene. This branch skips the review API while still calling trackReviewRequest; the removed scene-less fallback handled these apps.

Keep `SKStoreReviewController.requestReview()` as the non-visionOS fallback when neither scene lookup succeeds.
🟠 Warning · F2 · topMostViewController breaks scene-less test purchases and restores

correctness · Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift:28

UIViewController.topMostViewController reads activeWindow, which searches only connectedScenes (UIApplication+ActiveWindow.swift:17–30). In app-delegate-based apps without scenes it returns nil despite an existing key window, so purchases fail with productUnavailable and restores fail without showing either drawer; the removed helper used that key window.

Preserve a legacy key-window fallback for presenter lookup in both `handlePurchase` and `handleRestore`.
🤖 Prompt to fix all 2 findings with an AI agent
Findings from an automated review of commit 02d97c33b6c94663d042dcaad67fde6d9bf3140c. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift:1442-1452
`requestReview` drops requests from apps without scenes
A UIKit app using the app-delegate window lifecycle can present the controller returned by `getPaywall`, but neither its window nor `activeWindowScene` supplies a scene. This branch skips the review API while still calling `trackReviewRequest`; the removed scene-less fallback handled these apps.
Suggested fix: Keep `SKStoreReviewController.requestReview()` as the non-visionOS fallback when neither scene lookup succeeds.

---

F2 · Warning · correctness · Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift:28
`topMostViewController` breaks scene-less test purchases and restores
`UIViewController.topMostViewController` reads `activeWindow`, which searches only `connectedScenes` (`UIApplication+ActiveWindow.swift:17–30`). In app-delegate-based apps without scenes it returns nil despite an existing key window, so purchases fail with `productUnavailable` and restores fail without showing either drawer; the removed helper used that key window.
Suggested fix: Preserve a legacy key-window fallback for presenter lookup in both `handlePurchase` and `handleRestore`.
What was checked
  • Read platform-floor and availability changes across all delegated groups.
  • Verified Core Data returns a date rather than a managed object across queues.
  • Read loader tests covering success, HTTP errors, empty responses, and transport failures.
Files not reviewed (1)

The review ended before it read these diffs, so nothing above vouches for them.

  • xcode12.py

02d97c3 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found. The test nitpick from the last review is fixed, and the tvOS cleanup is complete.

Reviewed changes: the two commits since the prior pullfrog review at dd25776.

  • Removed the remaining tvOS references: os(tvOS) is gone from the #if conditions in AttributionFetcher, SystemInfo+NotificationName and Validation.swift. tvOS is gone from the @available/#available lists in EntitlementProcessor (including the test mock), SK2StoreProduct and ProductPurchaserSK2. @available(tvOS, unavailable) is gone from AttributionPoster.getAdServicesTokenIfNeeded. None of this changes behavior on the platforms the SDK builds for, and a repo-wide search of Swift, YAML, podspec and shell files no longer finds tvOS.
  • Updated two test comments: the comments in AttributionDeviceIdentifiersTests and LocationPermissionDelegateTests no longer refer to iOS 14 / 14.5 behavior.
  • Tightened the compiler version test: DeviceHelperTests now also checks compilerVersion != "Unknown", so the test fails if the version check falls through to its #else branch.

At 02d97c3, Package-SwiftLint passes. build-maccatalyst, build-visionos and run-tests were still pending when I wrote this.

Pullfrog  | View workflow run | Using claude-opus-5.5 | 𝕏

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 inline notes from Maple's review. The score and summary are in the review comment above.

Comment thread Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift
@greptile-apps

greptile-apps Bot commented Oct 7, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 Failed Presentation Blocks Retries Sources/SuperwallKit/CustomerCenter/CustomerCenterManager.swift:143 ▶

    If the selected presenter is detached, disappearing, or otherwise unable to present, UIKit can reject this presentation. The manager has already stored the controller and retained the delegate, but cleanup only occurs through a dismissal lifecycle that never starts. Later calls are then rejected as already presented, the delegate remains retained, and dismissCustomerCenter may never invoke its completion. Clear this state when presentation fails, or only commit it after the controller enters the presentation hierarchy.

@yusuftor
yusuftor merged commit 526ac6f into develop Oct 7, 2026
7 checks passed
@yusuftor
yusuftor deleted the chore/min-ios15-swift62 branch October 7, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-required Ships a customer-facing change that needs a superwall/docs update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant