From 80addac83069a8f5ac564c11d3e7a73334cdb860 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:03:32 -0700 Subject: [PATCH 1/6] set NSUserTrackingUsageDescription in plist --- Kickstarter-iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Kickstarter-iOS/Info.plist b/Kickstarter-iOS/Info.plist index cc301f760b..3b2828b51e 100644 --- a/Kickstarter-iOS/Info.plist +++ b/Kickstarter-iOS/Info.plist @@ -2,6 +2,8 @@ + NSUserTrackingUsageDescription + We use personal data to provide a good experience on Kickstarter, and to help connect you with projects you'll love. CFBundleDevelopmentRegion en CFBundleExecutable From 78972ee03f2fc812d0cf76b0ca8581a378f1e8df Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:11:41 -0700 Subject: [PATCH 2/6] request ATTrackingAuthorization on app applicationdidFinishLaunching --- Kickstarter-iOS/AppDelegateViewModel.swift | 36 +++++++++++++++++++ .../AppDelegateViewModelTests.swift | 8 +++++ 2 files changed, 44 insertions(+) diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index 018f4073fc..a4ed6af5b3 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -1,4 +1,5 @@ import AppboyKit +import AppTrackingTransparency import KsApi import Library import Prelude @@ -18,6 +19,12 @@ public enum NotificationAuthorizationStatus { case provisional } +public enum ATTrackingAuthorizationStatus { + case authorized + case denied + case notDetermined +} + public protocol AppDelegateViewModelInputs { /// Call when the application is handed off to. func applicationContinueUserActivity(_ userActivity: NSUserActivity) -> Bool @@ -179,6 +186,9 @@ public protocol AppDelegateViewModelOutputs { /// Emits when we should register the device push token in Segment Analytics. var registerPushTokenInSegment: Signal { get } + + /// Emits when application didFinishLaunchingWithOptions. + var requestATTrackingAuthorizationStatus: Signal { get } /// Emits when our config updates with the enabled state for Semgent Analytics. var segmentIsEnabled: Signal { get } @@ -785,6 +795,10 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.brazeWillDisplayInAppMessageReturnProperty <~ self.brazeWillDisplayInAppMessageProperty.signal .skipNil() .map { _ in .displayInAppMessageNow } + + self.requestATTrackingAuthorizationStatus = self.applicationLaunchOptionsProperty.signal + .skipNil() + .map { _ in atTrackingAuthorizationStatus() } } public var inputs: AppDelegateViewModelInputs { return self } @@ -956,6 +970,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi public let pushTokenRegistrationStarted: Signal<(), Never> public let pushTokenSuccessfullyRegistered: Signal public let registerPushTokenInSegment: Signal + public let requestATTrackingAuthorizationStatus: Signal public let segmentIsEnabled: Signal public let setApplicationShortcutItems: Signal<[ShortcutItem], Never> public let showAlert: Signal @@ -965,6 +980,27 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi public let updateConfigInEnvironment: Signal } +private func atTrackingAuthorizationStatus() -> ATTrackingAuthorizationStatus { + var authorizationStatus: ATTrackingAuthorizationStatus = .notDetermined + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in + switch status { + case .notDetermined: + authorizationStatus = .notDetermined + case .authorized: + authorizationStatus = .authorized + case .denied: + authorizationStatus = .denied + default: + authorizationStatus = .denied + } + }) + } + + return authorizationStatus +} + /// Handles the deeplink route with both an id and text based name for a deeplink to categories. private func deepLinkCategories(rawParams: [String: String]) -> (Param?, Param?) { let parentCategoryParams = rawParams["parent_category_id"] diff --git a/Kickstarter-iOS/AppDelegateViewModelTests.swift b/Kickstarter-iOS/AppDelegateViewModelTests.swift index e766e0244e..9cdd49abe5 100644 --- a/Kickstarter-iOS/AppDelegateViewModelTests.swift +++ b/Kickstarter-iOS/AppDelegateViewModelTests.swift @@ -42,6 +42,7 @@ final class AppDelegateViewModelTests: TestCase { private let pushRegistrationStarted = TestObserver<(), Never>() private let pushTokenSuccessfullyRegistered = TestObserver() private let registerPushTokenInSegment = TestObserver() + private let requestATTrackingAuthorizationStatus = TestObserver() private let setApplicationShortcutItems = TestObserver<[ShortcutItem], Never>() private let segmentIsEnabled = TestObserver() private let showAlert = TestObserver() @@ -96,6 +97,7 @@ final class AppDelegateViewModelTests: TestCase { self.vm.outputs.pushTokenRegistrationStarted.observe(self.pushRegistrationStarted.observer) self.vm.outputs.pushTokenSuccessfullyRegistered.observe(self.pushTokenSuccessfullyRegistered.observer) self.vm.outputs.registerPushTokenInSegment.observe(self.registerPushTokenInSegment.observer) + self.vm.outputs.requestATTrackingAuthorizationStatus.observe(self.requestATTrackingAuthorizationStatus.observer) self.vm.outputs.setApplicationShortcutItems.observe(self.setApplicationShortcutItems.observer) self.vm.outputs.showAlert.observe(self.showAlert.observer) self.vm.outputs.segmentIsEnabled.observe(self.segmentIsEnabled.observer) @@ -3004,6 +3006,12 @@ final class AppDelegateViewModelTests: TestCase { self.updateCurrentUserInEnvironment.assertValues([user, updatedUser]) } } + + func testRequestATTrackingAuthorizationStatus_CalledOnceOnDidFinishLaunching() { + self.vm.inputs.applicationDidFinishLaunching(application: UIApplication.shared, launchOptions: nil) + + self.requestATTrackingAuthorizationStatus.assertValueCount(1) + } } private let backingForCreatorPushData: [String: Any] = [ From 00591f24db4917a8401a0407614944e9864ecd99 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:12:11 -0700 Subject: [PATCH 3/6] formatting --- Kickstarter-iOS/AppDelegateViewModel.swift | 6 +++--- Kickstarter-iOS/AppDelegateViewModelTests.swift | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index a4ed6af5b3..130b083008 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -186,7 +186,7 @@ public protocol AppDelegateViewModelOutputs { /// Emits when we should register the device push token in Segment Analytics. var registerPushTokenInSegment: Signal { get } - + /// Emits when application didFinishLaunchingWithOptions. var requestATTrackingAuthorizationStatus: Signal { get } @@ -795,7 +795,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.brazeWillDisplayInAppMessageReturnProperty <~ self.brazeWillDisplayInAppMessageProperty.signal .skipNil() .map { _ in .displayInAppMessageNow } - + self.requestATTrackingAuthorizationStatus = self.applicationLaunchOptionsProperty.signal .skipNil() .map { _ in atTrackingAuthorizationStatus() } @@ -982,7 +982,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi private func atTrackingAuthorizationStatus() -> ATTrackingAuthorizationStatus { var authorizationStatus: ATTrackingAuthorizationStatus = .notDetermined - + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in switch status { diff --git a/Kickstarter-iOS/AppDelegateViewModelTests.swift b/Kickstarter-iOS/AppDelegateViewModelTests.swift index 9cdd49abe5..108d19ada8 100644 --- a/Kickstarter-iOS/AppDelegateViewModelTests.swift +++ b/Kickstarter-iOS/AppDelegateViewModelTests.swift @@ -97,7 +97,8 @@ final class AppDelegateViewModelTests: TestCase { self.vm.outputs.pushTokenRegistrationStarted.observe(self.pushRegistrationStarted.observer) self.vm.outputs.pushTokenSuccessfullyRegistered.observe(self.pushTokenSuccessfullyRegistered.observer) self.vm.outputs.registerPushTokenInSegment.observe(self.registerPushTokenInSegment.observer) - self.vm.outputs.requestATTrackingAuthorizationStatus.observe(self.requestATTrackingAuthorizationStatus.observer) + self.vm.outputs.requestATTrackingAuthorizationStatus + .observe(self.requestATTrackingAuthorizationStatus.observer) self.vm.outputs.setApplicationShortcutItems.observe(self.setApplicationShortcutItems.observer) self.vm.outputs.showAlert.observe(self.showAlert.observer) self.vm.outputs.segmentIsEnabled.observe(self.segmentIsEnabled.observer) @@ -3006,7 +3007,7 @@ final class AppDelegateViewModelTests: TestCase { self.updateCurrentUserInEnvironment.assertValues([user, updatedUser]) } } - + func testRequestATTrackingAuthorizationStatus_CalledOnceOnDidFinishLaunching() { self.vm.inputs.applicationDidFinishLaunching(application: UIApplication.shared, launchOptions: nil) From 8ee0ac62f5c8f8bbba11c64c570f6d90d613e327 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:52:17 -0700 Subject: [PATCH 4/6] gate behind consent management dialog feature flag --- Kickstarter-iOS/AppDelegateViewModel.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index 130b083008..2d33c0c2d4 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -798,7 +798,10 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.requestATTrackingAuthorizationStatus = self.applicationLaunchOptionsProperty.signal .skipNil() - .map { _ in atTrackingAuthorizationStatus() } + .map { _ -> ATTrackingAuthorizationStatus in + guard featureConsentManagementDialogEnabled() else { return .notDetermined } + return atTrackingAuthorizationStatus() + } } public var inputs: AppDelegateViewModelInputs { return self } @@ -997,7 +1000,7 @@ private func atTrackingAuthorizationStatus() -> ATTrackingAuthorizationStatus { } }) } - + return authorizationStatus } From e166e8023f550735f95aaae75fe06c91ab383095 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Thu, 19 Jan 2023 09:24:40 -0700 Subject: [PATCH 5/6] pr feedback * ATTrackingAuthorizationStatus to its own file * Use `.ksr_debounce` on Signal instead of `asyncAfter` * Handle `restricted` and `@unknown` requestTrackingAuthorization status cases * Improve unit test --- Kickstarter-iOS/AppDelegateViewModel.swift | 35 ++++++++----------- .../AppDelegateViewModelTests.swift | 4 +++ Kickstarter.xcodeproj/project.pbxproj | 4 +++ .../ATTrackingAuthorizationStatus.swift | 6 ++++ 4 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 Library/Tracking/ATTrackingAuthorizationStatus.swift diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index 2d33c0c2d4..d067916830 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -19,12 +19,6 @@ public enum NotificationAuthorizationStatus { case provisional } -public enum ATTrackingAuthorizationStatus { - case authorized - case denied - case notDetermined -} - public protocol AppDelegateViewModelInputs { /// Call when the application is handed off to. func applicationContinueUserActivity(_ userActivity: NSUserActivity) -> Bool @@ -798,6 +792,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.requestATTrackingAuthorizationStatus = self.applicationLaunchOptionsProperty.signal .skipNil() + .ksr_debounce(.seconds(1), on: AppEnvironment.current.scheduler) .map { _ -> ATTrackingAuthorizationStatus in guard featureConsentManagementDialogEnabled() else { return .notDetermined } return atTrackingAuthorizationStatus() @@ -986,20 +981,20 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi private func atTrackingAuthorizationStatus() -> ATTrackingAuthorizationStatus { var authorizationStatus: ATTrackingAuthorizationStatus = .notDetermined - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in - switch status { - case .notDetermined: - authorizationStatus = .notDetermined - case .authorized: - authorizationStatus = .authorized - case .denied: - authorizationStatus = .denied - default: - authorizationStatus = .denied - } - }) - } + ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in + switch status { + case .notDetermined: + authorizationStatus = .notDetermined + case .authorized: + authorizationStatus = .authorized + case .denied: + authorizationStatus = .denied + case .restricted: + authorizationStatus = .restricted + @unknown default: + authorizationStatus = .notDetermined + } + }) return authorizationStatus } diff --git a/Kickstarter-iOS/AppDelegateViewModelTests.swift b/Kickstarter-iOS/AppDelegateViewModelTests.swift index 108d19ada8..e7e4d7cf65 100644 --- a/Kickstarter-iOS/AppDelegateViewModelTests.swift +++ b/Kickstarter-iOS/AppDelegateViewModelTests.swift @@ -3009,8 +3009,12 @@ final class AppDelegateViewModelTests: TestCase { } func testRequestATTrackingAuthorizationStatus_CalledOnceOnDidFinishLaunching() { + self.requestATTrackingAuthorizationStatus.assertValueCount(0) + self.vm.inputs.applicationDidFinishLaunching(application: UIApplication.shared, launchOptions: nil) + self.scheduler.advance(by: .seconds(1)) + self.requestATTrackingAuthorizationStatus.assertValueCount(1) } } diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 487b450642..a40567a1a6 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -485,6 +485,7 @@ 6067BCE9293E49AC0036ABB1 /* FacebookResetPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067BCE7293E48140036ABB1 /* FacebookResetPasswordViewController.swift */; }; 6067BCEC293E49F00036ABB1 /* FacebookResetPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067BCEA293E49CB0036ABB1 /* FacebookResetPasswordViewModel.swift */; }; 6067BCF2293FC3520036ABB1 /* FacebookResetPasswordViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6067BCEF293FC10E0036ABB1 /* FacebookResetPasswordViewModelTests.swift */; }; + 606F214429799A1200BA5CDF /* ATTrackingAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606F214229799A0000BA5CDF /* ATTrackingAuthorizationStatus.swift */; }; 608E7A5328ABDBAE00289E92 /* SetYourPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608E7A5128ABD5E700289E92 /* SetYourPasswordViewController.swift */; }; 608E7A5628ABE6CD00289E92 /* SetYourPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608E7A5428ABE27400289E92 /* SetYourPasswordViewModel.swift */; }; 60DA50EB28B689A4002E2DF1 /* SetYourPasswordViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60DA50E928B68990002E2DF1 /* SetYourPasswordViewModelTests.swift */; }; @@ -2069,6 +2070,7 @@ 6067BCE7293E48140036ABB1 /* FacebookResetPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookResetPasswordViewController.swift; sourceTree = ""; }; 6067BCEA293E49CB0036ABB1 /* FacebookResetPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookResetPasswordViewModel.swift; sourceTree = ""; }; 6067BCEF293FC10E0036ABB1 /* FacebookResetPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookResetPasswordViewModelTests.swift; sourceTree = ""; }; + 606F214229799A0000BA5CDF /* ATTrackingAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATTrackingAuthorizationStatus.swift; sourceTree = ""; }; 608E7A5128ABD5E700289E92 /* SetYourPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetYourPasswordViewController.swift; sourceTree = ""; }; 608E7A5428ABE27400289E92 /* SetYourPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetYourPasswordViewModel.swift; sourceTree = ""; }; 60DA50E928B68990002E2DF1 /* SetYourPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetYourPasswordViewModelTests.swift; sourceTree = ""; }; @@ -5971,6 +5973,7 @@ 8A213CE4239EAEA400BBB4C7 /* TrackingClientType.swift */, 94BE15C125E857C4007CD9A4 /* TrackingHelpers.swift */, 94BE15C925E96F06007CD9A4 /* TrackingHelpersTests.swift */, + 606F214229799A0000BA5CDF /* ATTrackingAuthorizationStatus.swift */, ); path = Tracking; sourceTree = ""; @@ -7733,6 +7736,7 @@ 8A6C58932475E5950098D5A2 /* UIRefreshControl+StartRefreshing.swift in Sources */, D7A37CCF1E2FF93D00EA066D /* SearchEmptyStateCellViewModel.swift in Sources */, 80D73AF61D50F1A60099231F /* Navigation.swift in Sources */, + 606F214429799A1200BA5CDF /* ATTrackingAuthorizationStatus.swift in Sources */, 473DE012273C502F0033331D /* ProjectRisksCellViewModel.swift in Sources */, A755115F1C8642C3005355CF /* Format.swift in Sources */, 598D96CB1D42AE85003F3F66 /* ActivitySampleProjectCellViewModel.swift in Sources */, diff --git a/Library/Tracking/ATTrackingAuthorizationStatus.swift b/Library/Tracking/ATTrackingAuthorizationStatus.swift new file mode 100644 index 0000000000..bb0258a42b --- /dev/null +++ b/Library/Tracking/ATTrackingAuthorizationStatus.swift @@ -0,0 +1,6 @@ +public enum ATTrackingAuthorizationStatus { + case authorized + case denied + case notDetermined + case restricted +} From a5df41ea5891678c2a71c2c8e2e0374bb130afec Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Thu, 19 Jan 2023 10:22:14 -0700 Subject: [PATCH 6/6] use ksr_delay insted of ksr_debounce --- Kickstarter-iOS/AppDelegateViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index d067916830..6ae8b428d3 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -792,7 +792,7 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.requestATTrackingAuthorizationStatus = self.applicationLaunchOptionsProperty.signal .skipNil() - .ksr_debounce(.seconds(1), on: AppEnvironment.current.scheduler) + .ksr_delay(.seconds(1), on: AppEnvironment.current.scheduler) .map { _ -> ATTrackingAuthorizationStatus in guard featureConsentManagementDialogEnabled() else { return .notDetermined } return atTrackingAuthorizationStatus()