Why your App Tracking Transparency prompt doesn't show up (and how it got my app rejected)

2026年8月16日1 次浏览来源:Dev.to阅读原文

App Review rejected my iOS app under Guideline 2.1.

The note said reviewers were unable to locate the App Tracking Transparency permission request when they tested the build.

The prompt worked on my iPhone.

Every single launch.

It just didn't work on theirs.

The cause turned out to be two properties of the ATT API that are easy to miss individually and genuinely nasty in combination: together they produce a bug that is invisible on a fast device and completely reproducible on a slow one.

Your test device is fast.

The reviewer's device is not necessarily.

This post is the root cause, the fix I shipped, and the list of other things that silently suppress the prompt.

The two facts that explain everything

  1. iOS only presents the ATT prompt while your app is active Apple's documentation for states, for iOS 15 and later: "Calls to the API only prompt when the application state is UIApplicationStateActive." That's — not merely "in the foreground," and not "the code is running." During launch there is a window where your JS/UI is already executing but the app is still : splash screen dismissal, the first render, a modal transition animating in or out.
    Call the API in that window and iOS declines to present.
    2.
    When iOS declines to present, you don't get an error You get back ( in ) — which is the exact same value you get when the user simply hasn't answered yet.
    There is no "I couldn't show it" signal.
    There is no thrown error.
    There is no flag.
    From the return value alone, "the user hasn't decided yet" and "iOS silently no-op'd your request" are indistinguishable.
    That's the trap.
    The API looks like it succeeded.
    The bug I shipped Reduced to its essentials: Two mistakes, stacked: Requested too early.
    The call fired before the app reached , so iOS sometimes skipped the prompt entirely.
    Treated as a final answer.
    Anything that wasn't got folded into "not granted," cached for the process lifetime, and never retried.
    Individually, either one is survivable.
    Together they mean: if the first attempt slips, that install never sees the prompt again — not on that launch, and on subsequent launches the same race can repeat.
    On my phone, launch was fast enough that the app was usually by the time the call landed.
    On the review device it wasn't.
    That timing difference is the entire distance between "ships" and "rejected." The tell: if comes back after you explicitly requested authorization, that is not a user declining.
    That is iOS never asking.
    The fix, part 1: wait for Don't request on a timer, and don't request "after 2 seconds" and hope.
    Observe the actual app state.
    Two details worth stealing: The re-check after subscribing.
    There is a real gap between reading and the listener being registered.
    If the transition happens inside that gap, you wait for an event that already fired.
    This is the kind of race that shows up once a week in production and never on your desk.
    The timeout.
    A permission helper that can hang forever will eventually hang forever, and it will take your ad SDK init (or worse, your splash screen) with it.
    Resolve on timeout and let the caller carry on.
    The fix, part 2: retry on instead of giving up Since "not presented" and "not answered" look identical, the only way to tell them apart is to try again and see if anything changes.
    Why an increasing delay rather than a fixed one: the situations that suppress the prompt (transition animations, another permission dialog on screen, the system settling after launch) all resolve on their own, but on timescales you can't predict.
    Short first attempt so a healthy launch prompts quickly; longer later attempts so a slow device still gets there.
    Don't hammer it.
    Requesting permissions in a tight loop is its own failure mode, and other permission dialogs (notifications, location) put your app into while they're on screen — so a burst of retries during a notification prompt is five guaranteed misses.
    Also worth noting: this whole helper should run once per process and hand the same promise to every caller.
    Two concurrent ATT requests do not queue up; the second one is simply lost.
    And don't it before showing ads.
    Start ads in non-personalized mode, and let the consent result flip the flag when it arrives.
    A retry loop that gates your first ad request is a retry loop that costs you your first ad impression.
    The fix, part 3: give users a manual trigger There will still be environments where the automatic request slips.
    So I added a row in Settings — "Ad tracking settings" — that calls directly on tap.
    This is worth doing for two reasons beyond the user-facing one: It gives App Review a deterministic path to the prompt that doesn't depend on launch timing, and you can describe it in the review notes.
    Once the user has answered, iOS won't show the dialog again — so this row should detect that state and deep-link to instead of silently doing nothing. handles that on iOS.
    The same two rules in native Swift I ship the React Native version above, so treat this as the principle translated rather than production code I've run: Same two rules: request only while , and never record as a decision.
    Other reasons the prompt won't appear Before you go rewrite your state handling, rule these out — several of them will make a correct implementation look broken: The user already answered. iOS remembers the choice for the lifetime of the install.
    Per Expo's docs, it won't prompt again unless the app is deleted and reinstalled.
    This is the number one reason "my fix didn't work" — you already tapped a button on that device.
    System-wide tracking requests are off.
    Settings → Privacy & Security → Tracking → "Allow Apps to Request to Track" disabled means no app gets a prompt, ever.
    Your API call returns denied/undetermined with nothing shown.
    Another permission dialog is pending.
    Apple documents that the prompt won't display while another permission request is awaiting the user.
    If you fire notifications + location + ATT at launch, they don't queue politely.
    Sequence them — request one, and only request the next from the previous one's completion handler.
    You're calling from an app extension.
    Apple documents that calls through an app extension don't prompt. is missing from your Info.plist.
    No string, no prompt.
    In Expo this goes in under (or via the config plugin for ). iOS 13 or earlier / non-iOS.
    No ATT framework.
    Guard with an availability check so this path doesn't look like a failure.
    Simulator.
    Behavior differs from device.
    Verify on hardware before concluding anything.
    Resetting so you can actually test the fix Because the answer is sticky per install, testing takes discipline: Delete and reinstall the app.
    This is the reliable reset for the per-app status.
    Toggling "Allow Apps to Request to Track" off and on in Settings also affects behavior, and is the fastest way to reproduce the "nothing shows up" state on purpose.
    To reproduce the original bug rather than the fix, artificially delay reaching — put the request behind a heavy synchronous startup path, or test on the oldest supported device you have.
    Fast hardware hides this defect.
    What I sent App Review Code changes alone weren't what got it through — evidence was.
    A screen recording on a real device, from cold launch to the prompt appearing, unedited.
    Review notes with explicit steps: launch the app, wait on the home screen, prompt appears; alternatively, Settings → Ad tracking settings → tap.
    A one-line explanation of what changed since the previous submission.
    It passed on the third submission.
    One honest caveat: Apple documents the -state requirement, but Apple does not document "retry on " as the sanctioned remedy.
    That part is what fixed it in my case, on my code path.
    Treat it as a field report, not as a spec.
    Bonus: the other rejection, if you ship subscriptions Since it's the same trip through App Review and it's another pure "did you know" problem — my first rejection was Guideline 3.1.2: auto-renewable subscriptions offered without a functional link to the Terms
分享
Baike.dev

baike.dev helps you discover great languages, frameworks, databases, DevOps and cloud-native tools.

Quick links

About

Contribute

Found a great developer tool? Share it with the community.

Submit a tool
© 2026 baike.dev Developer EncyclopediaUpdated daily · Discover great developer tools