Skip to main content

Fixing ITMS-91061: Missing Privacy Manifests in Flutter iOS Builds

 The CI/CD pipeline reports a success, the .ipa uploads to TestFlight, and five minutes later, you receive the automated email from App Store Connect:

ITMS-91061: Missing Privacy Manifest - Your app includes a third-party SDK that does not include a privacy manifest, or your app uses a Required Reason API without declaring it.

As of Spring 2024, Apple enforces strict privacy manifest requirements. For Flutter applications, this is particularly volatile because your codebase is a wrapper around dozens of Objective-C/Swift CocoaPods, many of which haven't been updated to include the required PrivacyInfo.xcprivacy resource.

This guide details how to audit your Flutter dependencies, map the required reasons, and implement a compliant manifest to unblock your release pipeline.

The Root Cause: Required Reason APIs & Dependency Chains

Apple identifies "fingerprinting" risks through specific standard libraries—specifically file system access, user defaults, and boot time APIs. These are classified as Required Reason APIs.

When a Flutter plugin (e.g., shared_preferencespath_provider, or sqflite) compiles into your iOS runner, it links against these APIs.

  1. The Mechanism: During the App Store processing phase, Apple scans the binary symbols. If they detect symbols like UserDefaults but find no corresponding NSPrivacyAccessedAPITypes entry in a PrivacyInfo.xcprivacy file, the build is flagged.
  2. The Flutter Disconnect: While Flutter 3.19+ tooling attempts to aggregate manifests, it relies on plugin authors shipping the manifest in their Podspec resource bundles. If a plugin uses statfs (disk space) but relies on an older Podspec, the symbol exists in your binary, but the manifest does not.

To fix this, the host application (Runner) must authoritatively declare the usage of these APIs on behalf of its dependencies.

The Fix: Authoritative Privacy Manifest Injection

We will solve this by explicitly defining a PrivacyInfo.xcprivacy in the iOS runner that covers the missing declarations from lagging plugins.

Step 1: Identify the Offending APIs

Do not guess. Use nm to scan your compiled framework symbols to see exactly which APIs are triggering the requirement.

Navigate to your build artifacts (usually in build/ios/Release-iphoneos) and run this shell script to scan for common culprits:

# Navigate to your compiled frameworks
cd build/ios/Release-iphoneos/XCFrameworkIntermediates

# Scan for UserDefaults usage
grep -r "NSUserDefaults" .

# Scan for File Timestamp/Stat usage
grep -r "fstat" .
grep -r "statfs" .

If shared_preferences appears in the grep results, you need the UserDefaults category. If path_provider appears, you likely need the Disk Space category.

Step 2: Create the Privacy Manifest

In your Flutter project, navigate to ios/Runner. Create a new file named PrivacyInfo.xcprivacy. This is a standard Property List XML file.

You must ensure this file is included in the "Runner" target membership in Xcode.

Below is a robust manifest template covering the most common Flutter plugin requirements (path_providershared_preferences, and file handling).

<?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 IDFA for tracking. Most apps should be false. -->
    <key>NSPrivacyTracking</key>
    <false/>

    <!-- 2. Privacy Nutrition Labels -->
    <!-- This section mirrors what you select in App Store Connect "App Privacy" -->
    <key>NSPrivacyCollectedDataTypes</key>
    <array>
        <!-- Example: Coarse Location -->
        <dict>
            <key>NSPrivacyCollectedDataType</key>
            <string>NSPrivacyCollectedDataTypeLocationCoarse</string>
            <key>NSPrivacyCollectedDataTypeLinked</key>
            <false/>
            <key>NSPrivacyCollectedDataTypeTracking</key>
            <false/>
            <key>NSPrivacyCollectedDataTypePurposes</key>
            <array>
                <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
            </array>
        </dict>
    </array>

    <!-- 3. Required Reason APIs (The Critical Fix for ITMS-91061) -->
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
        <!-- CATEGORY: User Defaults -->
        <!-- Required by: shared_preferences, flutter_secure_storage -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- CA92: Access info from same app domain -->
                <string>CA92</string>
            </array>
        </dict>

        <!-- CATEGORY: Disk Space -->
        <!-- Required by: path_provider, sqflite, image_picker -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- E174: Check storage space to write files -->
                <string>E174</string>
            </array>
        </dict>

        <!-- CATEGORY: File Timestamp -->
        <!-- Required by: flutter_cache_manager, http clients caching to disk -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- C617: Inside app container, used for file validity/download -->
                <string>C617</string>
            </array>
        </dict>
        
        <!-- CATEGORY: System Boot Time -->
        <!-- Required by: device_info_plus, uuid -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <!-- 35F9: Measure time elapsed for events (uptime) -->
                <string>35F9</string>
            </array>
        </dict>
    </array>
</dict>
</plist>

Step 3: Validate Target Membership

Simply creating the file is not enough; it must be bundled.

  1. Open ios/Runner.xcworkspace.
  2. Select the PrivacyInfo.xcprivacy file in the project navigator.
  3. In the right-hand Inspector pane, look at Target Membership.
  4. Ensure the checkbox for Runner is checked.

Step 4: Verify via Privacy Report

Before submitting to the App Store again, generate a Privacy Report locally to confirm the merge.

  1. In Xcode, select Product -> Archive.
  2. Once the archive is complete, the Organizer window opens.
  3. Right-click your archive -> Generate Privacy Report.
  4. Save the PDF.

Inspect the PDF. You should see a section titled "App Privacy Configuration". Ensure that the APIs listed in your XML (UserDefaults, File Timestamp, etc.) appear there. If this section is empty or missing categories, the file is not linked correctly.

Why This Works

The App Store's validation logic aggregates privacy manifests. When you include PrivacyInfo.xcprivacy in the main app bundle (Runner.app), it acts as the top-level declaration.

Even if a Pod (static library or framework) accesses NSUserDefaults without its own internal manifest, Apple's validator accepts the host app's manifest as the declaration for that access. This effectively "pollyfills" the compliance requirements for outdated Flutter plugins until the maintainers update their Podspecs.

Conclusion

ITMS-91061 is a blockage in the release pipeline that cannot be ignored. While the Flutter ecosystem is slowly updating to include manifests natively in plugins, Release Engineers must manually intervene by creating a comprehensive PrivacyInfo.xcprivacy in the Runner target. This ensures that all "Required Reason APIs" accessed by your dependency tree are legally declared, allowing your build to pass automated validation.