DEV Community

Danny
Danny

Posted on • Updated on

Apple privacy manifest for React Native

If you've been shipping to the app store in the past month you will probably have seen this warning from apple

Image description

This looks scary and confusing but its actually a lot simpler than it looks.

What those docs mean

If you follow one of the links in the email it will take you to a wall of text about the required reason API.

All this means is that now for apis that could be used to identify a user you must provide a reason for its use.

In the documentation here you will find the different apis that match this description.

For example for file access these are the apis that would need a reason

creationDate
modificationDate
fileModificationDate
contentModificationDateKey
creationDateKey
getattrlist(_:_:_:_:_:)
getattrlistbulk(_:_:_:_:_:)
fgetattrlist(_:_:_:_:_:)
stat
fstat(_:_:)
fstatat(_:_:_:_:)
lstat(_:_:)
getattrlistat(_:_:_:_:_:_:)
Enter fullscreen mode Exit fullscreen mode

Heres where you can see that in the docs

image showing this in the docs

What to do

For most people though you probably aren't using any of these directly and you just need to add reasons for the apis that React Native uses internally.

A member of the community already found out the reasons required and shared them here

If you're using expo you can update to 50.0.17 and add this to your app.json (or equivalent app.config.js etc)

"ios": {
  "privacyManifests": {
    "NSPrivacyAccessedAPITypes": [
      {
        "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",
        "NSPrivacyAccessedAPITypeReasons": [
          "C617.1"
        ]
      },
      {
        "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategorySystemBootTime",
        "NSPrivacyAccessedAPITypeReasons": [
          "35F9.1"
        ]
      },
      {
        "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",
        "NSPrivacyAccessedAPITypeReasons": [
          "E174.1"
        ]
      },
      {
        "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
        "NSPrivacyAccessedAPITypeReasons": [
          "CA92.1"
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Check the gist in the tweet above for the full example.

There is also a guide from expo on how to do this in case you're still stuck https://docs.expo.dev/guides/apple-privacy/

Expo pre v50

For earlier versions of expo check out this comment from @getKonstantin:

Comment for #27796

UPDATE: We just updated the implementation to this version of code as the prev implementation was placing privacy config into source build phase instead of bundle resources.


RE: Pre-Expo 50

For ppl who aren't on Expo 50 yet. We've taken the new plugin from official Expo, stripped types to simplify to easily runnable one JS file. And we also added a safeguard to fail in case if you're upgrading to Expo 50 (we usually use defensive checks to just rid of things once we know official support is there).

And got it working for Expo 48 version that we're running ⬇️

  • Copy the file to your project
  • Add it to config.plugins (as it's a custom plugin you need to apply)
  • Define config.ios.privacyManifest in your config as per new Expo docs.

Run npx expo prebuild --clean (and --no-install for ppl like me who don't like to wait for CocoaPods to be installed), it should generate the PrivacyInfo.xcprivacy to your project.

Plugin:

const plist = require("@expo/plist").default;
const fs = require("fs");
const path = require("path");

const {
  addResourceFileToGroup,
  getProjectName,
} = require("@expo/config-plugins/build/ios/utils/Xcodeproj");
const { withXcodeProject } = require("@expo/config-plugins");

function withPrivacyInfoInternal(config) {
  const privacyManifests = config.ios?.privacyManifests;
  if (!privacyManifests) {
    return config;
  }

  return withXcodeProject(config, (projectConfig) => {
    return setPrivacyInfo(projectConfig, privacyManifests);
  });
}

function setPrivacyInfo(projectConfig, privacyManifests) {
  const { projectRoot, platformProjectRoot } = projectConfig.modRequest;

  const projectName = getProjectName(projectRoot);

  const privacyFilePath = path.join(
    platformProjectRoot,
    projectName,
    "PrivacyInfo.xcprivacy",
  );

  const existingFileContent = getFileContents(privacyFilePath);

  const parsedContent = existingFileContent
    ? plist.parse(existingFileContent)
    : {};
  const mergedContent = mergePrivacyInfo(parsedContent, privacyManifests);
  const contents = plist.build(mergedContent);

  ensureFileExists(privacyFilePath, contents);

  if (!projectConfig.modResults.hasFile(privacyFilePath)) {
    projectConfig.modResults = addResourceFileToGroup({
      filepath: privacyFilePath,
      groupName: projectName,
      project: projectConfig.modResults,
      isBuildFile: true,
      verbose: true,
    });
  }

  return projectConfig;
}

function getFileContents(filePath) {
  if (!fs.existsSync(filePath)) {
    return null;
  }
  return fs.readFileSync(filePath, { encoding: "utf8" });
}

function ensureFileExists(filePath, contents) {
  if (!fs.existsSync(path.dirname(filePath))) {
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
  }
  fs.writeFileSync(filePath, contents);
}

function mergePrivacyInfo(existing, privacyManifests) {
  let {
    NSPrivacyAccessedAPITypes = [],
    NSPrivacyCollectedDataTypes = [],
    NSPrivacyTracking = false,
    NSPrivacyTrackingDomains = [],
  } = structuredClone(existing);
  // tracking is a boolean, so we can just overwrite it
  NSPrivacyTracking =
    privacyManifests.NSPrivacyTracking ?? existing.NSPrivacyTracking ?? false;
  // merge the api types – for each type ensure the key is in the array, and if it is add the reason if it's not there
  privacyManifests.NSPrivacyAccessedAPITypes?.forEach((newType) => {
    const existingType = NSPrivacyAccessedAPITypes.find(
      (t) => t.NSPrivacyAccessedAPIType === newType.NSPrivacyAccessedAPIType,
    );
    if (!existingType) {
      NSPrivacyAccessedAPITypes.push(newType);
    } else {
      existingType.NSPrivacyAccessedAPITypeReasons = [
        ...new Set(
          existingType?.NSPrivacyAccessedAPITypeReasons?.concat(
            ...newType.NSPrivacyAccessedAPITypeReasons,
          ),
        ),
      ];
    }
  });
  // merge the collected data types – for each type ensure the key is in the array, and if it is add the purposes if it's not there
  privacyManifests.NSPrivacyCollectedDataTypes?.forEach((newType) => {
    const existingType = NSPrivacyCollectedDataTypes.find(
      (t) =>
        t.NSPrivacyCollectedDataType === newType.NSPrivacyCollectedDataType,
    );
    if (!existingType) {
      NSPrivacyCollectedDataTypes.push(newType);
    } else {
      existingType.NSPrivacyCollectedDataTypePurposes = [
        ...new Set(
          existingType?.NSPrivacyCollectedDataTypePurposes?.concat(
            ...newType.NSPrivacyCollectedDataTypePurposes,
          ),
        ),
      ];
    }
  });
  // merge the tracking domains
  NSPrivacyTrackingDomains = [
    ...new Set(
      NSPrivacyTrackingDomains.concat(
        privacyManifests.NSPrivacyTrackingDomains ?? [],
      ),
    ),
  ];

  return {
    NSPrivacyAccessedAPITypes,
    NSPrivacyCollectedDataTypes,
    NSPrivacyTracking,
    NSPrivacyTrackingDomains,
  };
}

/**
 * This was added at a time before Expo supported privacy manifests in versions below Expo 50.
 *
 * Copied from https://github.com/expo/expo/blob/97ca37377d70507d2ce30545f0a35ac73d172dcc/packages/%40expo/config-plugins/src/ios/PrivacyInfo.ts
 */
function withPrivacyInfo(config) {
  if (config.sdkVersion.startsWith("50")) {
    throw new Error(
      `withPrivacyInfo plugin is unneeded for Expo SDK 50+, official support was added: https://github.com/expo/expo/pull/28005`,
    );
  }

  return withPrivacyInfoInternal(config);
}

module.exports = withPrivacyInfo;

RN CLI

If you're not using expo then you can follow the steps to create a privacy manifest in XCode.

  1. Open your ios project in Xcode
  2. Choose File > New File.
  3. Scroll down to the Resource section, and select App Privacy File type.
  4. Click Next.
  5. Check your app target in the Targets list.
  6. Click Create.
  7. under NSPrivacyAccessedAPITypes add the apis and reasons from above

Your PrivacyInfo.xcprivacy file should get something like this

<?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>
  <key>NSPrivacyCollectedDataTypes</key>
  <array>
  </array>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>C617.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
      </array>
    </dict>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>35F9.1</string>
      </array>
    </dict>
  <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>E174.1</string>
      </array>
    </dict>
  </array>
  <key>NSPrivacyTracking</key>
  <false/>
</dict>
</plist>
Enter fullscreen mode Exit fullscreen mode

This is a modified version of the file from the react native cli template

What do those values mean?

These values come from the apple documentation, heres what they mean

File access time

API Name: NSPrivacyAccessedAPICategoryFileTimestamp

C617.1: Declare this reason to access the timestamps, size, or other metadata of files inside the app container, app group container, or the app’s CloudKit container.

System boot time

API Name: NSPrivacyAccessedAPICategorySystemBootTime

35F9.1: Declare this reason to access the system boot time in order to measure the amount of time that has elapsed between events that occurred within the app or to perform calculations to enable timers.

Disk space

API Name: NSPrivacyAccessedAPICategoryDiskSpace

E174.1: Declare this reason to check whether there is sufficient disk space to write files, or to check whether the disk space is low so that the app can delete files when the disk space is low. The app must behave differently based on disk space in a way that is observable to users.

User defaults

API Name: NSPrivacyAccessedAPICategoryUserDefaults

CA92.1: Declare this reason to access user defaults to read and write information that is only accessible to the app itself.

Summary

Hopefully you should now be able to get your application warning free, but do make sure you check these values for yourself since you may be using other apis.

Let me know if this works for you or if I got anything wrong here.

twitter: @Danny_H_W
github: dannyhw

Top comments (3)

Collapse
 
gautham495 profile image
Gautham Vijayan

Awesome!

Collapse
 
cth profile image
christof

How did you choose the NSPrivacyAccessedAPITypeReasons? These are all third-party packages that access these packages (typically). How did you decide on each of those reasons? Thanks!

Collapse
 
dannyhw profile image
Danny • Edited

For me I searched my code for the functions related to those apple apis and was able to see for example that system boot time was always used for timers. However it’s also just that the reasons are pretty generic, its like are you (a) spying on users or (b) not spying on users. The warnings I got also were also exactly the same as catlins so I was able to use what he shared as a starting point and it ended up being correct for me too. Unless you’re using custom native code I would expect yours to be the same too because its just coming from react native. Third party packages must include their own manifest files now.