App Store

    How do I fix ITMS-90338 non-public API usage in React Native?

    A Mac running Xcode with a React Native iOS project open, an App Store Connect rejection email naming ITMS-90338 non-public API usage in the foreground, and a terminal window listing Objective-C selectors found under node_modules

    The build uploaded cleanly, App Store Connect started processing, and then the ITMS-90338 email arrived with a list of Objective-C selectors you have never typed in your life. The fix almost always lives somewhere inside node_modules, and the path to it is mechanical once you know where to look.

    Short answer

    ITMS-90338 fires when Apple's submission scanner finds Objective-C selectors in your compiled IPA that match the closed list of private framework methods. In React Native projects, the offending selectors usually originate in a third-party module under node_modules (react-native-reanimated, react-native-webrtc, an older React Native core file, or a development helper compiled into a Release build). Removing the symbol, patching the source, or upgrading the offending package clears the rejection on the next upload.

    What you should know

    • The rejection is binary-level, not source-level. Apple's submission scanner inspects the symbol table of the compiled IPA, so the offending selector has to leave the binary, not just the JavaScript bundle.
    • Apple Guideline 2.5.1 is the underlying rule. Per the published App Review Guidelines, apps may only use public APIs; the ITMS-90338 email is the automated enforcement of that rule.
    • The DEBUG flag is a common false trigger. RCTKeyCommands and other React Native modules expose private selectors when DEBUG is defined; archiving in Release mode is the first check.
    • Common culprits are a short list. react-native-reanimated, react-native-gesture-handler, react-native-webrtc, the WKWebView wrapper in older RN core, and a handful of analytics SDKs account for most reports.
    • Apple ships no debugger for ITMS-90338. No first-party tool maps a flagged selector back to the framework that owns it; you grep your way to the answer.

    What does the ITMS-90338 email actually say?

    The short answer is that Apple's submission pipeline tells you exactly which selectors tripped the scanner and warns that the upload is in a state that blocks both TestFlight distribution and App Store review.

    The email body lists between two and twenty selector names in quotes (centerY, hide:, isPassthrough, onSuccess:, removeValuesForKeys:completion:, setProcessPool:, _modifierFlags, and similar), followed by the standard advisory that the symbols may live in a static library and that renaming them in source code is the suggested remediation. The build appears in App Store Connect but cannot be assigned to a TestFlight group or submitted to App Review until a corrected version arrives with a higher build number. Per Apple's App Review Guidelines, Section 2.5.1, apps may only use public APIs, which is the rule the scanner is enforcing.

    The selector list is the only diagnostic Apple provides. There is no path, no framework name, no line number. The list itself, plus a few targeted grep commands across node_modules, is enough to identify the offender in most React Native projects.

    Why does Apple flag React Native builds for selectors I never wrote?

    The short answer is that React Native pulls in compiled native code from dozens of Pods, and any one of them can reference a selector that collides with a private Apple API.

    When you run pod install inside the ios/ folder, every native module under node_modules contributes its .h and .m files (or a precompiled framework) to the final IPA. The Objective-C runtime keeps every selector reference in the binary, regardless of whether the code path is ever reached. Apple's static analyzer reads that symbol table and matches it against an internal list of method names reserved for Apple frameworks (UIKit, WebKit, Foundation, internal SPI). A collision is enough to trigger ITMS-90338, even when the third-party method has a completely different implementation.

    The pattern appears most often in three categories. The first is React Native's own development tooling. _modifierFlags is reserved by UIKit and was historically used in RCTKeyCommands.m for the in-app dev menu shortcut; older RN versions shipped the symbol even in Release builds because the DEBUG guard was incomplete. The second is animation and gesture libraries that wrap UIKit internals. The third is WebView and WebRTC libraries that reach into WKWebView selectors (frameInfo, callWithArguments:, setProcessPool:, navigationType) that are not part of the public surface.

    A spike of these rejections happened across React Native, Flutter, Capacitor, and Ionic in mid-2022 after a change to the submission scanner. The pattern reported on the Apple Developer Forums thread on ITMS-90338 was a sudden wave of identical rejections on previously accepted builds. The takeaway is that the rule itself never moved (Guideline 2.5.1 has been on the books for years); what moves is the detection layer, and it moves without notice.

    How do I find which library in node_modules carries the forbidden selector?

    The short answer is grep on the selector name across node_modules and Pods, narrow the result, then confirm with nm on the compiled binary.

    The fastest first pass is a recursive text search across the .h, .m, .mm, and .swift files inside the React Native dependency tree. Each selector reported in the email gets its own search; the first match (or two) usually points at the library. The table below maps the selectors that show up most often in ITMS-90338 reports to the package that typically owns them. (Match patterns shift with library versions, so confirm before deleting code.)

    Selector reported by AppleCommon origin in node_modulesPublic alternative or fix
    _modifierFlags, _isKeyDown, _modifiedInputnode_modules/react-native/Libraries/.../RCTKeyCommands.m (dev menu)Patch to _modifierEventFlags, or upgrade RN past the fix
    centerY, setLabelText:, hide:, show:node_modules/react-native-reanimated older builds, or in-house view helpersRename in source, or update the package to a patched release
    frameInfo, isMainFrame, navigationType, setNavigationDelegate:, setProcessPool:, userContentController, callWithArguments:WKWebView wrappers in legacy react-native-webview forks, react-native-webrtc, JavaScriptCore bridgesUpgrade to current react-native-webview; replace forks with maintained versions
    onSuccess:, removeValuesForKeys:completion:, permissionType, valueOffset, viewManager, isPassthroughOlder analytics SDKs (legacy AppsFlyer, certain Firebase combinations), react-native-system-settingUpgrade the SDK to a version that ships clean symbols; remove unmaintained modules

    The second pass is nm -arch arm64 on the .o files inside the Pods build output (or on the .framework binaries inside ios/Pods/), piped to grep for the selector name. The symbol appears as a constant string reference in the literal section of the binary; finding the file that contains it confirms the source. The discussion on the React Native issue #33789 walks through the same workflow with real selector lists from production projects.

    For builders who want an external read of which symbols and selectors actually live inside the compiled IPA before resubmitting, PTKD.com (https://ptkd.com) is one of the platforms that scans the build and lists the methods present in the binary, alongside the OWASP MASTG checks that flag risky symbol patterns in native modules.

    What patches and Podfile workarounds actually clear the rejection?

    The short answer is the smallest patch that removes the offending selector from the compiled binary, applied as a Podfile post-install hook so the fix survives pod install.

    Three patterns cover almost every reported case.

    First, the targeted source patch. The historical example is the _modifierFlags rename in RCTKeyCommands.m: an sed -i '' call inside the Podfile's post_install block rewrites the symbol to _modifierEventFlags and the binary stops shipping the private selector. This is brittle (the patch has to track the React Native version and the file path), so prefer an upgrade when one exists.

    Second, the dependency upgrade. The maintained React Native, react-native-webview, react-native-reanimated, and react-native-webrtc releases have all shipped fixes for specific ITMS-90338 selectors over time. Reading the changelog of each suspect package, then running npx react-native upgrade for the core, is the path that does not need a custom patch. Per the React Native upgrading guide, the upgrade helper diffs the iOS template files and surfaces the changes you need to apply.

    Third, the build-mode check. Build a fresh Release archive (Product, Archive in Xcode with the Generic iOS Device target) rather than a Debug archive, and confirm the xcconfig sets the right DEBUG=0 flag for Release. Several reported false-trigger cases on the Apple Developer Forums resolved with nothing more than a clean Release build.

    If the selector genuinely belongs to a third party with no fix in sight, removing the package is the safe path. Appealing to App Review with an explanation works for a small number of edge cases, but Guideline 2.5.1 leaves little room: an app that uses a private API is not a public-API app, and the appeal lands or fails on whether the reviewer accepts the collision argument. For native module authors who want to verify that their own library does not ship a private selector, the OWASP MASVS and the MASTG test cases give a reusable checklist for symbol hygiene.

    What to watch out for

    The first trap is patching a node_modules file by hand and committing it. The patch disappears the next time anyone runs npm install or upgrades the package. Use patch-package or a Podfile post-install hook so the change is checked in and reapplied automatically.

    The second trap is treating ITMS-90338 as a one-time event. The scanner runs against every upload, including bug-fix releases of long-shipping apps. A package upgrade that adds a new native screen, a switch from the public WebView to a custom fork, or a new analytics SDK can reopen the rejection on a build that previously cleared review.

    The third trap is over-trusting the selector list as a description of intent. A collision is not the same as a private-API call; the binary just contains a string that matches an Apple-reserved name. Renaming the method in your code (or in the vendored library) is enough, and the scanner does not require any deeper proof of behavior change.

    The fourth trap is silent resubmission. A second build with the same build number is rejected before the scanner runs. The corrected upload needs a higher build number and a fresh archive.

    Key takeaways

    • ITMS-90338 is a binary-level rejection: Apple's scanner reads selector references in the compiled IPA, not the JavaScript bundle. The fix lives in compiled native code from React Native or a dependency.
    • The selector list in the email is the entire diagnostic. A targeted grep across node_modules and ios/Pods, narrowed with nm on the compiled framework, identifies the offending package in most projects.
    • Prefer upgrading the suspect dependency to patching its source. When a patch is the only option, apply it through patch-package or a Podfile post_install hook so the fix survives pod install and npm install.
    • Treat the symbol audit as part of every release diff, not a one-time setup. New native modules, new analytics SDKs, and major React Native upgrades all bring fresh ITMS-90338 risk.
    • Some teams outsource the symbol audit to platforms like PTKD.com (https://ptkd.com), which scan the compiled IPA against OWASP MASVS and MASTG controls and surface the private-API selectors a binary actually ships before the next App Store Connect upload.
    • #itms-90338
    • #react native
    • #non-public api
    • #ios
    • #app store connect
    • #private api
    • #node_modules
    • #rejection

    Frequently asked questions

    Will switching from Debug to Release archive fix ITMS-90338 on its own?
    Sometimes yes. React Native's RCTKeyCommands and a few dev-only helpers expose private selectors when DEBUG is defined. A Debug-built IPA submitted by mistake triggers ITMS-90338 even when the underlying dependencies are clean. Reproduce the rejection on a fresh Release archive built against the Generic iOS Device target before touching any dependency code. If the Release build clears, no further patch is needed for that submission.
    Can I appeal an ITMS-90338 rejection by explaining that the selector is a name collision?
    Occasionally. App Review accepts the collision argument for some well-known cases, especially when the selector lives in a maintained third-party SDK with no current alternative. The pattern reported by developers is mixed, and the appeal usually buys time rather than a permanent pass. Plan for a code-side fix in parallel; removing the symbol is the only path that survives the next submission scan.
    Does updating React Native to the latest version remove all ITMS-90338 risk?
    It removes the risk inside react-native core, which is the most common single source of the rejection. It does not protect against third-party native modules. The reanimated, gesture-handler, webview, and webrtc packages each have their own private-selector history. Run a grep pass across the full node_modules tree after the upgrade and rebuild a Release archive to confirm the scanner has nothing to flag.
    How do I patch a node_modules file in a way that survives npm install?
    Use the patch-package tool to capture the edit as a .patch file in the repo, then add a postinstall script to package.json that runs patch-package on every install. Each fresh install replays the patch. For pure iOS source fixes, a Podfile post_install block with a small ruby script or sed call has the same effect at the Pods layer. Both patterns are version-controlled and reproducible.
    Does ITMS-90338 affect Expo managed workflow builds?
    Yes. Expo managed builds compile the same native code under the hood; an Expo SDK release that bundles a library with a private selector triggers the same rejection. The fix usually arrives in a patch release of the Expo SDK, listed in the changelog. Pinning the affected package to a clean version, or moving to a bare workflow long enough to apply patch-package, is the typical workaround.

    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