diff --git a/CHANGELOG.md b/CHANGELOG.md index 512d6946db..c74476d519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/sup - Fixes network requests that can never succeed, such as those with an invalid API key, taking up to a minute to fail instead of failing straight away. Timeouts and server errors still retry as before. - Fixes failed network requests being reported as a decoding error rather than the HTTP error that actually occurred. +- Prevents unused App Tracking Transparency support from triggering App Store Connect tracking warnings. ## 4.16.1 diff --git a/Sources/SuperwallKit/Permissions/Handlers/Tracking/FakeTrackingManager.swift b/Sources/SuperwallKit/Permissions/Handlers/Tracking/FakeTrackingManager.swift deleted file mode 100644 index 01a00e2fef..0000000000 --- a/Sources/SuperwallKit/Permissions/Handlers/Tracking/FakeTrackingManager.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// FakeTrackingManager.swift -// SuperwallKit -// -// Created by Yusuf Tör on 13/01/2026. -// - -import Foundation - -final class FakeTrackingManager: NSObject { - /// Class property returning notDetermined - @objc static var trackingAuthorizationStatus: Int { - return FakeTrackingAuthorizationStatus.notDetermined.rawValue - } - - /// Class method for requesting authorization - @objc static func requestTrackingAuthorization( - completionHandler: @escaping (Int) -> Void - ) { - completionHandler(FakeTrackingAuthorizationStatus.notDetermined.rawValue) - } -} diff --git a/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift b/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift index 9f193e7947..b8223237d1 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/Tracking/PermissionsHandler+Tracking.swift @@ -44,7 +44,7 @@ extension PermissionHandler { return currentStatus } - let status = await proxy.requestTrackingAuthorization() + let status = await proxy.requestAuthorization() let permissionStatus = status.toTrackingPermissionStatus return permissionStatus diff --git a/Sources/SuperwallKit/Permissions/Handlers/Tracking/TrackingManagerProxy.swift b/Sources/SuperwallKit/Permissions/Handlers/Tracking/TrackingManagerProxy.swift index 761a9fdd66..a974c59201 100644 --- a/Sources/SuperwallKit/Permissions/Handlers/Tracking/TrackingManagerProxy.swift +++ b/Sources/SuperwallKit/Permissions/Handlers/Tracking/TrackingManagerProxy.swift @@ -25,8 +25,15 @@ final class TrackingManagerProxy: NSObject { static let mangledRequestTrackingSelector = "erdhrfgGenpxvatNhgubevmngvbaJvguPbzcyrgvbaUnaqyre:" - static var trackingManagerClass: AnyClass? { - NSClassFromString(mangledTrackingManagerClassName.rot13()) + private let trackingManagerClass: AnyClass? + + init( + trackingManagerClass: AnyClass? = NSClassFromString( + TrackingManagerProxy.mangledTrackingManagerClassName.rot13() + ) + ) { + self.trackingManagerClass = trackingManagerClass + super.init() } @objc var trackingStatusSelectorName: String { @@ -43,23 +50,27 @@ final class TrackingManagerProxy: NSObject { } func trackingAuthorizationStatus() -> Int { - let cls: AnyClass = Self.trackingManagerClass ?? FakeTrackingManager.self + guard let trackingManagerClass else { + return FakeTrackingAuthorizationStatus.notDetermined.rawValue + } let sel = NSSelectorFromString(trackingStatusSelectorName) - guard let imp = Self.classIMP(cls, sel) else { + guard let imp = Self.classIMP(trackingManagerClass, sel) else { return FakeTrackingAuthorizationStatus.notDetermined.rawValue } typealias Function = @convention(c) (AnyObject, Selector) -> Int let function = unsafeBitCast(imp, to: Function.self) - return function(cls as AnyObject, sel) + return function(trackingManagerClass as AnyObject, sel) } - func requestTrackingAuthorization() async -> Int { - let cls: AnyClass = Self.trackingManagerClass ?? FakeTrackingManager.self + func requestAuthorization() async -> Int { + guard let trackingManagerClass else { + return FakeTrackingAuthorizationStatus.notDetermined.rawValue + } let sel = NSSelectorFromString(requestTrackingSelectorName) - guard let imp = Self.classIMP(cls, sel) else { + guard let imp = Self.classIMP(trackingManagerClass, sel) else { return FakeTrackingAuthorizationStatus.notDetermined.rawValue } @@ -70,7 +81,7 @@ final class TrackingManagerProxy: NSObject { typealias Function = @convention(c) (AnyObject, Selector, AnyObject) -> Void let function = unsafeBitCast(imp, to: Function.self) - function(cls as AnyObject, sel, completion as AnyObject) + function(trackingManagerClass as AnyObject, sel, completion as AnyObject) } } } diff --git a/Sources/SuperwallKit/Permissions/PermissionHandler.swift b/Sources/SuperwallKit/Permissions/PermissionHandler.swift index 9675457978..a3d8f3e5a0 100644 --- a/Sources/SuperwallKit/Permissions/PermissionHandler.swift +++ b/Sources/SuperwallKit/Permissions/PermissionHandler.swift @@ -20,7 +20,11 @@ final class PermissionHandler: PermissionHandling { static let contacts = "NSContactsUsageDescription" static let locationWhenInUse = "NSLocationWhenInUseUsageDescription" static let locationAlways = "NSLocationAlwaysAndWhenInUseUsageDescription" - static let tracking = "NSUserTrackingUsageDescription" + // ROT13("NSUserTrackingUsageDescription") + static let mangledTracking = "AFHfreGenpxvatHfntrQrfpevcgvba" + static var tracking: String { + mangledTracking.rot13() + } static let microphone = "NSMicrophoneUsageDescription" } diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index 04f8a8f561..6763f9c474 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -376,7 +376,6 @@ B0AD4A89AD5101360F93652D /* SubscriptionTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACDC7427B6340E9D86F9B0F /* SubscriptionTransaction.swift */; }; B0B0AD9409CEFE7CA8225146 /* Array+SafeRemove.swift in Sources */ = {isa = PBXBuildFile; fileRef = C855DE8F5341D67C614E3AF5 /* Array+SafeRemove.swift */; }; B0DC8290B081B74CC65E9305 /* CELEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79E9DBDDA7FEE63C15FBEAF /* CELEvaluatorTests.swift */; }; - B0F7F66E24C7AEDE460F34FE /* FakeTrackingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */; }; B10030CC414C2C341487F4B8 /* PaywallViewControllerDelegateAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990461F7A9B2F3ED62B3A628 /* PaywallViewControllerDelegateAdapter.swift */; }; B146C134ABE092C3C9ACADEC /* PurchaseResult+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A524F7AAE90E48C3B8D7E99A /* PurchaseResult+Internal.swift */; }; B15607185B9E4229C6C4F240 /* SK2StoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B96E2A1A289D96267EC0BC /* SK2StoreTransaction.swift */; }; @@ -1110,7 +1109,6 @@ D5E2D026C30691F11D4E839F /* SurveyShowCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyShowCondition.swift; sourceTree = ""; }; D6340ACDA40937ACAC66FA3D /* EntitlementPriorityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntitlementPriorityTests.swift; sourceTree = ""; }; D69BCC259F5FBE15AB02D662 /* PermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionHandler.swift; sourceTree = ""; }; - D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTrackingManager.swift; sourceTree = ""; }; D7434029CB9E4680C85D3FB6 /* PermissionHandler+Microphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PermissionHandler+Microphone.swift"; sourceTree = ""; }; D7B0C7BDA06D25D9D5A865A3 /* TestModeManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeManagerTests.swift; sourceTree = ""; }; D7E232690489360042465DB2 /* Redeemable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redeemable.swift; sourceTree = ""; }; @@ -1372,7 +1370,6 @@ isa = PBXGroup; children = ( 019FA4010BA11D24C68B8544 /* FakeTrackingAuthorizationStatus.swift */, - D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */, 20DA18E503D82C213BC5567B /* PermissionsHandler+Tracking.swift */, A78C5C57C3C92444EBAC2E38 /* TrackingManagerProxy.swift */, ); @@ -3486,7 +3483,6 @@ 11798EDE58E5D225E5414F2E /* FakeLocationAuthorizationStatus.swift in Sources */, BD6ABB9DB883BC62D2407392 /* FakeLocationManager.swift in Sources */, E9D95044254D79D2439D7B3E /* FakeTrackingAuthorizationStatus.swift in Sources */, - B0F7F66E24C7AEDE460F34FE /* FakeTrackingManager.swift in Sources */, E0F3648081AB86077201EB5D /* FeatureFlags.swift in Sources */, ED1C693657DA7FCBAE2DDDC6 /* FeatureGatingBehaviour.swift in Sources */, 767974DF68CE67AE2066E3D6 /* FileManagerMigrator.swift in Sources */, diff --git a/Tests/SuperwallKitTests/Permissions/Tracking/TrackingManagerProxyTests.swift b/Tests/SuperwallKitTests/Permissions/Tracking/TrackingManagerProxyTests.swift index 860c0cdb8d..0963847d19 100644 --- a/Tests/SuperwallKitTests/Permissions/Tracking/TrackingManagerProxyTests.swift +++ b/Tests/SuperwallKitTests/Permissions/Tracking/TrackingManagerProxyTests.swift @@ -33,23 +33,22 @@ struct TrackingManagerProxyTests { // Should return a valid ATTrackingManager.AuthorizationStatus value (0-3) #expect(status >= 0 && status <= 3) } -} -// MARK: - FakeTrackingManager Tests + @Test func trackingPlistKey_isCorrectlyDecoded() { + #expect(PermissionHandler.PlistKey.tracking == "NSUserTrackingUsageDescription") + } + + @Test func missingManager_trackingAuthorizationStatus_returnsNotDetermined() { + let proxy = TrackingManagerProxy(trackingManagerClass: nil) + let status = proxy.trackingAuthorizationStatus() -@Suite -struct FakeTrackingManagerTests { - @Test func trackingAuthorizationStatus_returnsNotDetermined() { - let status = FakeTrackingManager.trackingAuthorizationStatus #expect(status == FakeTrackingAuthorizationStatus.notDetermined.rawValue) } - @Test func requestTrackingAuthorization_callsCompletionWithNotDetermined() async { - await withCheckedContinuation { continuation in - FakeTrackingManager.requestTrackingAuthorization { status in - #expect(status == FakeTrackingAuthorizationStatus.notDetermined.rawValue) - continuation.resume() - } - } + @Test func missingManager_requestAuthorization_returnsNotDetermined() async { + let proxy = TrackingManagerProxy(trackingManagerClass: nil) + let status = await proxy.requestAuthorization() + + #expect(status == FakeTrackingAuthorizationStatus.notDetermined.rawValue) } }