Skip to main content

Implementing iOS Privacy Manifests in Flutter: Avoid App Store Rejections

 If you have submitted a Flutter app to TestFlight or the App Store recently, you likely encountered a warning—or a rejection—citing ITMS-91053: Missing API declaration.

Apple now enforces strict declaration requirements for "Required Reason APIs." Even if your Dart code never directly touches UserDefaults or file timestamps, your dependencies do. Specifically, the ubiquitous shared_preferences package relies on NSUserDefaults, and path_provider often triggers file timestamp checks.

Ignoring this will result in binary rejection. This guide covers the root cause and the specific implementation required to make your Flutter ios build compliant.

The Root Cause: Indirect API Usage

Apple's initiative is designed to prevent "fingerprinting"—the practice of using device signals to track users without consent. To enforce this, they flagged a specific set of standard iOS APIs (Required Reason APIs) that are frequently abused for fingerprinting.

The most common offenders in the Flutter ecosystem are:

  1. UserDefaults (NSPrivacyAccessedAPICategoryUserDefaults): Used by shared_preferences to store persistent key-value pairs.
  2. File Timestamp APIs (NSPrivacyAccessedAPICategoryFileTimestamp): Used by file system operations (like path_provider or sqflite) to manage caching or file integrity.

When you compile a Flutter app, the CocoaPods build process links these native plugins into your App Runner. Apple's static analysis detects calls to [[NSUserDefaults standardUserDefaults] ...] within your binary structure. Without a PrivacyInfo.xcprivacy file explicitly stating why you are using these APIs, the build is flagged as non-compliant.

Flutter 3.19+ introduced tooling to merge manifests from plugins, but the App Runner itself (your main application entry point) usually requires its own manifest to cover app-level configurations and ensure compliance logic is explicitly declared at the root level.

The Fix: Implementing PrivacyInfo.xcprivacy

You must add a Property List file named PrivacyInfo.xcprivacy to your Xcode project. This file declares the restricted API categories and the specific "Reason Codes" provided by Apple.

Step 1: Create the Privacy Manifest

  1. Open your project in Xcode (ios/Runner.xcworkspace).
  2. Right-click on the Runner folder (the yellow folder icon, not the blue project icon) in the Project Navigator.
  3. Select New File.
  4. Search for "App Privacy" and select App Privacy File.
  5. Name it exactly PrivacyInfo (Xcode will append .xcprivacy).
  6. Crucial: Ensure "Runner" is checked in the Targets list at the bottom of the dialog.

Step 2: Configure Required Reasons

While you can edit this using the Xcode GUI, it is safer and faster to edit the raw XML source to ensure keys are exact.

Right-click the PrivacyInfo.xcprivacy file in Xcode > Open As > Source Code.

Replace the contents with the following XML. This configuration covers the standard usage for shared_preferences (App storage) and file system access.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 1. Tracking Usage Description -->
    <!-- set to true ONLY if you use App Tracking Transparency (IDFA) -->
    <key>NSPrivacyTracking</key>
    <false/>

    <!-- 2. Collected Data Types -->
    <!-- Define what data your app collects. Empty array if none beyond basics. -->
    <key>NSPrivacyCollectedDataTypes</key>
    <array/>

    <!-- 3. Accessed API Types (The Critical Section) -->
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
        <!-- A. User Defaults Declaration -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- CA92.1: Access info from same app/group to enable app functionality -->
                <string>CA92.1</string>
            </array>
        </dict>

        <!-- B. File Timestamp Declaration -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- C617.1: Inside app container, e.g. for cache expiration or download checks -->
                <string>C617.1</string>
            </array>
        </dict>
        
        <!-- C. System Boot Time Declaration (Common in measuring uptime/performance) -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- 35F9.1: Measure time elapsed for app functionality (e.g. timers) -->
                <string>35F9.1</string>
            </array>
        </dict>
    </array>
</dict>
</plist>

Step 3: Verification

Once the file is saved:

  1. Switch back to Open As > Property List.
  2. Verify the hierarchy. It should look like this:
    • App Privacy Accessed API Types (Array)
      • Item 0 (Dictionary)
        • App Privacy Accessed API TypeNSPrivacyAccessedAPICategoryUserDefaults
        • App Privacy Accessed API ReasonsCA92.1
  3. Clean Build Folder: Product > Clean Build Folder (Shift + Cmd + K).
  4. Archive: Product > Archive.

Step 4: Validate the Privacy Report

Before submitting to App Store Connect, you can verify that the manifest is correctly bundled.

  1. In the Xcode Organizer (Window > Organizer), select your generated Archive.
  2. Right-click the Archive > Show in Finder.
  3. Right-click the .xcarchive file > Show Package Contents.
  4. Navigate to /Products/Applications/Runner.app.
  5. Right-click Runner.app > Show Package Contents.
  6. Ensure PrivacyInfo.xcprivacy exists in this root directory.

Additionally, Xcode 15+ allows you to generate a privacy report: Product > Archive -> Right-click the Archive in Organizer -> Generate Privacy Report. This PDF will confirm if your declared reasons match the symbols found in the binary.

Breakdown of Reason Codes

Understanding the codes used in the XML above is critical for compliance. Do not simply copy-paste if your app does something non-standard.

  • CA92.1 (User Defaults): This is the "safe" code for most Flutter apps. It asserts that your app uses UserDefaults only to read/write data local to the app itself. If you are using App Groups to share data with a Widget or App Clip, usage falls under this as well.
  • C617.1 (File Timestamp): This asserts that you are checking file creation/modification dates strictly for files inside the app's sandbox (e.g., checking if a cached image is too old and needs re-fetching).
  • 35F9.1 (System Boot Time): Flutter engine components or packages like device_info_plus may access boot time to calculate relative timestamps for event logging.

Conclusion

The PrivacyInfo.xcprivacy file is no longer optional. For Flutter developers, the abstraction layer of Dart often hides the native APIs being invoked, leading to surprise rejections. By explicitly declaring your API usage in the Runner's target, you satisfy Apple's static analysis requirements and ensure your CI/CD pipeline remains green.

If you update your plugins in the future (specifically shared_preferences or webview_flutter), check their changelogs; newer versions may bundle their own privacy manifests which Xcode will merge with yours during the build process, but maintaining a root-level manifest remains best practice for application-specific logic.