Apple's ATT framework (external apple-att-framework) requires explicit user authorisation before any app reads the IDFA or allows tracking SDKs to access cross-app identifiers. Initialising AdMob, Meta, or AppsFlyer in AppDelegate.application(_:didFinishLaunchingWithOptions:) before the ATT prompt resolves means those SDKs read IDFA before consent — a direct violation of Apple's policy, GDPR Art.7 (consent must precede processing), and CCPA §1798.120 (right to opt out of sale/sharing). Apple has rejected apps for this pattern since iOS 14.5 and continues to enforce it on every submission. Missing NSUserTrackingUsageDescription in Info.plist causes the app to crash when the ATT dialog is triggered.
Critical because ATT violations cause immediate binary rejection, and reading IDFA before consent is grounds for enforcement action by Apple and regulators under GDPR Art.7.
Request ATT authorisation before initialising any SDK that reads device identifiers. Trigger the prompt in applicationDidBecomeActive, not didFinishLaunchingWithOptions:
import AppTrackingTransparency
func applicationDidBecomeActive(_ application: UIApplication) {
ATTrackingManager.requestTrackingAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
GADMobileAds.sharedInstance().start()
default:
GADMobileAds.sharedInstance().start() // limited mode
}
}
}
}
For Expo/React Native, use expo-tracking-transparency and await requestTrackingPermissionsAsync() before any SDK initialisation. Add NSUserTrackingUsageDescription to Info.plist with a specific description of the tracking purpose.
app-store-privacy-data.tracking-advertising.att-before-trackingcriticalASIdentifierManager.sharedManager().advertisingIdentifier (IDFA); SKAdNetwork attribution calls; third-party SDK initializations that read IDFA internally (AdMob GADMobileAds.sharedInstance().start(), Meta FBSDKApplicationDelegate.sharedInstance().application(...), AppsFlyer AppsFlyerLib.shared().start()). Then find where ATTrackingManager.requestTrackingAuthorization(completionHandler:) is called (or requestPermissionsAsync() from expo-tracking-transparency). Verify the temporal ordering: ATT authorization must be requested and a result received BEFORE any tracking SDK is initialized or IDFA is read. Look for patterns where SDKs are initialized in AppDelegate.application(_:didFinishLaunchingWithOptions:) or a top-level useEffect BEFORE the ATT prompt appears. Also check: is NSUserTrackingUsageDescription set in Info.plist? (Required — without it, the ATT dialog crashes the app on iOS 14+.) Does the app handle the .denied and .restricted cases by switching to a limited-data or contextual-ads-only mode?NSUserTrackingUsageDescription is set with a specific explanation.NSUserTrackingUsageDescription missing; ATT denial not handled (app crashes or breaks tracking-dependent features)."GADMobileAds.sharedInstance().start() called in AppDelegate before ATTrackingManager.requestTrackingAuthorization — AdMob reads IDFA before consent is collected" or "NSUserTrackingUsageDescription missing from Info.plist — app will crash when ATT dialog is triggered on iOS 14+"// AppDelegate or app startup
import AppTrackingTransparency
func applicationDidBecomeActive(_ application: UIApplication) {
ATTrackingManager.requestTrackingAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
// Initialize tracking SDKs
GADMobileAds.sharedInstance().start()
default:
// Initialize SDKs in limited/contextual mode
GADMobileAds.sharedInstance().start()
// Ensure IDFA is not passed
}
}
}
}
expo-tracking-transparency:
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
const { status } = await requestTrackingPermissionsAsync();
// Initialize SDKs after this resolves
applicationDidBecomeActive (not didFinishLaunchingWithOptions) to ensure the dialog appears at the right time