diff --git a/vibetype.xcodeproj/project.pbxproj b/vibetype.xcodeproj/project.pbxproj index fd6688b..a4b4882 100644 --- a/vibetype.xcodeproj/project.pbxproj +++ b/vibetype.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 595F23AF25CEFBFE0053416C /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F23A425CEFBFE0053416C /* WebView.swift */; }; 6BC0B4AB2ED94D4100E03379 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 6BC0B4AA2ED94D3A00E03379 /* README.md */; }; 7692219AF6CB60CE94E971C2 /* Pods_vibetype.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4248D6343DB1391C0EB00BB7 /* Pods_vibetype.framework */; }; + 936658952F3085DF00CE9A4A /* TrackingTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658942F3085DF00CE9A4A /* TrackingTransparency.swift */; }; CDC0FE292388222C002C8D56 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDC0FE252388222B002C8D56 /* Main.storyboard */; }; CDC0FE2A2388222C002C8D56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDC0FE272388222B002C8D56 /* LaunchScreen.storyboard */; }; DDBCB1142D6C602600313680 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDBCB1132D6C602600313680 /* GoogleService-Info.plist */; }; @@ -37,6 +38,7 @@ 595F23A325CEFBFE0053416C /* Printer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Printer.swift; path = vibetype/Printer.swift; sourceTree = ""; }; 595F23A425CEFBFE0053416C /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WebView.swift; path = vibetype/WebView.swift; sourceTree = ""; }; 6BC0B4AA2ED94D3A00E03379 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 936658942F3085DF00CE9A4A /* TrackingTransparency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TrackingTransparency.swift; path = vibetype/TrackingTransparency.swift; sourceTree = ""; }; B3109B700B2E429F9589C698 /* Pods-vibetype.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-vibetype.release.xcconfig"; path = "Target Support Files/Pods-vibetype/Pods-vibetype.release.xcconfig"; sourceTree = ""; }; CDC0FE262388222B002C8D56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = vibetype/Base.lproj/Main.storyboard; sourceTree = ""; }; CDC0FE282388222B002C8D56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = vibetype/Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -80,6 +82,7 @@ 595F23A125CEFBFE0053416C /* ViewController.swift */, CDC0FE272388222B002C8D56 /* LaunchScreen.storyboard */, CDC0FE252388222B002C8D56 /* Main.storyboard */, + 936658942F3085DF00CE9A4A /* TrackingTransparency.swift */, 595F23A425CEFBFE0053416C /* WebView.swift */, 30FCACC6A7BF53CD6D9CF6C0 /* Pods */, 59333BAA25CFF706003392A4 /* vibetype.app */, @@ -200,10 +203,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks.sh\"\n"; @@ -218,6 +225,7 @@ files = ( 595F23AD25CEFBFE0053416C /* AppDelegate.swift in Sources */, 595F23A825CEFBFE0053416C /* SceneDelegate.swift in Sources */, + 936658952F3085DF00CE9A4A /* TrackingTransparency.swift in Sources */, 595F23A525CEFBFE0053416C /* Settings.swift in Sources */, 595F23AE25CEFBFE0053416C /* Printer.swift in Sources */, 595F23AC25CEFBFE0053416C /* ViewController.swift in Sources */, diff --git a/vibetype/Info.plist b/vibetype/Info.plist index fa0cc66..da89060 100644 --- a/vibetype/Info.plist +++ b/vibetype/Info.plist @@ -52,6 +52,8 @@ The app allows taking photos of event posters to show interest in the listed events. NSLocationWhenInUseUsageDescription The app allows accessing your location to provide event recommendations based on your area. + NSUserTrackingUsageDescription + Your device's advertising identifier will be used by Google Analytics to measure and analyze app usage across apps and websites, helping us understand how our service is being used and improve it for all users. UIApplicationSceneManifest diff --git a/vibetype/TrackingTransparency.swift b/vibetype/TrackingTransparency.swift new file mode 100644 index 0000000..8e22f1e --- /dev/null +++ b/vibetype/TrackingTransparency.swift @@ -0,0 +1,59 @@ +import Foundation +import AppTrackingTransparency +import AdSupport + +@available(iOS 14, *) +class TrackingTransparencyManager { + + // Request ATT permission and return the result via callback + static func requestPermission(completion: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) { + ATTrackingManager.requestTrackingAuthorization { status in + DispatchQueue.main.async { + completion(status) + } + } + } + + // Get current tracking authorization status + static func getStatus() -> ATTrackingManager.AuthorizationStatus { + return ATTrackingManager.trackingAuthorizationStatus + } + + // Get status as a string representation for JavaScript + static func getStatusString() -> String { + return statusToString(getStatus()) + } + + // Convert status to string + static func statusToString(_ status: ATTrackingManager.AuthorizationStatus) -> String { + switch status { + case .notDetermined: + return "notDetermined" + case .restricted: + return "restricted" + case .denied: + return "denied" + case .authorized: + return "authorized" + @unknown default: + return "unknown" + } + } + + // Get IDFA (Identifier for Advertisers) if authorized + static func getIDFA() -> String? { + guard getStatus() == .authorized else { + return nil + } + + let idfa = ASIdentifierManager.shared().advertisingIdentifier + + // Apple returns all zeros when tracking is not authorized + let zeroIDFA = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + guard idfa != zeroIDFA else { + return nil + } + + return idfa.uuidString + } +} diff --git a/vibetype/ViewController.swift b/vibetype/ViewController.swift index 6aaa894..103494d 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -231,20 +231,79 @@ extension UIColor { extension ViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == "att-get-idfa" { + handleATTGetIDFA() + } + if message.name == "att-get-status" { + handleATTGetStatus() + } + if message.name == "att-request-permission" { + handleATTPermissionRequest() + } if message.name == "print" { printView(webView: vibetype.webView) } - if message.name == "push-subscribe" { - handleSubscribeTouch(message: message) - } if message.name == "push-permission-request" { handlePushPermission() } if message.name == "push-permission-state" { handlePushState() } + if message.name == "push-subscribe" { + handleSubscribeTouch(message: message) + } if message.name == "push-token" { handleFCMToken() } } } + +// MARK: - App Tracking Transparency Handlers +extension ViewController { + + private func dispatchATTEvent(eventName: String, detail: String) { + func toJsonString(_ value: String) -> String? { + guard let data = try? JSONSerialization.data(withJSONObject: [value]), + let json = String(data: data, encoding: .utf8) else { return nil } + return String(json.dropFirst().dropLast()) // strip wrapping [ and ] + } + guard let detailJson = toJsonString(detail), + let eventNameJson = toJsonString(eventName) else { + print("Error encoding ATT event data for: \(eventName)") + return + } + let script = "window.dispatchEvent(new CustomEvent(\(eventNameJson), { detail: \(detailJson) }));" + vibetype.webView.evaluateJavaScript(script) { _, error in + if let error = error { + print("Error dispatching \(eventName): \(error)") + } + } + } + + func handleATTPermissionRequest() { + if #available(iOS 14, *) { + TrackingTransparencyManager.requestPermission { [weak self] status in + guard let self else { return } + self.dispatchATTEvent(eventName: "att-permission-response", detail: TrackingTransparencyManager.statusToString(status)) + } + } else { + dispatchATTEvent(eventName: "att-permission-response", detail: "unavailable") + } + } + + func handleATTGetStatus() { + if #available(iOS 14, *) { + dispatchATTEvent(eventName: "att-status-response", detail: TrackingTransparencyManager.getStatusString()) + } else { + dispatchATTEvent(eventName: "att-status-response", detail: "unavailable") + } + } + + func handleATTGetIDFA() { + if #available(iOS 14, *) { + dispatchATTEvent(eventName: "att-idfa-response", detail: TrackingTransparencyManager.getIDFA() ?? "") + } else { + dispatchATTEvent(eventName: "att-idfa-response", detail: "") + } + } +} diff --git a/vibetype/WebView.swift b/vibetype/WebView.swift index f6e0eee..fd42933 100644 --- a/vibetype/WebView.swift +++ b/vibetype/WebView.swift @@ -3,19 +3,35 @@ import WebKit import AuthenticationServices import SafariServices +final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + private weak var delegate: AnyObject? -func createWebView(container: UIView, WKSMH: WKScriptMessageHandler, WKND: WKNavigationDelegate, NSO: NSObject, VC: ViewController) -> WKWebView{ + init(delegate: AnyObject & WKScriptMessageHandler) { + self.delegate = delegate + super.init() + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + (delegate as? WKScriptMessageHandler)?.userContentController(userContentController, didReceive: message) + } +} + +func createWebView(container: UIView, WKSMH: AnyObject & WKScriptMessageHandler, WKND: WKNavigationDelegate, NSO: NSObject, VC: ViewController) -> WKWebView{ let config = WKWebViewConfiguration() let userContentController = WKUserContentController() let deviceModel = UIDevice.current.model let osVersion = ProcessInfo().operatingSystemVersion - - userContentController.add(WKSMH, name: "print") - userContentController.add(WKSMH, name: "push-subscribe") - userContentController.add(WKSMH, name: "push-permission-request") - userContentController.add(WKSMH, name: "push-permission-state") - userContentController.add(WKSMH, name: "push-token") + let weakHandler = WeakScriptMessageHandler(delegate: WKSMH) + + userContentController.add(weakHandler, name: "att-get-idfa") + userContentController.add(weakHandler, name: "att-get-status") + userContentController.add(weakHandler, name: "att-request-permission") + userContentController.add(weakHandler, name: "print") + userContentController.add(weakHandler, name: "push-permission-request") + userContentController.add(weakHandler, name: "push-permission-state") + userContentController.add(weakHandler, name: "push-subscribe") + userContentController.add(weakHandler, name: "push-token") config.userContentController = userContentController