From 45d58c722337b8e2b9d8f5e012b8977498e0b852 Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Mon, 2 Feb 2026 05:58:18 +0100 Subject: [PATCH 1/5] feat: implement app tracking transparency --- vibetype.xcodeproj/project.pbxproj | 8 +++ vibetype/Info.plist | 2 + vibetype/TrackingTransparency.swift | 64 +++++++++++++++++++++ vibetype/ViewController.swift | 88 +++++++++++++++++++++++++++++ vibetype/WebView.swift | 3 + 5 files changed, 165 insertions(+) create mode 100644 vibetype/TrackingTransparency.swift 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..a45dc47 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 + This allows us to provide you with personalized content and improve your experience. UIApplicationSceneManifest diff --git a/vibetype/TrackingTransparency.swift b/vibetype/TrackingTransparency.swift new file mode 100644 index 0000000..beeed20 --- /dev/null +++ b/vibetype/TrackingTransparency.swift @@ -0,0 +1,64 @@ +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 { + let status = getStatus() + switch status { + case .notDetermined: + return "notDetermined" + case .restricted: + return "restricted" + case .denied: + return "denied" + case .authorized: + return "authorized" + @unknown default: + return "unknown" + } + } + + // 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 + return idfa.uuidString + } +} diff --git a/vibetype/ViewController.swift b/vibetype/ViewController.swift index 6aaa894..91af73f 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -246,5 +246,93 @@ extension ViewController: WKScriptMessageHandler { if message.name == "push-token" { handleFCMToken() } + if message.name == "att-request-permission" { + handleATTPermissionRequest() + } + if message.name == "att-get-status" { + handleATTGetStatus() + } + if message.name == "att-get-idfa" { + handleATTGetIDFA() + } } } + +// MARK: - App Tracking Transparency Handlers +extension ViewController { + + func handleATTPermissionRequest() { + if #available(iOS 14, *) { + TrackingTransparencyManager.requestPermission { status in + let statusString = TrackingTransparencyManager.statusToString(status) + let script = """ + window.dispatchEvent(new CustomEvent('attPermissionResponse', { + detail: { status: '\(statusString)' } + })); + """ + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT permission response: \(error)") + } + } + } + } else { + // ATT is only available on iOS 14+ + let script = """ + window.dispatchEvent(new CustomEvent('attPermissionResponse', { + detail: { status: 'unavailable', error: 'ATT requires iOS 14 or later' } + })); + """ + vibetype.webView.evaluateJavaScript(script) + } + } + + func handleATTGetStatus() { + if #available(iOS 14, *) { + let statusString = TrackingTransparencyManager.getStatusString() + let script = """ + window.dispatchEvent(new CustomEvent('attStatusResponse', { + detail: { status: '\(statusString)' } + })); + """ + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT status response: \(error)") + } + } + } else { + // ATT is only available on iOS 14+ + let script = """ + window.dispatchEvent(new CustomEvent('attStatusResponse', { + detail: { status: 'unavailable' } + })); + """ + vibetype.webView.evaluateJavaScript(script) + } + } + + func handleATTGetIDFA() { + if #available(iOS 14, *) { + let idfa = TrackingTransparencyManager.getIDFA() + let idfaValue = idfa ?? "null" + let script = """ + window.dispatchEvent(new CustomEvent('attIDFAResponse', { + detail: { idfa: '\(idfaValue)' } + })); + """ + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT IDFA response: \(error)") + } + } + } else { + // ATT is only available on iOS 14+ + let script = """ + window.dispatchEvent(new CustomEvent('attIDFAResponse', { + detail: { idfa: null, error: 'ATT requires iOS 14 or later' } + })); + """ + vibetype.webView.evaluateJavaScript(script) + } + } +} diff --git a/vibetype/WebView.swift b/vibetype/WebView.swift index f6e0eee..1d0f32f 100644 --- a/vibetype/WebView.swift +++ b/vibetype/WebView.swift @@ -16,6 +16,9 @@ func createWebView(container: UIView, WKSMH: WKScriptMessageHandler, WKND: WKNav userContentController.add(WKSMH, name: "push-permission-request") userContentController.add(WKSMH, name: "push-permission-state") userContentController.add(WKSMH, name: "push-token") + userContentController.add(WKSMH, name: "att-request-permission") + userContentController.add(WKSMH, name: "att-get-status") + userContentController.add(WKSMH, name: "att-get-idfa") config.userContentController = userContentController From f26aa22bd3aefb896737ea97b3a5ee3918e73f91 Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Mon, 2 Feb 2026 08:21:38 +0100 Subject: [PATCH 2/5] chore: implement feedback --- vibetype/TrackingTransparency.swift | 21 ++++------ vibetype/ViewController.swift | 59 ++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/vibetype/TrackingTransparency.swift b/vibetype/TrackingTransparency.swift index beeed20..8e22f1e 100644 --- a/vibetype/TrackingTransparency.swift +++ b/vibetype/TrackingTransparency.swift @@ -21,19 +21,7 @@ class TrackingTransparencyManager { // Get status as a string representation for JavaScript static func getStatusString() -> String { - let status = getStatus() - switch status { - case .notDetermined: - return "notDetermined" - case .restricted: - return "restricted" - case .denied: - return "denied" - case .authorized: - return "authorized" - @unknown default: - return "unknown" - } + return statusToString(getStatus()) } // Convert status to string @@ -59,6 +47,13 @@ class TrackingTransparencyManager { } 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 91af73f..6989d94 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -265,9 +265,15 @@ extension ViewController { if #available(iOS 14, *) { TrackingTransparencyManager.requestPermission { status in let statusString = TrackingTransparencyManager.statusToString(status) + let detail: [String: Any] = ["status": statusString] + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT permission response") + return + } let script = """ window.dispatchEvent(new CustomEvent('attPermissionResponse', { - detail: { status: '\(statusString)' } + detail: \(jsonString) })); """ vibetype.webView.evaluateJavaScript(script) { result, error in @@ -283,16 +289,26 @@ extension ViewController { detail: { status: 'unavailable', error: 'ATT requires iOS 14 or later' } })); """ - vibetype.webView.evaluateJavaScript(script) + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT permission response: \(error)") + } + } } } func handleATTGetStatus() { if #available(iOS 14, *) { let statusString = TrackingTransparencyManager.getStatusString() + let detail: [String: Any] = ["status": statusString] + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT status response") + return + } let script = """ window.dispatchEvent(new CustomEvent('attStatusResponse', { - detail: { status: '\(statusString)' } + detail: \(jsonString) })); """ vibetype.webView.evaluateJavaScript(script) { result, error in @@ -302,22 +318,37 @@ extension ViewController { } } else { // ATT is only available on iOS 14+ + let detail: [String: Any] = ["status": "unavailable"] + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT status response") + return + } let script = """ window.dispatchEvent(new CustomEvent('attStatusResponse', { - detail: { status: 'unavailable' } + detail: \(jsonString) })); """ - vibetype.webView.evaluateJavaScript(script) + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT status response: \(error)") + } + } } } func handleATTGetIDFA() { if #available(iOS 14, *) { let idfa = TrackingTransparencyManager.getIDFA() - let idfaValue = idfa ?? "null" + let detail: [String: Any?] = ["idfa": idfa] + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail, options: []), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT IDFA response") + return + } let script = """ window.dispatchEvent(new CustomEvent('attIDFAResponse', { - detail: { idfa: '\(idfaValue)' } + detail: \(jsonString) })); """ vibetype.webView.evaluateJavaScript(script) { result, error in @@ -327,12 +358,22 @@ extension ViewController { } } else { // ATT is only available on iOS 14+ + let detail: [String: Any?] = ["idfa": nil, "error": "ATT requires iOS 14 or later"] + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail, options: []), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT IDFA response") + return + } let script = """ window.dispatchEvent(new CustomEvent('attIDFAResponse', { - detail: { idfa: null, error: 'ATT requires iOS 14 or later' } + detail: \(jsonString) })); """ - vibetype.webView.evaluateJavaScript(script) + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching ATT IDFA response: \(error)") + } + } } } } From 7fc5b7dacf4f752ab5dd3ee448440d0233ff532e Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Mon, 2 Feb 2026 08:35:44 +0100 Subject: [PATCH 3/5] chore: implement feedback --- vibetype/ViewController.swift | 118 +++++++++------------------------- 1 file changed, 32 insertions(+), 86 deletions(-) diff --git a/vibetype/ViewController.swift b/vibetype/ViewController.swift index 6989d94..7fd2261 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -261,39 +261,36 @@ extension ViewController: WKScriptMessageHandler { // MARK: - App Tracking Transparency Handlers extension ViewController { + // Helper method to dispatch ATT events to webview with proper JSON serialization + private func dispatchATTEvent(eventName: String, detail: [String: Any]) { + guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), + let jsonString = String(data: jsonData, encoding: .utf8) else { + print("Error serializing ATT response for event: \(eventName)") + return + } + let script = """ + window.dispatchEvent(new CustomEvent('\(eventName)', { + detail: \(jsonString) + })); + """ + vibetype.webView.evaluateJavaScript(script) { result, error in + if let error = error { + print("Error dispatching \(eventName): \(error)") + } + } + } + func handleATTPermissionRequest() { if #available(iOS 14, *) { TrackingTransparencyManager.requestPermission { status in let statusString = TrackingTransparencyManager.statusToString(status) let detail: [String: Any] = ["status": statusString] - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT permission response") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('attPermissionResponse', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT permission response: \(error)") - } - } + self.dispatchATTEvent(eventName: "attPermissionResponse", detail: detail) } } else { // ATT is only available on iOS 14+ - let script = """ - window.dispatchEvent(new CustomEvent('attPermissionResponse', { - detail: { status: 'unavailable', error: 'ATT requires iOS 14 or later' } - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT permission response: \(error)") - } - } + let detail: [String: Any] = ["status": "unavailable", "error": "ATT requires iOS 14 or later"] + dispatchATTEvent(eventName: "attPermissionResponse", detail: detail) } } @@ -301,79 +298,28 @@ extension ViewController { if #available(iOS 14, *) { let statusString = TrackingTransparencyManager.getStatusString() let detail: [String: Any] = ["status": statusString] - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT status response") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('attStatusResponse', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT status response: \(error)") - } - } + dispatchATTEvent(eventName: "attStatusResponse", detail: detail) } else { // ATT is only available on iOS 14+ let detail: [String: Any] = ["status": "unavailable"] - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT status response") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('attStatusResponse', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT status response: \(error)") - } - } + dispatchATTEvent(eventName: "attStatusResponse", detail: detail) } } func handleATTGetIDFA() { if #available(iOS 14, *) { let idfa = TrackingTransparencyManager.getIDFA() - let detail: [String: Any?] = ["idfa": idfa] - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail, options: []), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT IDFA response") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('attIDFAResponse', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT IDFA response: \(error)") - } + var detail: [String: Any] = [:] + if let idfa = idfa { + detail["idfa"] = idfa + } else { + detail["idfa"] = NSNull() } + dispatchATTEvent(eventName: "attIDFAResponse", detail: detail) } else { // ATT is only available on iOS 14+ - let detail: [String: Any?] = ["idfa": nil, "error": "ATT requires iOS 14 or later"] - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail, options: []), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT IDFA response") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('attIDFAResponse', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in - if let error = error { - print("Error dispatching ATT IDFA response: \(error)") - } - } + let detail: [String: Any] = ["idfa": NSNull(), "error": "ATT requires iOS 14 or later"] + dispatchATTEvent(eventName: "attIDFAResponse", detail: detail) } } } From d013c66748d578eb9b214ebf6f154f47f6e212c9 Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Fri, 10 Apr 2026 03:38:16 +0200 Subject: [PATCH 4/5] feat: simplify app tracking transparency --- vibetype/ViewController.swift | 69 +++++++++++------------------------ vibetype/WebView.swift | 8 ++-- 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/vibetype/ViewController.swift b/vibetype/ViewController.swift index 7fd2261..3c8b53c 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -231,49 +231,39 @@ 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() } - if message.name == "att-request-permission" { - handleATTPermissionRequest() - } - if message.name == "att-get-status" { - handleATTGetStatus() - } - if message.name == "att-get-idfa" { - handleATTGetIDFA() - } } } // MARK: - App Tracking Transparency Handlers extension ViewController { - // Helper method to dispatch ATT events to webview with proper JSON serialization - private func dispatchATTEvent(eventName: String, detail: [String: Any]) { - guard let jsonData = try? JSONSerialization.data(withJSONObject: detail), - let jsonString = String(data: jsonData, encoding: .utf8) else { - print("Error serializing ATT response for event: \(eventName)") - return - } - let script = """ - window.dispatchEvent(new CustomEvent('\(eventName)', { - detail: \(jsonString) - })); - """ - vibetype.webView.evaluateJavaScript(script) { result, error in + private func dispatchATTEvent(eventName: String, detail: String) { + let script = "window.dispatchEvent(new CustomEvent('\(eventName)', { detail: '\(detail)' }));" + vibetype.webView.evaluateJavaScript(script) { _, error in if let error = error { print("Error dispatching \(eventName): \(error)") } @@ -283,43 +273,26 @@ extension ViewController { func handleATTPermissionRequest() { if #available(iOS 14, *) { TrackingTransparencyManager.requestPermission { status in - let statusString = TrackingTransparencyManager.statusToString(status) - let detail: [String: Any] = ["status": statusString] - self.dispatchATTEvent(eventName: "attPermissionResponse", detail: detail) + self.dispatchATTEvent(eventName: "att-permission-response", detail: TrackingTransparencyManager.statusToString(status)) } } else { - // ATT is only available on iOS 14+ - let detail: [String: Any] = ["status": "unavailable", "error": "ATT requires iOS 14 or later"] - dispatchATTEvent(eventName: "attPermissionResponse", detail: detail) + dispatchATTEvent(eventName: "att-permission-response", detail: "unavailable") } } func handleATTGetStatus() { if #available(iOS 14, *) { - let statusString = TrackingTransparencyManager.getStatusString() - let detail: [String: Any] = ["status": statusString] - dispatchATTEvent(eventName: "attStatusResponse", detail: detail) + dispatchATTEvent(eventName: "att-status-response", detail: TrackingTransparencyManager.getStatusString()) } else { - // ATT is only available on iOS 14+ - let detail: [String: Any] = ["status": "unavailable"] - dispatchATTEvent(eventName: "attStatusResponse", detail: detail) + dispatchATTEvent(eventName: "att-status-response", detail: "unavailable") } } func handleATTGetIDFA() { if #available(iOS 14, *) { - let idfa = TrackingTransparencyManager.getIDFA() - var detail: [String: Any] = [:] - if let idfa = idfa { - detail["idfa"] = idfa - } else { - detail["idfa"] = NSNull() - } - dispatchATTEvent(eventName: "attIDFAResponse", detail: detail) + dispatchATTEvent(eventName: "att-idfa-response", detail: TrackingTransparencyManager.getIDFA() ?? "") } else { - // ATT is only available on iOS 14+ - let detail: [String: Any] = ["idfa": NSNull(), "error": "ATT requires iOS 14 or later"] - dispatchATTEvent(eventName: "attIDFAResponse", detail: detail) + dispatchATTEvent(eventName: "att-idfa-response", detail: "") } } } diff --git a/vibetype/WebView.swift b/vibetype/WebView.swift index 1d0f32f..124dc56 100644 --- a/vibetype/WebView.swift +++ b/vibetype/WebView.swift @@ -11,14 +11,14 @@ func createWebView(container: UIView, WKSMH: WKScriptMessageHandler, WKND: WKNav let deviceModel = UIDevice.current.model let osVersion = ProcessInfo().operatingSystemVersion + userContentController.add(WKSMH, name: "att-get-idfa") + userContentController.add(WKSMH, name: "att-get-status") + userContentController.add(WKSMH, name: "att-request-permission") 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-subscribe") userContentController.add(WKSMH, name: "push-token") - userContentController.add(WKSMH, name: "att-request-permission") - userContentController.add(WKSMH, name: "att-get-status") - userContentController.add(WKSMH, name: "att-get-idfa") config.userContentController = userContentController From b25a17b5835b58014919d42ed0b94b9993e15fa6 Mon Sep 17 00:00:00 2001 From: Jonas Thelemann Date: Fri, 10 Apr 2026 07:17:50 +0200 Subject: [PATCH 5/5] chore: implement feedback --- vibetype/Info.plist | 2 +- vibetype/ViewController.swift | 15 +++++++++++++-- vibetype/WebView.swift | 33 +++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/vibetype/Info.plist b/vibetype/Info.plist index a45dc47..da89060 100644 --- a/vibetype/Info.plist +++ b/vibetype/Info.plist @@ -53,7 +53,7 @@ NSLocationWhenInUseUsageDescription The app allows accessing your location to provide event recommendations based on your area. NSUserTrackingUsageDescription - This allows us to provide you with personalized content and improve your experience. + 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/ViewController.swift b/vibetype/ViewController.swift index 3c8b53c..103494d 100644 --- a/vibetype/ViewController.swift +++ b/vibetype/ViewController.swift @@ -262,7 +262,17 @@ extension ViewController: WKScriptMessageHandler { extension ViewController { private func dispatchATTEvent(eventName: String, detail: String) { - let script = "window.dispatchEvent(new CustomEvent('\(eventName)', { detail: '\(detail)' }));" + 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)") @@ -272,7 +282,8 @@ extension ViewController { func handleATTPermissionRequest() { if #available(iOS 14, *) { - TrackingTransparencyManager.requestPermission { status in + TrackingTransparencyManager.requestPermission { [weak self] status in + guard let self else { return } self.dispatchATTEvent(eventName: "att-permission-response", detail: TrackingTransparencyManager.statusToString(status)) } } else { diff --git a/vibetype/WebView.swift b/vibetype/WebView.swift index 124dc56..fd42933 100644 --- a/vibetype/WebView.swift +++ b/vibetype/WebView.swift @@ -3,22 +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: "att-get-idfa") - userContentController.add(WKSMH, name: "att-get-status") - userContentController.add(WKSMH, name: "att-request-permission") - userContentController.add(WKSMH, name: "print") - userContentController.add(WKSMH, name: "push-permission-request") - userContentController.add(WKSMH, name: "push-permission-state") - userContentController.add(WKSMH, name: "push-subscribe") - 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