If you've been shipping to the app store in the past month you will probably have seen this warning from apple
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(_:_:_:_:_:_:)
Heres where you can see that 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"
]
}
]
}
}
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.
- Open your ios project in Xcode
- Choose File > New File.
- Scroll down to the Resource section, and select App Privacy File type.
- Click Next.
- Check your app target in the Targets list.
- Click Create.
- 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>
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)
Awesome!
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!
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.