You finally got your Rork-generated iOS build through the App Store Connect upload, watched TestFlight finish processing, and then a few minutes later an email from Apple appears with the subject ITMS-90683: Missing Purpose String in Info.plist. Many developers shipping their first Rork app to TestFlight pause right here. Since 2014 I have been shipping personal iOS apps on the App Store, with apps that have accumulated 50 million downloads in total, and I still get this exact email a few times a year whenever I add a new SDK.
What makes it tricky is that it is not a build failure. Your binary actually arrived at App Store Connect successfully. The rejection happens later, when Apple statically analyzes the Info.plist and runs symbol-level checks on the binary. Think of it as an automated pre-review rejection rather than a build problem.
In this post I will walk through why ITMS-90683 happens for Rork-generated projects, how to fix it the right way via app.json, and how to avoid the cycle of submit-and-reject the next time you add a new dependency.
Why ITMS-90683 actually fires
Apple returns this error when your binary references one of the Privacy-Sensitive Data APIs without a matching Purpose String declared in Info.plist. The areas Apple watches include:
- Photo library, camera, microphone
- Location, motion, health data
- Contacts, calendars, reminders
- Bluetooth, local network, FaceID
- Tracking (IDFA), speech recognition, Siri
The critical point: it does not matter whether you wrote the code that calls the API. If any third-party SDK in your bundle references the API, Apple will detect it and demand a Purpose String. Rork projects pull in React Native packages through npx expo install, and several common dependencies quietly reference camera, microphone, or IDFA APIs internally. Ad SDKs are notorious for this.
So "I do not use that feature" is not a valid response. If the symbol is in your binary, you write the string.
Read the email carefully — it lists the exact keys
The rejection email always names every missing key. A typical entry looks like this:
The app's Info.plist must contain an NSCameraUsageDescription key
with a string value explaining to the user how the app uses this data.
NSCameraUsageDescription is the literal key you need to add. The email often lists several, so copy them all down before you do anything else.
Here are the keys you are most likely to encounter, paired with the scenarios where each one is required.
NSCameraUsageDescription— photo capture, QR scanning, live streaming SDKsNSPhotoLibraryUsageDescription— selecting from the photo libraryNSPhotoLibraryAddUsageDescription— saving generated images to the libraryNSMicrophoneUsageDescription— voice recording, video capture, voice SDKsNSLocationWhenInUseUsageDescription— foreground locationNSLocationAlwaysAndWhenInUseUsageDescription— background locationNSBluetoothAlwaysUsageDescription— BLE peripheral communicationNSLocalNetworkUsageDescription— Bonjour LAN communication, ChromecastNSFaceIDUsageDescription— FaceID authenticationNSUserTrackingUsageDescription— IDFA access (commonly required by AdMob, Firebase Analytics)NSContactsUsageDescription— contact access
The right way to add them in a Rork project
The single biggest mistake is editing Info.plist directly.
Rork uses the Expo build system (EAS Build) under the hood, and the build regenerates Info.plist from app.json (or app.config.js) on every run. Anything you change manually in Xcode will be overwritten on the next build. I once burned half a day on three consecutive rejections before realizing I had been editing the wrong file.
The correct location is the expo.ios.infoPlist section of app.json. For an app that uses AdMob, photo picker, and FaceID login, it looks like this:
{
"expo": {
"name": "MyRorkApp",
"ios": {
"bundleIdentifier": "com.example.myrorkapp",
"infoPlist": {
"NSCameraUsageDescription": "Used to scan QR codes with the camera.",
"NSPhotoLibraryUsageDescription": "Used to select your profile picture from your photo library.",
"NSPhotoLibraryAddUsageDescription": "Used to save generated images to your photo library.",
"NSFaceIDUsageDescription": "Used to sign you in securely with Face ID.",
"NSUserTrackingUsageDescription": "Used to deliver more relevant ads by associating your activity across other apps and websites."
}
}
}
}Once added, your next EAS Build will reflect them automatically. You do not have to open Xcode to verify, but greping ios/MyRorkApp/Info.plist after a local prebuild is a good sanity check.
The wording quality matters for App Review
Strictly speaking, you can write test and clear ITMS-90683. But sloppy strings will get caught later, during the actual App Review stage. Real rejection reasons I have received include:
- A string that only said "uses the camera in this app" without explaining what for
- Only an English version, with no Japanese localization
- Wording that did not match the actual feature (e.g., "used for photography" when the app only picks from the library)
A good test: when a user sees the permission alert, can they tell instantly why the app needs that data? Write one short, specific sentence per key, in a consistent tone, and you are usually fine.
If you need to localize the strings, you can either combine expo-localization with InfoPlist.strings, or on Expo SDK 50 and later, declare locales directly in app.json:
{
"expo": {
"locales": {
"en": "./languages/en.json",
"ja": "./languages/ja.json"
}
}
}Then create languages/en.json like so:
{
"NSCameraUsageDescription": "Used to scan QR codes with the camera.",
"NSPhotoLibraryUsageDescription": "Used to select your profile picture from your photo library."
}Create ja.json with the same keys translated, and iOS will surface the appropriate string by region. If you are planning to ship in multiple languages, set this up from the start; bolting it on later is painful.
When the SDK implicitly requires it
Half the time the problem is "a key I never use is listed in the email." If the email demands NSUserTrackingUsageDescription but you never called requestTrackingAuthorization anywhere, suspect your ad SDK or analytics SDK first.
A few real cases from my own apps:
react-native-google-mobile-ads(AdMob) — needsNSUserTrackingUsageDescriptionexpo-image-picker— needsNSPhotoLibraryUsageDescriptionandNSCameraUsageDescriptionexpo-camera— needsNSCameraUsageDescriptionandNSMicrophoneUsageDescription(for video)expo-local-authentication— needsNSFaceIDUsageDescriptionreact-native-zeroconf— needsNSLocalNetworkUsageDescriptionandNSBonjourServices@react-native-firebase/analytics— needsNSUserTrackingUsageDescriptionwhen IDFA is enabled
Some SDKs document this; many do not. The most reliable approach is to add whatever keys the rejection email lists, even if you cannot trace which SDK they came from. The symbol is in your binary, so you owe the user an explanation.
Resubmitting after the fix
After updating app.json, follow these steps to resubmit.
# 1. Bump the build number (required)
# In app.json, increment expo.ios.buildNumber
# e.g. "1.0.3" -> "1.0.4"
# 2. Trigger a new EAS Build
eas build --platform ios --profile production
# 3. After the build completes, submit
eas submit --platform ios --latestA common follow-on mistake is forgetting to bump buildNumber and getting hit with ERROR ITMS-90189: Redundant Binary Upload. App Store Connect will not accept a duplicate build number. If you are building via Rork's pipeline, the safest pattern is: tweak app.json directly after Rork generates the project, then trigger a new build.
5 to 15 minutes after submit, check the Activity tab in App Store Connect. If the new build appears as "Ready to Test", you are clear. If ITMS-90683 returns, recheck the email against the keys you added — it is usually a typo or a missed key.
A workflow to avoid the cycle entirely
Learning about a missing Purpose String only after a rejection email is exhausting. I now run this short checklist every time I install a new SDK:
- Right after
npx expo install, read the SDK README for iOS Info.plist requirements - If anything is listed, add it to
app.jsonimmediately (delaying always means forgetting) - Locally, run
npx expo prebuild --platform ios --cleanand grep the generatedios/*/Info.plistfor your keys - Before submitting, verify the EAS Build log shows
Bundle was successfully built
Running expo prebuild once locally is what made the biggest difference for me. With cloud builds only, you never see the intermediate Info.plist, which means you only discover gaps after Apple does.
One layer deeper — the Privacy Manifest connection
Starting with iOS 17, Apple introduced the Privacy Manifest (PrivacyInfo.xcprivacy), which sits next to Info.plist and declares data collection purposes at a different layer. Clearing ITMS-90683 alone does not guarantee Privacy Manifest compliance; you may still see warnings like ITMS-91053 about missing required reason API declarations.
In short: Info.plist Purpose Strings are the user-facing alert copy, while the Privacy Manifest is the structured declaration Apple uses for review and the App Store privacy label. For a Rork project pulling in major React Native SDKs, check whether each SDK ships its own PrivacyInfo.xcprivacy — that single check saves a later round of rejections.
Every time I see ITMS-90683 I remind myself that it is not really saying "your code is wrong." It is saying "the user is missing an explanation." Writing five to ten short sentences buys real trust at install time, and rejection emails become an opportunity to write better copy rather than a setback.
I hope this saves a few hours for anyone hitting the same wall. Thanks for reading.