The other day I tried to submit a new wallpaper app to App Store Connect. The build was fine, yet a red banner — "This app has no App Privacy information" — stopped me from going any further. The code was finished, but I was blocked at the very last step. When you ship apps as a solo developer, this declaration screen always makes you brace yourself a little.
The App Privacy section is where you, the developer, declare what kinds of data your app collects. The tricky part is that you are responsible not only for your own code, but also for whatever the third-party SDKs you embed — ads, billing, crash reporting — quietly collect in the background. For my first few apps I was bounced back here repeatedly, and ended up re-checking what each SDK gathers, one by one.
In this article I'll leave field notes on what I actually checked for a fairly standard setup: AdMob, RevenueCat, and Crashlytics. The exact answers shift with your configuration and SDK versions, so please verify against your own environment in the end.
App Privacy and the privacy manifest are two different things
These are easy to confuse, so let me separate them first. There are two similar-sounding things, and you need both.
| Name | What it is | Where you set it |
|---|---|---|
| Privacy manifest (PrivacyInfo.xcprivacy) | A file embedded in the app binary. Declares collected data types and the reasons for using "Required Reason APIs" in a machine-readable form | The build (Expo config / bundled by each SDK) |
| App Privacy (nutrition label) | The "data practices" list shown on your store product page. A developer self-declaration | The web form in App Store Connect |
The former is a machine-facing declaration read by crawlers; the latter is a human-facing summary that users read on the product page. Even if your manifest is correct, you still have to fill in the web-side declaration yourself. Early on I assumed these two were the same thing, got my manifest in order, still couldn't submit, and spent a while not understanding why.
Adding ads, billing, and crash reporting expands what you must declare
For a wallpaper app that only displays images, the declaration stays minimal. But once you add three SDKs for monetization and operations, it tips firmly toward "data is collected." Here is roughly what I ended up checking for my setup.
| SDK (purpose) | Main data types collected | Linked to the user | Used for tracking |
|---|---|---|---|
| AdMob (ads) | Identifiers (advertising ID / IDFA, etc.), Usage Data, Diagnostics | Yes (for ad delivery) | Yes (with personalized ads) |
| RevenueCat (billing) | Purchases, Identifiers (user ID), Usage Data | Yes | No |
| Crashlytics (crash reporting) | Diagnostics, Identifiers (installation ID) | No (in most setups) | No |
The biggest fork in the road here is AdMob's "used for tracking." Once you enable personalized ads, you are using the advertising ID to track users, which puts you under "Used to Track You" in the declaration. In my setup, RevenueCat and Crashlytics collect data to operate my own service but do not track across third parties, so I leave them out of the tracking column.
Each SDK publishes a page summarizing exactly what data it collects. If you fill the form from guesswork without reading these, discrepancies surface later. Before every declaration, I reopen each vendor's latest data-collection document and only then fill in the form.
Checking "used for tracking" makes ATT mandatory
This is the chain that trips people up most at submission time. If you declare "identifiers used for tracking" in App Privacy but your app never shows the App Tracking Transparency (ATT) permission dialog, the review will bounce you — because the declaration and the implementation contradict each other.
With Expo, add expo-tracking-transparency and set the permission string.
{
"expo": {
"plugins": [
[
"expo-tracking-transparency",
{
"userTrackingPermission": "We use your activity to show ads that are relevant to you."
}
]
]
}
}Then always request permission before loading personalized ads.
import { requestTrackingPermissionsAsync } from "expo-tracking-transparency";
const { status } = await requestTrackingPermissionsAsync();
// Only enable personalized ads when granted
const personalized = status === "granted";If permission is denied, turn personalization off and fall back to non-personalized ads. "Declared it = implemented ATT = stop tracking when the user declines" — unless all three line up, a contradiction will eventually show somewhere. I once set the ATT string but forgot to actually call the dialog, noticed only after submitting, and had to swap it in a hurry.
How to decide the three axes (collected / linked / tracking)
App Privacy is, for each data type, a matter of deciding three things in order. When in doubt, asking yourself in this order keeps it tidy.
First, "is it collected at all?" Take inventory of what your own code collects plus what the embedded SDKs collect. The moment you add an ad SDK, identifiers and usage data fall onto the "collected" side.
Second, "is it linked to the user?" If you can identify an individual through an account or device ID, it is "linked." Purchase history is tied to the account, so I put RevenueCat-related data under "linked."
Third, "is it used to track across third parties?" This is the heaviest call, and it ties directly to ATT. Personalized ads qualify; plain operational analytics usually does not. Leaving this vague and "checking everything just in case" needlessly forces ATT and lowers your opt-in rate, so I find it more practical to narrow it down with a clear rationale.
Revisit the declaration whenever you update an SDK
The hard part of operations is that this isn't fill-once-and-forget. A major SDK update can quietly add new collected data types. Since I run six apps in parallel, I now always set aside time at the once-a-year SDK update to reconcile each vendor's data-collection document against my own declaration.
Lately I keep a single mapping table of my own — "data collected per SDK to App Privacy category" — and when I ship a new app I copy it and only fix the differences. That has cut mistakes far more than thinking it through from scratch every time.
Before you submit your next app, start by listing the SDKs you've embedded and opening each one's latest data-collection page. Those ten-odd minutes turn out to be the shortcut, rather than getting stuck at the declaration after the code is done. I hope this helps anyone stuck at the same spot.