61 lines
5.6 KiB
Markdown
61 lines
5.6 KiB
Markdown
---
|
|
name: privacy-manifest
|
|
description: Add and check Apple privacy manifests (PrivacyInfo.xcprivacy) for an iOS app or game - required-reason API categories (UserDefaults, file timestamps, system boot time, disk space, active keyboards) with approved reason codes, tracking flag and domains, collected data types, and third-party SDK manifests (ad networks, analytics, Firebase, Unity, Capacitor plugins). Use when App Store Connect sends ITMS-91053 "Missing API declaration" or ITMS-91061 "Missing privacy manifest", or before submitting an iOS build.
|
|
---
|
|
|
|
# Privacy manifest (iOS)
|
|
|
|
`storebound scan` runs the same checks as part of its report (no manifest, undeclared required-reason APIs, SDKs older than their first manifest version) and proposes NSPrivacyTracking and the collected data types from the detected SDKs.
|
|
|
|
Scan: `storebound privacy-manifest --ios ios/App` (local, no credentials; `--propose` prints the app manifest
|
|
storebound proposes). Capacitor / Cordova / React Native plugins
|
|
live outside the iOS folder; they are found the way Node resolves them: the dependencies of the app's package.json
|
|
(next to capacitor.config) in the node_modules there and in every folder above it up to the repo root (so a monorepo's
|
|
`apps/native/node_modules` works), workspace packages (package.json `workspaces`, Bun / npm / yarn), local SwiftPM
|
|
packages (Capacitor 8's CapApp-SPM `Package.swift`), Pods and SwiftPM checkouts. `--sdk-dirs a,b` adds folders it
|
|
cannot find. Exit 1 when something blocks upload.
|
|
|
|
## What Apple checks at upload
|
|
- **ITMS-91064 Invalid tracking information**: "ITMS-91064: Invalid tracking information - A PrivacyInfo.xcprivacy file contains invalid tracking information at the following path: "PrivacyInfo.xcprivacy". NSPrivacyTracking must be true if NSPrivacyTrackingDomains isn't empty. Keys and values in your app's privacy manifests must be valid." Raised for
|
|
true with an empty list as well as for domains with false, in the app's manifest or any SDK's. `storebound
|
|
apple upload` checks every manifest in the IPA (app root, Frameworks, bundles; XML or binary) before sending it,
|
|
`apple build` after the export, `check` / `scan` / `privacy-manifest` in the project.
|
|
- **ITMS-91053 Missing API declaration**: the binary uses a required-reason API and no manifest in the app or in the
|
|
SDK that calls it declares the category with a reason.
|
|
- **ITMS-91061 Missing privacy manifest**: an SDK from Apple's list of commonly used SDKs (Firebase, Google utilities,
|
|
Alamofire, OneSignal, Unity framework, Capacitor, Flutter ...) is linked without its own manifest (and signature for
|
|
binary SDKs). Fix by updating the SDK, not by declaring it in your app's manifest.
|
|
|
|
## Steps
|
|
1. Xcode > File > New > File > App Privacy (PrivacyInfo.xcprivacy), target = the app.
|
|
2. Run the scan. For each used category add `NSPrivacyAccessedAPITypes` > item with `NSPrivacyAccessedAPIType` and
|
|
`NSPrivacyAccessedAPITypeReasons`. Typical for apps and games:
|
|
- `NSPrivacyAccessedAPICategoryUserDefaults` + `CA92.1` (settings / saves read and written by the app itself;
|
|
Capacitor Preferences and most engines use UserDefaults)
|
|
- `NSPrivacyAccessedAPICategoryFileTimestamp` + `C617.1` (timestamps of files in the app container)
|
|
- `NSPrivacyAccessedAPICategorySystemBootTime` + `35F9.1` (elapsed time between events)
|
|
- `NSPrivacyAccessedAPICategoryDiskSpace` + `E174.1` (check space before writing)
|
|
Pick the reason that is true; read Apple's reason list ("Describing use of required reason API") if unsure.
|
|
3. Tracking (ITMS-91064): in every manifest `NSPrivacyTracking` and `NSPrivacyTrackingDomains` must agree: true
|
|
needs a non-empty list of the domains the code of THAT bundle sends tracking data to, and a non-empty list needs
|
|
true. When the tracking comes from an SDK (ad network, mediation such as LevelPlay / AppLovin MAX / AdMob,
|
|
attribution), the SDK's own manifest declares it and the app's manifest says **false**, while App Privacy in App
|
|
Store Connect still says "used for tracking" and the ATT prompt stays (`store-declarations`). Never set the app's
|
|
manifest to true with an empty list: Apple refuses the build as Invalid Binary a minute after the upload. Only
|
|
when the app's own code tracks, list its domains in `appPrivacy.trackingDomains`;
|
|
`storebound privacy-manifest --ios ios/App --propose` prints the manifest with true only then.
|
|
4. `NSPrivacyCollectedDataTypes`: what the app itself collects; SDK manifests cover their own collection. Xcode's
|
|
Privacy Report (Product > Archive > Generate Privacy Report) merges all manifests; compare it with App Privacy.
|
|
5. SDKs flagged "ships no PrivacyInfo.xcprivacy": update to a version that has one (ad networks and mediation
|
|
adapters added manifests in 2024 releases; see `ads-mediation-checks` for adapter / SDK version pairs).
|
|
6. Re-run the scan, archive, upload; Apple's email lists any remaining ITMS-910xx issue per framework.
|
|
|
|
## Gotchas
|
|
- The scan is a text search: it can miss calls inside binary frameworks (their manifest must cover them) and flag a
|
|
harmless symbol name; confirm before adding a category you do not use.
|
|
- A manifest must be in the **app target's** Copy Bundle Resources, not only in the project.
|
|
- `check` saying "tracking declared by the <SDK> manifest" (info) is the correct setup for SDK tracking; a warning
|
|
that no manifest declares tracking means: check the SDK version ships its manifest (`pod install`, resolve
|
|
packages), not "set the app's manifest to true".
|
|
- Static libraries cannot carry a manifest bundle by themselves; the vendor ships a resource bundle or you merge their
|
|
declarations into the app's manifest as their docs say.
|