◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-21Intermediate

Rork iOS App Rejected with ITMS-90683 on TestFlight — How to Fix Missing Purpose Strings via app.json

If your Rork-built iOS app passes upload but gets an email titled ITMS-90683: Missing Purpose String in Info.plist, this guide walks through the real cause and the permanent fix via app.json, based on 12 years of shipping personal iOS apps with the same problem appearing across new SDK updates.

ITMS-90683Info.plist6TestFlight15App Store Connect15iOS116Expo219Rork585Privacy10Troubleshooting43

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 SDKs
  • NSPhotoLibraryUsageDescription — selecting from the photo library
  • NSPhotoLibraryAddUsageDescription — saving generated images to the library
  • NSMicrophoneUsageDescription — voice recording, video capture, voice SDKs
  • NSLocationWhenInUseUsageDescription — foreground location
  • NSLocationAlwaysAndWhenInUseUsageDescription — background location
  • NSBluetoothAlwaysUsageDescription — BLE peripheral communication
  • NSLocalNetworkUsageDescription — Bonjour LAN communication, Chromecast
  • NSFaceIDUsageDescription — FaceID authentication
  • NSUserTrackingUsageDescription — 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) — needs NSUserTrackingUsageDescription
  • expo-image-picker — needs NSPhotoLibraryUsageDescription and NSCameraUsageDescription
  • expo-camera — needs NSCameraUsageDescription and NSMicrophoneUsageDescription (for video)
  • expo-local-authentication — needs NSFaceIDUsageDescription
  • react-native-zeroconf — needs NSLocalNetworkUsageDescription and NSBonjourServices
  • @react-native-firebase/analytics — needs NSUserTrackingUsageDescription when 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 --latest

A 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:

  1. Right after npx expo install, read the SDK README for iOS Info.plist requirements
  2. If anything is listed, add it to app.json immediately (delaying always means forgetting)
  3. Locally, run npx expo prebuild --platform ios --clean and grep the generated ios/*/Info.plist for your keys
  4. 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-06-17
Your Rork App Is Stuck on "Processing" in App Store Connect — A Field Guide to Getting It Unstuck
You uploaded a Rork-built app to App Store Connect, but the build sits on Processing for hours and never shows up in TestFlight or for submission. Here is the exact diagnostic order I follow, how to read the ITMS error codes, and a short script that reads the processing state from the API — with working code.
⬡ Dev Tools2026-05-18
Fixing Linking.canOpenURL False Returns on iOS in Rork
Production iOS builds silently route every user to the mobile web instead of opening TikTok or X? This is almost always a missing LSApplicationQueriesSchemes entry. Here is the full fix path for Rork.