Our App Store review came back with a Guideline 2.1 rejection. The reviewer’s note was clear: “Your app references the App Tracking Transparency (ATT) framework, but we were unable to locate the ATT permission request in your app.”

I had never written a line of ATT code. I had never called AppTrackingTransparency.requestTrackingAuthorization(). I had never imported the framework, never added the NSUserTrackingUsageDescription plist key, never prompted for tracking permission. The rejection made no sense.

Apple flagged ATT in our binary — and I had never called a single ATT API. That was exactly the problem.

The diagnosis: it’s in your dependencies

I ran grep -r "AppTrackingTransparency" ios/ and found nothing in our source code. Then I checked the Podfile.lock. There it was:AppTrackingTransparency was a transitive dependency of google_mobile_ads — the AdMob SDK.

When you add AdMob to your Flutter app via google_mobile_ads: ^7.0, you pull in the iOS native SDK. That SDK includes ATT tracking permission as an optional feature. Even though we never requested it, the framework binary was linked into our app. And Apple’s static analysis scans the linked frameworks, not your source code.

The rule is simple but easy to miss:

If a framework is linked into your binary, Apple assumes you might use it. If that framework is ATT, they require you to either call it or remove it. There is no third option.

The fix: add the permission request

Since we actually use AdMob (it’s our revenue model), removing the dependency isn’t an option. The fix is to add the ATT permission request properly.

Step 1: Add the plist key to ios/Runner/Info.plist:

Info.plist
xml
<key>NSUserTrackingUsageDescription</key>
<string>We use tracking to show you personalized ads and measure their effectiveness.</string>

Step 2: Request permission before initializing AdMob. This is critical — AdMob must be initialized afterthe user responds to the ATT prompt, or it may not respect the user’s choice.

main.dart
dart
import 'package:app_tracking_transparency/app_tracking_transparency.dart';

Future<void> main() async {
  // ... Firebase, DI, etc.

  // Request ATT before initializing AdMob
  if (Platform.isIOS) {
    await AppTrackingTransparency.requestTrackingAuthorization();
  }

  // Now safe to initialize MobileAds
  await MobileAds.instance.initialize();

  runApp(const MyApp());
}

That’s it. Two changes — a plist entry and a single API call before AdMob init. The App Store review passed on the next submission.

Why Apple can’t tell whether you actually use it

Apple’s review process uses static binary analysis. It scans the final compiled app for linked frameworks and symbols. It can’t determine runtime behavior — whether you actually call the API or just have it available.

This is by design. The ATT framework is intended to give users a choice. If the framework is linked but no prompt is shown, the user never gets that choice. Apple’s static analysis is conservative: if the framework is there, you must present the prompt. Better to force a prompt than to risk silent tracking.

The alternative: strip the framework

If you don’t use AdMob (or any other SDK that transitively depends on ATT), you can remove the framework entirely. In a Flutter project, this means checking your Podfile.lock for AppTrackingTransparency and removing the dependency that pulls it in.

But for us, AdMob is non-negotiable — it’s our only revenue source. The ATT prompt is a small UX trade for having ads at all.

Different platforms, different rules

This issue is iOS-specific. Android’s equivalent (Google’s Advertising ID) is handled differently — the user opts out via device settings, not an in-app prompt. If you’re building for both platforms, you only need the ATT request on iOS.

But don’t skip it on iOS. The rejection is fast (usually within 24 hours) and completely blocks your update. Every day your update is stuck in review is a day your users don’t get the bug fixes you shipped.

What I’d Tell My Past Self

  1. Run grep -r "AppTrackingTransparency" before submitting. If the framework is in your Podfile.lock, you need to handle it — even if you never explicitly imported it.
  2. Request ATT before AdMob init, not after. The order matters. If AdMob initializes first, it may start tracking before the user has responded to the prompt.
  3. Add the NSUserTrackingUsageDescription plist key. Without it, the requestTrackingAuthorization()call will silently fail. The user won’t see any prompt.
  4. Don’t assume your dependencies are clean.Every third-party SDK you add may bring in frameworks you don’t use directly. Audit your Podfile.lock periodically, especially before App Store submissions.

The ATT rejection was the fastest fix I’ve ever shipped — two lines of code and a plist entry. But it cost 24 hours of review time and a lot of confusion. The framework wasn’t in my code. It was in my dependencies. And Apple didn’t care about the difference.