From 2971c77a3090c01a319ac09183a55d98843eed99 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:26:12 +0300 Subject: [PATCH 01/10] Raise minimum iOS deployment target to 15.0 --- OptableSDK.podspec | 2 +- OptableSDK.xcodeproj/project.pbxproj | 12 ++++++------ Package.swift | 4 ++-- .../demo-ios-objc.xcodeproj/project.pbxproj | 4 ++-- .../demo-ios-swift.xcodeproj/project.pbxproj | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/OptableSDK.podspec b/OptableSDK.podspec index 799808f..aa2b380 100644 --- a/OptableSDK.podspec +++ b/OptableSDK.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |spec| spec.author = { "Optable Technologies Inc" => "support@optable.co" } spec.platform = :ios - spec.ios.deployment_target = "12.0" + spec.ios.deployment_target = "15.0" spec.swift_version = "5.0" spec.source = { :git => "https://github.com/Optable/optable-ios-sdk.git", :tag => "#{spec.version}" } diff --git a/OptableSDK.xcodeproj/project.pbxproj b/OptableSDK.xcodeproj/project.pbxproj index 686c972..12615ab 100644 --- a/OptableSDK.xcodeproj/project.pbxproj +++ b/OptableSDK.xcodeproj/project.pbxproj @@ -286,7 +286,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -347,7 +347,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -374,7 +374,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -407,7 +407,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -431,7 +431,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -452,7 +452,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Package.swift b/Package.swift index 3491467..b51a145 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "OptableSDK", - platforms: [.iOS(.v12)], + platforms: [.iOS(.v15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj index 67e4701..3af9d3b 100644 --- a/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj +++ b/demo-ios-objc/demo-ios-objc.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -413,7 +413,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj index cf29466..e5a10f0 100644 --- a/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj +++ b/demo-ios-swift/demo-ios-swift.xcodeproj/project.pbxproj @@ -348,7 +348,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -402,7 +402,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; From b4ca94ca97f5ba3825a66a2271e89adf61462fa7 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:26:12 +0300 Subject: [PATCH 02/10] Fix deprecation warnings under iOS 15 target --- Source/Core/EdgeAPI.swift | 4 +++- Source/Misc/AppTrackingTransparency.swift | 25 ++++------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 4d62e22..d3da536 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -148,7 +148,9 @@ extension EdgeAPI { private func resolveUserAgent(callback: @escaping (_ useragent: String) -> Void) { var wkUserAgent = "" let myGroup = DispatchGroup() - let window = UIApplication.shared.keyWindow + let window = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.keyWindow } + .first let webView = WKWebView(frame: UIScreen.main.bounds) webView.isHidden = true diff --git a/Source/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift index 5139c9d..c9d18ac 100644 --- a/Source/Misc/AppTrackingTransparency.swift +++ b/Source/Misc/AppTrackingTransparency.swift @@ -29,21 +29,6 @@ } #endif - // MARK: isAdvertisingTrackingEnabled - - #if DEBUG - @available(iOS, introduced: 6, deprecated: 14, - message: "Replaced by ATTrackingManager in AppTrackingTransparency.") - static var isAdvertisingTrackingEnabled_DebugOverride: Bool? - static var isAdvertisingTrackingEnabled: Bool { - isAdvertisingTrackingEnabled_DebugOverride ?? ASIdentifierManager.shared().isAdvertisingTrackingEnabled - } - #else - static var isAdvertisingTrackingEnabled: Bool { - ASIdentifierManager.shared().isAdvertisingTrackingEnabled - } - #endif - // MARK: advertisingIdentifierAvailable #if DEBUG @@ -57,14 +42,12 @@ } #endif + // The SDK requires iOS 15+, where AppTrackingTransparency is always available, so the + // pre-iOS 14 `ASIdentifierManager.isAdvertisingTrackingEnabled` fallback no longer applies. #if canImport(AppTrackingTransparency) - if #available(iOS 14, *) { - return trackingStatus == .authorized - } else { - return isAdvertisingTrackingEnabled - } + return trackingStatus == .authorized #else - return isAdvertisingTrackingEnabled + return false #endif } From f1898f90cf645429b486fb9da86c04d2b080e3db Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:26:12 +0300 Subject: [PATCH 03/10] Raise demo pod targets to iOS 15, update pods --- demo-ios-objc/Podfile | 13 +++++++++++++ demo-ios-objc/Podfile.lock | 16 ++++++++-------- demo-ios-swift/Podfile | 13 +++++++++++++ demo-ios-swift/Podfile.lock | 16 ++++++++-------- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/demo-ios-objc/Podfile b/demo-ios-objc/Podfile index db4bb9b..72b1e52 100644 --- a/demo-ios-objc/Podfile +++ b/demo-ios-objc/Podfile @@ -11,3 +11,16 @@ target 'demo-ios-objc' do pod 'PrebidMobile' end + +# Xcode 27 refuses to build targets whose deployment target is below iOS 15. Some third-party pods +# (Google-Mobile-Ads-SDK, GoogleUserMessagingPlatform, PrebidMobile) still declare older minimums, +# so raise them to the platform version declared above. +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 15.0 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end + end +end diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index 8e0556a..44d9e11 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -1,11 +1,11 @@ PODS: - - Google-Mobile-Ads-SDK (13.7.0): + - Google-Mobile-Ads-SDK (13.11.0): - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (1.0.0) - - PrebidMobile (3.3.3): - - PrebidMobile/core (= 3.3.3) - - PrebidMobile/core (3.3.3) + - PrebidMobile (3.3.4): + - PrebidMobile/core (= 3.3.4) + - PrebidMobile/core (3.3.4) DEPENDENCIES: - Google-Mobile-Ads-SDK @@ -23,11 +23,11 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: dd0ded6717d122c6a32c69dc4afcace7d276e4e4 + Google-Mobile-Ads-SDK: 09d0868abe43b9f851b7b340a245679f0eafc96c GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c - OptableSDK: d7cb5f7a211f1bac2a0c25677f4c4b31ded8ed4e - PrebidMobile: 5ded0a10d6da41d4b140c1c8b86b42c7629133fa + OptableSDK: 127f749734ded99deff764f29bc1342b20153b2b + PrebidMobile: ce3afbb56904a5e8b3f5f5aa0696474d2786a96b -PODFILE CHECKSUM: 84321d4bbdf19f72ce3dfa6f4cb2f0a9869574ad +PODFILE CHECKSUM: 788f5abee95f41b7e281003e2cab30df58eb8547 COCOAPODS: 1.16.2 diff --git a/demo-ios-swift/Podfile b/demo-ios-swift/Podfile index 5e9d12e..7dfa9a5 100644 --- a/demo-ios-swift/Podfile +++ b/demo-ios-swift/Podfile @@ -11,3 +11,16 @@ target 'demo-ios-swift' do pod 'PrebidMobile' end + +# Xcode 27 refuses to build targets whose deployment target is below iOS 15. Some third-party pods +# (Google-Mobile-Ads-SDK, GoogleUserMessagingPlatform, PrebidMobile) still declare older minimums, +# so raise them to the platform version declared above. +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 15.0 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end + end +end diff --git a/demo-ios-swift/Podfile.lock b/demo-ios-swift/Podfile.lock index 59ae941..cd81e9f 100644 --- a/demo-ios-swift/Podfile.lock +++ b/demo-ios-swift/Podfile.lock @@ -1,11 +1,11 @@ PODS: - - Google-Mobile-Ads-SDK (13.7.0): + - Google-Mobile-Ads-SDK (13.11.0): - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (1.0.0) - - PrebidMobile (3.3.3): - - PrebidMobile/core (= 3.3.3) - - PrebidMobile/core (3.3.3) + - PrebidMobile (3.3.4): + - PrebidMobile/core (= 3.3.4) + - PrebidMobile/core (3.3.4) DEPENDENCIES: - Google-Mobile-Ads-SDK @@ -23,11 +23,11 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: dd0ded6717d122c6a32c69dc4afcace7d276e4e4 + Google-Mobile-Ads-SDK: 09d0868abe43b9f851b7b340a245679f0eafc96c GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c - OptableSDK: d7cb5f7a211f1bac2a0c25677f4c4b31ded8ed4e - PrebidMobile: 5ded0a10d6da41d4b140c1c8b86b42c7629133fa + OptableSDK: 127f749734ded99deff764f29bc1342b20153b2b + PrebidMobile: ce3afbb56904a5e8b3f5f5aa0696474d2786a96b -PODFILE CHECKSUM: aca8225c99e7af1a76b3862a46118652667f5944 +PODFILE CHECKSUM: ae16575987318eda44e4e135cd54cad3debe5193 COCOAPODS: 1.16.2 From 3e6c4bfa0ab8bc8ee7258055d3592f9fa75c5f5b Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:26:12 +0300 Subject: [PATCH 04/10] Run CI on xcode-27 runner --- .github/workflows/ios-sdk-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios-sdk-ci.yml b/.github/workflows/ios-sdk-ci.yml index 251beb7..6f0d999 100644 --- a/.github/workflows/ios-sdk-ci.yml +++ b/.github/workflows/ios-sdk-ci.yml @@ -10,7 +10,7 @@ on: jobs: sdk-tests: name: "SDK Tests" - runs-on: macos-15 + runs-on: xcode-27 steps: - name: Checkout Code uses: actions/checkout@v5 @@ -30,7 +30,7 @@ jobs: build-demo-swift: name: "Build Demo Swift" - runs-on: macos-15 + runs-on: xcode-27 needs: [sdk-tests] steps: - name: Checkout Code @@ -58,7 +58,7 @@ jobs: build-demo-objc: name: "Build Demo Objective-C" - runs-on: macos-15 + runs-on: xcode-27 env: FL_OUTPUT_DIR: output needs: [sdk-tests] From f51a43212ec6f5e7ca8bc0c6569ffbfc5a4da147 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:26:12 +0300 Subject: [PATCH 05/10] Fix objc result check in Slack steps --- .github/workflows/ios-sdk-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios-sdk-ci.yml b/.github/workflows/ios-sdk-ci.yml index 6f0d999..ceb2bdd 100644 --- a/.github/workflows/ios-sdk-ci.yml +++ b/.github/workflows/ios-sdk-ci.yml @@ -92,7 +92,7 @@ jobs: steps: - name: Successful Release Post to Slack uses: slackapi/slack-github-action@v2.1.1 - if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.build-demo-swift.result == 'success' && needs.build-demo-swift.result == 'success' }} + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && needs.build-demo-swift.result == 'success' && needs.build-demo-objc.result == 'success' }} with: method: chat.postMessage token: "${{ secrets.SLACK_MESSENGER_APP_TOKEN }}" @@ -117,7 +117,7 @@ jobs: text: ":white_check_mark: *iOS SDK release:*\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - name: Build failure Post to Slack uses: slackapi/slack-github-action@v2.1.1 - if: ${{ github.event_name == 'pull_request' && (needs.build-demo-swift.result != 'success' || needs.build-demo-swift.result != 'success')}} + if: ${{ github.event_name == 'pull_request' && (needs.build-demo-swift.result != 'success' || needs.build-demo-objc.result != 'success')}} with: method: chat.postMessage token: "${{ secrets.SLACK_MESSENGER_APP_TOKEN }}" From c1642f9e47cfc51da02c1e5209ff2727d07676da Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:33:09 +0300 Subject: [PATCH 06/10] Clean up resundant comments --- Source/Misc/AppTrackingTransparency.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift index c9d18ac..9d1bc87 100644 --- a/Source/Misc/AppTrackingTransparency.swift +++ b/Source/Misc/AppTrackingTransparency.swift @@ -42,8 +42,6 @@ } #endif - // The SDK requires iOS 15+, where AppTrackingTransparency is always available, so the - // pre-iOS 14 `ASIdentifierManager.isAdvertisingTrackingEnabled` fallback no longer applies. #if canImport(AppTrackingTransparency) return trackingStatus == .authorized #else From 5a619c6382dd03cc7d6dd97d74988a0841fc9a73 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 30 Sep 2026 20:48:24 +0300 Subject: [PATCH 07/10] Remove availability checks below iOS 15 --- Source/Core/EdgeAPI.swift | 21 +--- Source/Core/OptableIdentifierEncoder.swift | 29 +---- Source/Misc/AppTrackingTransparency.swift | 119 +++++++----------- .../RangeReplaceableCollection+Compat.swift | 22 ---- Source/OptableSDK.swift | 19 +-- Tests/Integration/OptableSDKTests.swift | 4 - ...SDKHelpersIdentifiersEnrichmentTests.swift | 2 - 7 files changed, 58 insertions(+), 158 deletions(-) delete mode 100644 Source/Misc/RangeReplaceableCollection+Compat.swift diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index d3da536..b0d5900 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -118,25 +118,8 @@ extension EdgeAPI { completionHandler(data, response, error) return } - if #available(iOS 13.0, *) { - if let passport = res.value(forHTTPHeaderField: self.kPassportHeader) { - self.storage.setPassport(passport) - } - } else { - // In older versions of iOS, we have to resort searching through headers via res.allHeaderFields - // Unlike res.value(forHTTPHeaderField:...) which was introduced in iOS 13.0, allHeaderFields is - // case-sensitive, so we need to take special care to perform a case-INsensitive search: - for (key, value) in res.allHeaderFields { - if let header = key as? String { - let result: ComparisonResult = header.compare(self.kPassportHeader, options: NSString.CompareOptions.caseInsensitive) - if result == .orderedSame { - if let pp = value as? String { - self.storage.setPassport(pp) - break - } - } - } - } + if let passport = res.value(forHTTPHeaderField: self.kPassportHeader) { + self.storage.setPassport(passport) } completionHandler(data, response, error) } diff --git a/Source/Core/OptableIdentifierEncoder.swift b/Source/Core/OptableIdentifierEncoder.swift index 0590761..8a65694 100644 --- a/Source/Core/OptableIdentifierEncoder.swift +++ b/Source/Core/OptableIdentifierEncoder.swift @@ -5,11 +5,8 @@ // Copyright © 2026 Optable Technologies, Inc. All rights reserved. // -import CommonCrypto +import CryptoKit import Foundation -#if canImport(CryptoKit) - import CryptoKit -#endif // MARK: - OptableIdentifierEncoder enum OptableIdentifierEncoder { @@ -196,25 +193,9 @@ enum OptableIdentifierEncoder { } private static func sha256(data: Data) -> String { - #if canImport(CryptoKit) - if #available(iOS 13.0, *) { - return SHA256 - .hash(data: data) - .compactMap({ String(format: "%02x", $0) }) - .joined() - } - #endif - - return cchash(data) - } - - private static func cchash(_ input: Data) -> String { - var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - input.withUnsafeBytes { bytes in - _ = CC_SHA256(bytes.baseAddress, CC_LONG(input.count), &digest) - } - return digest.makeIterator().compactMap { - String(format: "%02x", $0) - }.joined() + SHA256 + .hash(data: data) + .compactMap({ String(format: "%02x", $0) }) + .joined() } } diff --git a/Source/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift index 9d1bc87..0410f5e 100644 --- a/Source/Misc/AppTrackingTransparency.swift +++ b/Source/Misc/AppTrackingTransparency.swift @@ -8,11 +8,7 @@ #if canImport(AdSupport) import AdSupport - - #if canImport(AppTrackingTransparency) - import AppTrackingTransparency - #endif - + import AppTrackingTransparency import Foundation enum ATT { @@ -42,11 +38,7 @@ } #endif - #if canImport(AppTrackingTransparency) - return trackingStatus == .authorized - #else - return false - #endif + return trackingStatus == .authorized } // MARK: attAvailable @@ -62,86 +54,69 @@ } #endif - if #available(iOS 14, *) { - return true - } else { - return false - } + return true } - #if canImport(AppTrackingTransparency) + // MARK: canAuthorize - // MARK: canAuthorize + #if DEBUG + static var canAuthorize_DebugOverride: Bool? + #endif + static var canAuthorize: Bool { #if DEBUG - @available(iOS 14, *) - static var canAuthorize_DebugOverride: Bool? + if let override = canAuthorize_DebugOverride { + return override + } #endif - static var canAuthorize: Bool { - if #available(iOS 14, *) { - #if DEBUG - if let override = canAuthorize_DebugOverride { - return override - } - #endif - - return ATTrackingManager.trackingAuthorizationStatus == .notDetermined - } else { - return false - } - } + return ATTrackingManager.trackingAuthorizationStatus == .notDetermined + } + + // MARK: trackingStatus - // MARK: trackingStatus + #if DEBUG + static var trackingStatus_DebugOverride: ATTrackingManager.AuthorizationStatus? + #endif + static var trackingStatus: ATTrackingManager.AuthorizationStatus { #if DEBUG - @available(iOS 14, *) - static var trackingStatus_DebugOverride: ATTrackingManager.AuthorizationStatus? + return trackingStatus_DebugOverride ?? ATTrackingManager.trackingAuthorizationStatus + #else + return ATTrackingManager.trackingAuthorizationStatus #endif + } - @available(iOS 14, *) - static var trackingStatus: ATTrackingManager.AuthorizationStatus { - #if DEBUG - return trackingStatus_DebugOverride ?? ATTrackingManager.trackingAuthorizationStatus - #else - return ATTrackingManager.trackingAuthorizationStatus - #endif - } + // MARK: RequestAuthorization - // MARK: RequestAuthorization - - @available(iOS 14, *) - static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) { - #if DEBUG - if let override = trackingStatus_DebugOverride { - completion?(override == .authorized) - return - } - #endif - - ATTrackingManager.requestTrackingAuthorization { status in - switch status { - case .authorized: - completion?(true) - case .denied, .notDetermined, .restricted: - completion?(false) - @unknown default: - completion?(true) - } + static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) { + #if DEBUG + if let override = trackingStatus_DebugOverride { + completion?(override == .authorized) + return } - } + #endif - @available(iOS 14, *) - @discardableResult - static func requestATTAuthorization() async -> Bool { - await withCheckedContinuation { continuation in - requestATTAuthorization { isAuthorized in - continuation.resume(returning: isAuthorized) - } + ATTrackingManager.requestTrackingAuthorization { status in + switch status { + case .authorized: + completion?(true) + case .denied, .notDetermined, .restricted: + completion?(false) + @unknown default: + completion?(true) } } + } - #endif + @discardableResult + static func requestATTAuthorization() async -> Bool { + await withCheckedContinuation { continuation in + requestATTAuthorization { isAuthorized in + continuation.resume(returning: isAuthorized) + } + } + } } #endif diff --git a/Source/Misc/RangeReplaceableCollection+Compat.swift b/Source/Misc/RangeReplaceableCollection+Compat.swift deleted file mode 100644 index 329a242..0000000 --- a/Source/Misc/RangeReplaceableCollection+Compat.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// RangeReplaceableCollection+Compat.swift -// OptableSDK -// -// Copyright © 2026 Optable Technologies, Inc. All rights reserved. -// - -import Foundation -import SwiftUI - -extension RangeReplaceableCollection where Self: MutableCollection, Index == Int { - mutating func removeCompat(atOffsets offsets: IndexSet) { - if #available(iOS 13.0, *) { - remove(atOffsets: offsets) - } else { - // Remove from highest index to lowest to avoid shifting issues - for index in offsets.sorted(by: >) { - remove(at: index) - } - } - } -} diff --git a/Source/OptableSDK.swift b/Source/OptableSDK.swift index 308213f..23b315d 100644 --- a/Source/OptableSDK.swift +++ b/Source/OptableSDK.swift @@ -101,7 +101,6 @@ public extension OptableSDK { Instead of completion callbacks, results are returned via async/await. */ - @available(iOS 13.0, *) func identify(_ ids: [OptableIdentifier]) async throws -> HTTPURLResponse { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { @@ -164,7 +163,6 @@ public extension OptableSDK { Instead of completion callbacks, results are returned via async/await. */ - @available(iOS 13.0, *) func targeting(_ ids: [OptableIdentifier]? = nil, hids: [OptableIdentifier]? = nil) async throws -> OptableTargeting { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { @@ -197,7 +195,6 @@ public extension OptableSDK { Instead of completion callbacks, results are returned via async/await. */ - @available(iOS 13.0, *) func witness(event: String, properties: [String: Any]) async throws -> HTTPURLResponse { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { @@ -249,7 +246,6 @@ public extension OptableSDK { Instead of completion callbacks, results are returned via async/await. */ - @available(iOS 13.0, *) func profile(traits: [String: Any], id: String? = nil, neighbors: [String]? = nil) async throws -> OptableTargeting { return try await withCheckedThrowingContinuation({ [unowned self] continuation in do { @@ -432,18 +428,11 @@ extension OptableSDK { ATT.advertisingIdentifier != UUID(uuid: uuid_t(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) { let systemIDFA = ATT.advertisingIdentifier.uuidString - var idfaMatchingSystemIdxs: [Int] = [] - - for idx in ids.indices { - if case let .appleIDFA(value) = ids[idx] { - if value == systemIDFA { - idfaMatchingSystemIdxs.append(idx) - } - } - } - // Remove all matching systemIDFA (deduplicate) - ids.removeCompat(atOffsets: IndexSet(idfaMatchingSystemIdxs)) + ids.removeAll { id in + if case let .appleIDFA(value) = id { return value == systemIDFA } + return false + } // Prepend all identifiers with systemIDFA ids.insert(.appleIDFA(systemIDFA), at: ids.startIndex) diff --git a/Tests/Integration/OptableSDKTests.swift b/Tests/Integration/OptableSDKTests.swift index 6dcb627..fbcfce5 100644 --- a/Tests/Integration/OptableSDKTests.swift +++ b/Tests/Integration/OptableSDKTests.swift @@ -24,7 +24,6 @@ class OptableSDKTests: XCTestCase { } // MARK: Identify - @available(iOS 13.0, *) func test_identify_async() async throws { let response = try await sdk.identify([.emailAddress("test@test.com")]) XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) @@ -52,7 +51,6 @@ class OptableSDKTests: XCTestCase { } // MARK: Target - @available(iOS 13.0, *) func test_target_async() async throws { let response = try await sdk.targeting([.emailAddress("test@test.com")]) XCTAssert(response.targetingData.keys.isEmpty == false) @@ -102,7 +100,6 @@ class OptableSDKTests: XCTestCase { } // MARK: Witness - @available(iOS 13.0, *) func test_witness_async() async throws { let response: HTTPURLResponse = try await sdk.witness(event: "test", properties: ["integration-test-witness": "integration-test-witness-value"]) XCTAssert(response.allHeaderFields.keys.contains("x-optable-visitor")) @@ -130,7 +127,6 @@ class OptableSDKTests: XCTestCase { } // MARK: Profile - @available(iOS 13.0, *) func test_profile_async() async throws { let response = try await sdk.profile(traits: ["integration-test-profile": "integration-test-profile-value"]) XCTAssert(response.targetingData.keys.isEmpty == false) diff --git a/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift b/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift index 085cb4a..ea53d53 100644 --- a/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift +++ b/Tests/Unit/OptableSDKHelpersIdentifiersEnrichmentTests.swift @@ -63,7 +63,6 @@ class OptableSDKHelpersIdentifiersEnrichmentTests: XCTestCase { } } - @available(iOS 14, *) func test_idfa_detection_enabled_enrich_system_idfa_same_as_user_idfa_prepend() { ATT.advertisingIdentifierAvailable_DebugOverride = true ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) @@ -94,7 +93,6 @@ class OptableSDKHelpersIdentifiersEnrichmentTests: XCTestCase { } } - @available(iOS 14, *) func test_idfa_detection_enabled_enrich_system_idfa_user_idfa_persist() { ATT.advertisingIdentifierAvailable_DebugOverride = true ATT.advertisingIdentifier_DebugOverride = UUID(uuidString: systemIDFA) From 9303f38d73a3cf5497dde718a2d24b1b1bd341ee Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 7 Oct 2026 10:38:57 +0300 Subject: [PATCH 08/10] Remove unused ATT helpers --- Source/Misc/AppTrackingTransparency.swift | 63 ----------------------- 1 file changed, 63 deletions(-) diff --git a/Source/Misc/AppTrackingTransparency.swift b/Source/Misc/AppTrackingTransparency.swift index 0410f5e..433b73d 100644 --- a/Source/Misc/AppTrackingTransparency.swift +++ b/Source/Misc/AppTrackingTransparency.swift @@ -41,38 +41,6 @@ return trackingStatus == .authorized } - // MARK: attAvailable - - #if DEBUG - static var attAvailable_DebugOverride: Bool? - #endif - - static var attAvailable: Bool { - #if DEBUG - if let override = attAvailable_DebugOverride { - return override - } - #endif - - return true - } - - // MARK: canAuthorize - - #if DEBUG - static var canAuthorize_DebugOverride: Bool? - #endif - - static var canAuthorize: Bool { - #if DEBUG - if let override = canAuthorize_DebugOverride { - return override - } - #endif - - return ATTrackingManager.trackingAuthorizationStatus == .notDetermined - } - // MARK: trackingStatus #if DEBUG @@ -86,37 +54,6 @@ return ATTrackingManager.trackingAuthorizationStatus #endif } - - // MARK: RequestAuthorization - - static func requestATTAuthorization(completion: ((Bool) -> Void)? = nil) { - #if DEBUG - if let override = trackingStatus_DebugOverride { - completion?(override == .authorized) - return - } - #endif - - ATTrackingManager.requestTrackingAuthorization { status in - switch status { - case .authorized: - completion?(true) - case .denied, .notDetermined, .restricted: - completion?(false) - @unknown default: - completion?(true) - } - } - } - - @discardableResult - static func requestATTAuthorization() async -> Bool { - await withCheckedContinuation { continuation in - requestATTAuthorization { isAuthorized in - continuation.resume(returning: isAuthorized) - } - } - } } #endif From 954d1e4c6abd4cc877c011666974d230cf6bb12b Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 7 Oct 2026 10:40:54 +0300 Subject: [PATCH 09/10] Use checkout@v5 in demo-objc job --- .github/workflows/ios-sdk-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-sdk-ci.yml b/.github/workflows/ios-sdk-ci.yml index ceb2bdd..63a1baf 100644 --- a/.github/workflows/ios-sdk-ci.yml +++ b/.github/workflows/ios-sdk-ci.yml @@ -64,7 +64,7 @@ jobs: needs: [sdk-tests] steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Bundle install uses: ruby/setup-ruby@v1 with: From 24bda57f7edf83e24c7a9ec435a99a8406e2b023 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Wed, 21 Oct 2026 13:44:00 -0600 Subject: [PATCH 10/10] Remove iOS 14 version qualifiers from IDFA warnings in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 15 is now the minimum deployment target, so ATT authorization is always required — the "as of iOS 14.0" and "on iOS 14.0 or above" conditionals are no longer accurate. Co-Authored-By: Claude Sonnet 4.6 --- docs/usage-objc.md | 2 +- docs/usage-swift.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage-objc.md b/docs/usage-objc.md index 4262d33..8b94b82 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -99,7 +99,7 @@ NSArray *ids = @[ Note that `error` will be set only in case of an internal SDK exception. Otherwise, any configured delegate `identifyOk` or `identifyErr` will be invoked to signal success or failure, respectively. Providing an empty `ppid` as in the above example simply will not send any `ppid`. -> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `YES` in calls to `identify` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. +> :warning: **IDFA requires user authorization**: Apple requires permission to use IDFA via the [AppTrackingTransparency](https://developer.apple.com/app-store/user-privacy-and-data-use/) framework. If you intend to set `aaid` to `YES` in calls to `identify`, the SDK will automatically trigger the ATT user prompt before sending the IDFA value to your DCN. We recommend configuring the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist` to customize the prompt. ### Profile API diff --git a/docs/usage-swift.md b/docs/usage-swift.md index 2a66bbe..d2f1e1c 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -89,7 +89,7 @@ The SDK `identify()` method will asynchronously connect to the configured DCN an Since the `sendIDFA` value provided to `identify()` via the `aaid` (Apple Advertising ID or IDFA) boolean parameter is `true`, the SDK will attempt to fetch and send the Apple IDFA in the call to `identify` too, unless the user has turned on "Limit ad tracking" in their iOS device privacy settings. -> :warning: **As of iOS 14.0**, Apple has introduced [additional restrictions on IDFA](https://developer.apple.com/app-store/user-privacy-and-data-use/) which will require prompting users to request permission to use IDFA. Therefore, if you intend to set `aaid` to `true` in calls to `identify()` on iOS 14.0 or above, you should expect that the SDK will automatically trigger a user prompt via the `AppTrackingTransparency` framework before it is permitted to send the IDFA value to your DCN. Additionally, we recommend that you ensure to configure the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist`, as it enables you to customize some elements of the resulting user prompt. +> :warning: **IDFA requires user authorization**: Apple requires permission to use IDFA via the [AppTrackingTransparency](https://developer.apple.com/app-store/user-privacy-and-data-use/) framework. If you intend to set `aaid` to `true` in calls to `identify()`, the SDK will automatically trigger the ATT user prompt before sending the IDFA value to your DCN. We recommend configuring the _Privacy - Tracking Usage Description_ attribute string in your application's `Info.plist` to customize the prompt. The frequency of invocation of `identify` is up to you, however for optimal identity resolution we recommended to call the `identify()` method on your SDK instance every time you authenticate a user, as well as periodically, such as for example once every 15 to 60 minutes while the application is being actively used and an internet connection is available.