Security

    Does Apple review the source code of React Native apps?

    App Store Connect showing an ITMS-90338 non-public API rejection on a React Native build, with the symbol list returned by Apple's static binary scan

    You submit a React Native build to App Store Connect, the review comes back two days later with ITMS-90338 or a 2.5.1 rejection, and the question lands: did Apple actually read your JavaScript or your native code? The answer matters because it changes what you have to fix before resubmitting.

    Short answer

    The honest answer is no. App Review does not receive the source code of a React Native app, the Xcode project, or the contents of node_modules. What Apple receives is the IPA, the compiled binary that Xcode produced, plus the embedded JavaScript bundle and any signed entitlements. App Review runs static analysis on that package looking for forbidden symbol names, private API calls, malware patterns, and disallowed behaviors per App Review Guideline 2.5.1.

    What you should know

    • Apple reviews the binary, not the source. The IPA contains compiled Mach-O code and a packaged JS bundle. The reviewer never sees Swift, Objective-C, or TypeScript files from the project.
    • Static analysis scans for private API symbols. Rejection code ITMS-90338 is triggered when the binary contains selector names that match Apple's private API list.
    • The JavaScript bundle is part of the binary surface. Apple scans the bundled .jsbundle alongside compiled code for forbidden patterns and suspicious strings.
    • OTA updates to the JS bundle are permitted. The Developer Program License Agreement Section 3.3(b) allows interpreted code updates that do not change the app's primary purpose.
    • False positives are common. Generic method names in third-party libraries can collide with private API selectors and trigger 90338 even when no private API is actually called.
    • Manual review is short. App Review staff spend roughly five to fifteen minutes per app on average; the automated static scan does the heavy lifting.

    How does App Review actually scan a React Native binary?

    The short answer is that App Review applies a two-stage pipeline to every IPA upload. The first stage is fully automated and runs as soon as the build clears Transporter; the second is a human reviewer who opens the app and exercises it on a real device.

    The mechanism for stage one is static analysis on the Mach-O executable and the embedded resources. Apple's tooling parses the binary, enumerates Objective-C selectors, dynamic symbol table entries, and string literals. It compares those entries against an internal list of private API symbol names and signature patterns. Per Apple's App Review Guidelines, section 2.5.1, apps may only use public APIs. The scanner produces ITMS error codes like ITMS-90338 ("Non-public API usage") when it finds matches.

    The mechanism for stage two is human. A reviewer installs the IPA on a test device, opens it, taps through the main flows, fills any login screen, and exercises payment paths. They watch for crashes, misleading metadata, missing privacy disclosures, and behavior that contradicts the App Store Connect submission. The reviewer does not have your repo. They are working from the same binary the automated scan ran against.

    The limit is that neither stage examines the source code of the project. The scan operates on the compiled artifact and the resources Xcode packaged into the IPA. The reviewer operates on a runtime build.

    StageWhat it inspectsWhat it does not see
    Automated static scanIPA Mach-O, embedded JS bundle, Privacy Manifest, entitlements, asset catalogSource code, node_modules, Xcode project files
    Human reviewerRuntime behavior on a test device, App Store Connect metadata, in-app purchase flowsSource code, debug symbols, commit history
    Neither stagen/aThe project repository or the build server logs

    What exactly does the scanner look for inside the IPA?

    The short answer is forbidden symbols, forbidden behaviors, missing declarations, and mismatches between the binary and the App Store Connect metadata.

    The first thing the scan does is enumerate symbol references. For Objective-C selectors, the scan compares the selectors in the binary against Apple's private API list. Per the React Native issue tracker, Apple has flagged React Native builds for selectors named centerY, hide:, isPassthrough, onSuccess:, permissionType, removeValuesForKeys:completion:, setLabelText:, show:, valueOffset, and viewManager. Those are not actually private API calls in most cases. They are method names in third-party libraries that happen to overlap with the private API list. The scanner does not distinguish.

    The second thing the scan does is check the Privacy Manifest, the entitlement list, and the required reason API declarations. An IPA that links the file timestamp API without declaring a reason in the Privacy Manifest triggers a separate ITMS error.

    The third thing the scan does is look for download-and-execute patterns. Per Guideline 2.5.2, apps may not download or execute code that changes their primary purpose. A React Native build that loads remote JS at runtime through a hot-update mechanism is allowed under DPLA 3.3(b), but only if the new bundle calls the same audited native APIs already in the binary.

    The fourth thing the scan does is sample the JavaScript bundle. The bundle is part of the IPA, and forbidden strings (for example, calls into private bridge methods or web view hooks that try to reach iOS internals) can be picked up by string-search heuristics in the bundle.

    What does the JavaScript bundle have to do with App Review?

    The short answer is that the .jsbundle ships inside the binary and Apple treats it as part of the reviewed app.

    The mechanism is that React Native embeds the JavaScript bundle as a resource in the app bundle, typically main.jsbundle, generated by Metro at build time. When the app launches on the test device, the JavaScript runtime loads that file. From the reviewer's perspective, the JS is application code, no different from compiled Swift or Objective-C.

    That has two practical consequences. First, the bundled JS is included in the static scan. Strings, function names, and embedded URLs can flag suspicious patterns. Second, the reviewer sees the runtime behavior the JS produces. If the JS triggers a screen Apple considers misleading or violating a guideline, the build is rejected on that behavior even though the JS itself does not directly touch the device hardware.

    The limit, per Bitrise's policy explainer on OTA updates, is that subsequent OTA updates to the JS bundle do not go through App Review. Apple permits this because the JS can only call into the native APIs already audited at the initial submission. The 2017 removal of apps using Rollout.io is the canonical example of where Apple drew the line: Rollout.io modified the Objective-C runtime directly to reach private APIs, not just replaced a JS bundle.

    What is ITMS-90338 and how does it apply to React Native?

    The short answer is that ITMS-90338 is the App Store Connect error for non-public API usage detected during binary analysis.

    The mechanism is the symbol scan described above. App Store Connect emits ITMS-90338 with a list of the offending selectors or symbols. For React Native projects, the most common cause is a third-party native module whose method names collide with private API selectors. Less common, but more serious, is the deliberate use of a private API through dynamic dispatch (NSSelectorFromString, objc_msgSend) inside a native module.

    The evidence is documented in dozens of React Native GitHub issues. The facebook/react-native issue 33789 thread lists the selector matches Apple's tooling has flagged on a vanilla React Native 0.68 build. The community-recommended fix is to rename the colliding methods in the offending library or remove the library, then resubmit.

    The consequence is that the rejection often blocks builds that contain no actual policy violation. The scanner is a string match. It does not know the intent of the symbol or whether the corresponding code path is ever executed at runtime.

    The limit is that the error is silent on which symbol came from which library. The developer has to grep the project, scan node_modules, and run nm or otool -ov against the compiled binary to locate the source of each flagged name.

    Why does this matter for a vibe-coded or AI-generated React Native app?

    The short answer is that AI-generated React Native apps inherit the dependency surface of whatever starter or template the model used, and that surface frequently contains libraries with private API collisions.

    The mechanism is that Claude Code, Cursor, and similar tools assemble apps by pulling popular React Native packages from npm. Many of those packages were written before Apple tightened the static scan in 2023, and many include native iOS code with method names that match the private API list. A developer who never reads node_modules cannot easily see which library is the source of a 90338 rejection.

    In practice, the rejection cycle for an AI-generated build looks like this: submit, wait two days, receive ITMS-90338 with a list of selectors, search the symbol list against the project, identify the offending library, fork or patch it, rebuild, resubmit. Each loop costs a review window plus the developer's time. For builds with two or three colliding libraries, the loop runs more than once before the binary passes.

    For developers who want an external automated read of the build before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS for compiled iOS builds. The scan inspects symbol tables and JS bundle strings in the IPA so the developer can see the same kind of matches App Review will flag, before the submission goes in.

    What can a developer actually check before submitting?

    The short answer is to run the same kind of static analysis Apple runs, against the IPA, before uploading.

    The mechanism is a small set of command-line tools. Run nm -gU YourApp.app/YourApp to enumerate exported symbols. Run otool -ov YourApp.app/YourApp to list Objective-C selectors. Search the output against the public list of selectors Apple has flagged historically; the React Native community maintains an informal list in issues like 33789. For the JavaScript bundle, run strings main.jsbundle and look for obvious markers: hard-coded API endpoints, base64-encoded scripts, eval patterns, references to private iOS classes.

    Add to that an automated scanner. The output of NowSecure, MobSF, or PTKD.com surfaces the same kind of static findings against the IPA. None of those tools are inside Apple's review pipeline, but they run the same class of analysis against the same artifact.

    The limit is that a clean static scan does not guarantee a clean review. Apple's automated stage flags symbols; the human reviewer flags behavior. A build can pass the symbol scan and still be rejected for misleading metadata, missing privacy disclosure, or a flow that violates Guideline 5.1.1.

    What to watch out for

    The first trap is treating "no source code review" as "no scrutiny." The binary scan is more thorough than most developers expect; it picks up selector names hidden inside transitive dependencies most teams never read.

    The second trap is assuming the JavaScript bundle is invisible. The bundle is shipped inside the IPA and is part of the scanned surface, both for static analysis and for the runtime review.

    The third trap is hot-fixing a JS bundle to bypass a rejection. A new bundle that calls into native APIs added after the initial review is a 2.5.2 violation, not a bug fix. The 2017 Rollout.io removal is the canonical example, per Bitrise's OTA policy explainer.

    The fourth trap is ignoring ITMS-90338 as a "false positive" and resubmitting unchanged. Apple's scanner does not move on; the rejection persists until the offending symbols are removed or renamed.

    Key takeaways

    • Apple does not read the source code of a React Native app; App Review runs automated static analysis on the IPA and a short human review of the runtime build.
    • ITMS-90338 is the most common React Native rejection from this scan; it is a symbol collision check, not proof of malicious code.
    • The JavaScript bundle ships inside the binary and is part of the scanned surface; OTA updates to the bundle are permitted under DPLA Section 3.3(b) when they only call previously audited native APIs.
    • Pre-submission tooling (nm, otool, MobSF) can surface the same kind of findings Apple's static scan produces, before the build is uploaded.
    • Some teams running pre-submission audits use platforms like PTKD.com (https://ptkd.com) to scan compiled IPAs for private API symbol collisions and JS bundle red flags, so the surprise from App Review is smaller.
    • #react native
    • #app store
    • #itms-90338
    • #private api
    • #static analysis
    • #binary scan
    • #ios

    Frequently asked questions

    Does Apple ever request my source code for a React Native build?
    Not as part of the standard App Review. App Review works from the IPA you upload through Transporter, which is the compiled binary plus the embedded JavaScript bundle and any signed entitlements. Apple may request source access in specific antitrust or legal proceedings, but the day-to-day review process never reads your repo, your node_modules directory, or your Xcode project files.
    Why does my React Native build get ITMS-90338 when I do not call any private APIs?
    The scanner is a symbol match, not an intent check. It enumerates the Objective-C selectors in the compiled binary and compares each one to the private API list. Third-party React Native libraries often define method names like centerY or setLabelText that happen to collide with private API selectors, and the scanner flags them anyway. Renaming the colliding methods or removing the library clears the rejection.
    Can Apple see what is inside my JavaScript bundle?
    Yes. The main.jsbundle file ships inside the IPA and the static scan reads its contents alongside the compiled code. The scanner looks for forbidden strings, references to private iOS classes through bridge methods, eval-style execution patterns, and remote endpoints that could load additional code at runtime. The human reviewer also sees the runtime behavior the bundle produces when they exercise the app on a test device.
    Does CodePush or Expo Updates count as downloading code under Guideline 2.5.2?
    Both push new JavaScript bundles to a deployed app, which is interpreted code under Section 3.3(b) of the Developer Program License Agreement. That is permitted, as long as the new bundle only calls native APIs already audited in the original review. A hot-update that switches the app to a new business model or activates features hidden at review time is a Guideline 2.5.2 violation.
    Will running MobSF or a third-party scanner guarantee my build passes review?
    No. A pre-submission scanner can surface private API symbol matches, missing Privacy Manifest entries, and risky bundle strings, which catches the most common automated rejections. The human reviewer still inspects runtime behavior, in-app purchase flows, metadata accuracy, and privacy disclosure consistency. Those failures are behavioral and not visible to any static tool. The scanner reduces surprise; it does not replace App Review.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free