Skip to content

Commit 12eec81

Browse files
authored
Merge pull request #31 from maevsi/feat/app-tracking-transparency
feat: implement app tracking transparency
2 parents f2a615a + b25a17b commit 12eec81

5 files changed

Lines changed: 154 additions & 10 deletions

File tree

vibetype.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
595F23AF25CEFBFE0053416C /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F23A425CEFBFE0053416C /* WebView.swift */; };
1919
6BC0B4AB2ED94D4100E03379 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 6BC0B4AA2ED94D3A00E03379 /* README.md */; };
2020
7692219AF6CB60CE94E971C2 /* Pods_vibetype.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4248D6343DB1391C0EB00BB7 /* Pods_vibetype.framework */; };
21+
936658952F3085DF00CE9A4A /* TrackingTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658942F3085DF00CE9A4A /* TrackingTransparency.swift */; };
2122
CDC0FE292388222C002C8D56 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDC0FE252388222B002C8D56 /* Main.storyboard */; };
2223
CDC0FE2A2388222C002C8D56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDC0FE272388222B002C8D56 /* LaunchScreen.storyboard */; };
2324
DDBCB1142D6C602600313680 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDBCB1132D6C602600313680 /* GoogleService-Info.plist */; };
@@ -37,6 +38,7 @@
3738
595F23A325CEFBFE0053416C /* Printer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Printer.swift; path = vibetype/Printer.swift; sourceTree = "<group>"; };
3839
595F23A425CEFBFE0053416C /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WebView.swift; path = vibetype/WebView.swift; sourceTree = "<group>"; };
3940
6BC0B4AA2ED94D3A00E03379 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
41+
936658942F3085DF00CE9A4A /* TrackingTransparency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TrackingTransparency.swift; path = vibetype/TrackingTransparency.swift; sourceTree = "<group>"; };
4042
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 = "<group>"; };
4143
CDC0FE262388222B002C8D56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = vibetype/Base.lproj/Main.storyboard; sourceTree = "<group>"; };
4244
CDC0FE282388222B002C8D56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = vibetype/Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -80,6 +82,7 @@
8082
595F23A125CEFBFE0053416C /* ViewController.swift */,
8183
CDC0FE272388222B002C8D56 /* LaunchScreen.storyboard */,
8284
CDC0FE252388222B002C8D56 /* Main.storyboard */,
85+
936658942F3085DF00CE9A4A /* TrackingTransparency.swift */,
8386
595F23A425CEFBFE0053416C /* WebView.swift */,
8487
30FCACC6A7BF53CD6D9CF6C0 /* Pods */,
8588
59333BAA25CFF706003392A4 /* vibetype.app */,
@@ -200,10 +203,14 @@
200203
inputFileListPaths = (
201204
"${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks-${CONFIGURATION}-input-files.xcfilelist",
202205
);
206+
inputPaths = (
207+
);
203208
name = "[CP] Embed Pods Frameworks";
204209
outputFileListPaths = (
205210
"${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks-${CONFIGURATION}-output-files.xcfilelist",
206211
);
212+
outputPaths = (
213+
);
207214
runOnlyForDeploymentPostprocessing = 0;
208215
shellPath = /bin/sh;
209216
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-vibetype/Pods-vibetype-frameworks.sh\"\n";
@@ -218,6 +225,7 @@
218225
files = (
219226
595F23AD25CEFBFE0053416C /* AppDelegate.swift in Sources */,
220227
595F23A825CEFBFE0053416C /* SceneDelegate.swift in Sources */,
228+
936658952F3085DF00CE9A4A /* TrackingTransparency.swift in Sources */,
221229
595F23A525CEFBFE0053416C /* Settings.swift in Sources */,
222230
595F23AE25CEFBFE0053416C /* Printer.swift in Sources */,
223231
595F23AC25CEFBFE0053416C /* ViewController.swift in Sources */,

vibetype/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
<string>The app allows taking photos of event posters to show interest in the listed events.</string>
5353
<key>NSLocationWhenInUseUsageDescription</key>
5454
<string>The app allows accessing your location to provide event recommendations based on your area.</string>
55+
<key>NSUserTrackingUsageDescription</key>
56+
<string>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.</string>
5557
<!-- <key>NSMicrophoneUsageDescription</key>
5658
<string>Capture Audio by user request</string> -->
5759
<key>UIApplicationSceneManifest</key>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
import AppTrackingTransparency
3+
import AdSupport
4+
5+
@available(iOS 14, *)
6+
class TrackingTransparencyManager {
7+
8+
// Request ATT permission and return the result via callback
9+
static func requestPermission(completion: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) {
10+
ATTrackingManager.requestTrackingAuthorization { status in
11+
DispatchQueue.main.async {
12+
completion(status)
13+
}
14+
}
15+
}
16+
17+
// Get current tracking authorization status
18+
static func getStatus() -> ATTrackingManager.AuthorizationStatus {
19+
return ATTrackingManager.trackingAuthorizationStatus
20+
}
21+
22+
// Get status as a string representation for JavaScript
23+
static func getStatusString() -> String {
24+
return statusToString(getStatus())
25+
}
26+
27+
// Convert status to string
28+
static func statusToString(_ status: ATTrackingManager.AuthorizationStatus) -> String {
29+
switch status {
30+
case .notDetermined:
31+
return "notDetermined"
32+
case .restricted:
33+
return "restricted"
34+
case .denied:
35+
return "denied"
36+
case .authorized:
37+
return "authorized"
38+
@unknown default:
39+
return "unknown"
40+
}
41+
}
42+
43+
// Get IDFA (Identifier for Advertisers) if authorized
44+
static func getIDFA() -> String? {
45+
guard getStatus() == .authorized else {
46+
return nil
47+
}
48+
49+
let idfa = ASIdentifierManager.shared().advertisingIdentifier
50+
51+
// Apple returns all zeros when tracking is not authorized
52+
let zeroIDFA = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
53+
guard idfa != zeroIDFA else {
54+
return nil
55+
}
56+
57+
return idfa.uuidString
58+
}
59+
}

vibetype/ViewController.swift

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,20 +231,79 @@ extension UIColor {
231231

232232
extension ViewController: WKScriptMessageHandler {
233233
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
234+
if message.name == "att-get-idfa" {
235+
handleATTGetIDFA()
236+
}
237+
if message.name == "att-get-status" {
238+
handleATTGetStatus()
239+
}
240+
if message.name == "att-request-permission" {
241+
handleATTPermissionRequest()
242+
}
234243
if message.name == "print" {
235244
printView(webView: vibetype.webView)
236245
}
237-
if message.name == "push-subscribe" {
238-
handleSubscribeTouch(message: message)
239-
}
240246
if message.name == "push-permission-request" {
241247
handlePushPermission()
242248
}
243249
if message.name == "push-permission-state" {
244250
handlePushState()
245251
}
252+
if message.name == "push-subscribe" {
253+
handleSubscribeTouch(message: message)
254+
}
246255
if message.name == "push-token" {
247256
handleFCMToken()
248257
}
249258
}
250259
}
260+
261+
// MARK: - App Tracking Transparency Handlers
262+
extension ViewController {
263+
264+
private func dispatchATTEvent(eventName: String, detail: String) {
265+
func toJsonString(_ value: String) -> String? {
266+
guard let data = try? JSONSerialization.data(withJSONObject: [value]),
267+
let json = String(data: data, encoding: .utf8) else { return nil }
268+
return String(json.dropFirst().dropLast()) // strip wrapping [ and ]
269+
}
270+
guard let detailJson = toJsonString(detail),
271+
let eventNameJson = toJsonString(eventName) else {
272+
print("Error encoding ATT event data for: \(eventName)")
273+
return
274+
}
275+
let script = "window.dispatchEvent(new CustomEvent(\(eventNameJson), { detail: \(detailJson) }));"
276+
vibetype.webView.evaluateJavaScript(script) { _, error in
277+
if let error = error {
278+
print("Error dispatching \(eventName): \(error)")
279+
}
280+
}
281+
}
282+
283+
func handleATTPermissionRequest() {
284+
if #available(iOS 14, *) {
285+
TrackingTransparencyManager.requestPermission { [weak self] status in
286+
guard let self else { return }
287+
self.dispatchATTEvent(eventName: "att-permission-response", detail: TrackingTransparencyManager.statusToString(status))
288+
}
289+
} else {
290+
dispatchATTEvent(eventName: "att-permission-response", detail: "unavailable")
291+
}
292+
}
293+
294+
func handleATTGetStatus() {
295+
if #available(iOS 14, *) {
296+
dispatchATTEvent(eventName: "att-status-response", detail: TrackingTransparencyManager.getStatusString())
297+
} else {
298+
dispatchATTEvent(eventName: "att-status-response", detail: "unavailable")
299+
}
300+
}
301+
302+
func handleATTGetIDFA() {
303+
if #available(iOS 14, *) {
304+
dispatchATTEvent(eventName: "att-idfa-response", detail: TrackingTransparencyManager.getIDFA() ?? "")
305+
} else {
306+
dispatchATTEvent(eventName: "att-idfa-response", detail: "")
307+
}
308+
}
309+
}

vibetype/WebView.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,35 @@ import WebKit
33
import AuthenticationServices
44
import SafariServices
55

6+
final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
7+
private weak var delegate: AnyObject?
68

7-
func createWebView(container: UIView, WKSMH: WKScriptMessageHandler, WKND: WKNavigationDelegate, NSO: NSObject, VC: ViewController) -> WKWebView{
9+
init(delegate: AnyObject & WKScriptMessageHandler) {
10+
self.delegate = delegate
11+
super.init()
12+
}
13+
14+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
15+
(delegate as? WKScriptMessageHandler)?.userContentController(userContentController, didReceive: message)
16+
}
17+
}
18+
19+
func createWebView(container: UIView, WKSMH: AnyObject & WKScriptMessageHandler, WKND: WKNavigationDelegate, NSO: NSObject, VC: ViewController) -> WKWebView{
820

921
let config = WKWebViewConfiguration()
1022
let userContentController = WKUserContentController()
1123
let deviceModel = UIDevice.current.model
1224
let osVersion = ProcessInfo().operatingSystemVersion
13-
14-
userContentController.add(WKSMH, name: "print")
15-
userContentController.add(WKSMH, name: "push-subscribe")
16-
userContentController.add(WKSMH, name: "push-permission-request")
17-
userContentController.add(WKSMH, name: "push-permission-state")
18-
userContentController.add(WKSMH, name: "push-token")
25+
let weakHandler = WeakScriptMessageHandler(delegate: WKSMH)
26+
27+
userContentController.add(weakHandler, name: "att-get-idfa")
28+
userContentController.add(weakHandler, name: "att-get-status")
29+
userContentController.add(weakHandler, name: "att-request-permission")
30+
userContentController.add(weakHandler, name: "print")
31+
userContentController.add(weakHandler, name: "push-permission-request")
32+
userContentController.add(weakHandler, name: "push-permission-state")
33+
userContentController.add(weakHandler, name: "push-subscribe")
34+
userContentController.add(weakHandler, name: "push-token")
1935

2036
config.userContentController = userContentController
2137

0 commit comments

Comments
 (0)