The rejection email lands within minutes of a successful Xcode upload. App Store Connect lists ITMS-90338, Non-public API usage, and prints a small set of method names: isPassthrough, onSuccess:, removeValuesForKeys:completion:, viewManager, and a few others depending on the version of React Native and the pods inside the project. The instinct is to grep your own source code. In a React Native project that grep usually returns nothing, because the colliding selectors live in third-party Pods rather than in your bridging modules.
Short answer
ITMS-90338 is an automated ingestion check inside App Store Connect that compares Objective-C selector strings in your compiled iOS binary against Apple's internal list of private API names. The scanner does not run your code; it reads strings. In a React Native project the matched selectors almost always sit inside a third-party Pod, most often Flipper in older RN templates, async-storage, OneSignal, Auth0, or in-house bridging modules. The repair path is to upgrade React Native, strip debug-only pods from the release configuration, fork or patch the offending pod to rename the selector, or reply to App Review naming the library, version, and upstream source. Per App Review Guideline 2.5.1, apps must use only public APIs.
What you should know
- ITMS-90338 is an ingestion check, not an App Review decision. App Store Connect runs it during binary processing and the build never reaches a human reviewer.
- The scanner compares selector strings in your binary to Apple's private API list. The list is not published, but the rejection email names the matched selectors.
- In a React Native project, the offending selector almost always lives in a Pod. Flipper, async-storage, OneSignal, Auth0, and older WebRTC pods are the historical sources.
- Upgrading to React Native 0.74 or newer clears several of the most reported selectors. The new default template no longer bundles Flipper.
- A clear reply to App Review with the library reference often clears the rejection without a code change. Apple looks for a named library, version, and upstream source link.
- The check is not a security audit. Builds that pass ITMS-90338 can still ship with hardcoded keys, weak transport, or insecure storage.
What does ITMS-90338 actually scan for in a React Native binary?
The scanner reads the compiled Mach-O binary inside the .ipa and extracts every Objective-C selector from the __objc_methname and __objc_methtype sections. Each selector is compared against Apple's private API string table. Any exact match triggers ITMS-90338, and the rejection email names the matched selectors so the developer can grep for them.
Apple's own response on the Apple Developer Forums thread on ITMS-90338 reads: "If method names in your source code match the private Apple APIs listed above, altering your method names will help prevent this app from being flagged in future submissions. In addition, note that one or more of the above APIs may be located in a static library that was included with your app." The wording matters. Apple is looking at strings, not at call sites. A library that defines a method called onSuccess: and never invokes the private onSuccess: on a UIKit class is flagged the same as a library that does. The fix is therefore at the string level, not the runtime level.
The private API list itself is not published. Apple updates it without public announcement, which means a build that passed last quarter can fail this quarter on the same code.
Which React Native libraries are the usual sources?
Selectors that appeared in published rejection emails through 2024 and 2025 include centerY, hide:, isPassthrough, onSuccess:, permissionType, removeValuesForKeys:completion:, setLabelText:, show:, valueOffset, viewManager, _isKeyDown, _modifiedInput, _modifierFlags, handleNotification:, and setCategoryID:. Each one traces to a recognizable upstream.
| Selector flagged | Common upstream library | Note |
|---|---|---|
| isPassthrough, viewManager | React Native core (older RN < 0.73) | Cleaner in newer releases |
| onSuccess: | OAuth wrappers, payment SDKs | Coincidental name collision |
| removeValuesForKeys:completion: | UserDefaults wrappers, telemetry pods | Rename or update |
| setLabelText:, valueOffset | Older WebRTC pods | Drop unused screensharing helpers |
| permissionType | Permission handling libraries | Patch via patch-package |
| _isKeyDown, _modifierFlags | Flipper debug pods | Remove from release configuration |
The async-storage discussion #804 and the React Native issue #33789 both confirm the same pattern: developers see a long list of selectors in the rejection email, none of which exist in their own application source. The maintainers point at the same set of upstream pods.
How do you isolate the framework that triggered the rejection?
The rejection email names selectors but does not name the offending pod. Three steps narrow the search.
First, locate the compiled binary. After archiving, the .ipa sits in ~/Library/Developer/Xcode/Archives/. Unzip it and find the main binary at Payload/<App>.app/<App>, along with every framework inside Payload/<App>.app/Frameworks/.
Second, run nm -j <binary> | grep -E 'isPassthrough|onSuccess|removeValuesForKeys|viewManager' against each Mach-O. The match isolates the suspect framework in seconds. The nm -j flag prints just the symbol names, which keeps the output readable.
Third, cross-reference the suspect pod's source. Search the library's GitHub repository for the selector text in Objective-C source files. The owning file usually appears on the first page of results. If the symbol is in your own bridging code, rename it and rebuild. If it is in a third-party pod, you have three options: upgrade the pod to a release that renamed it, fork the pod and update your Podfile to point at the fork, or reply to App Review with the library reference.
What does the fix look like in your Podfile and source tree?
Three flavors of fix, ordered by labor.
First, upgrade. If the project sits on React Native 0.71 or older, upgrading to 0.74 or newer removes Flipper from the template and clears several of the most frequently reported selectors. Confirm the upgrade by running pod deintegrate && pod install and checking that the Flipper pods are gone from the Pods directory. The React Native 0.74 release notes describe the Flipper removal and the new debugging story.
Second, surgical pod-level edits. For a colliding selector inside a vendored pod, point the Podfile at a fork of the library where the selector is renamed. Objective-C selectors are symbol identifiers, not API contracts, so renaming a private internal method does not break the library's public surface.
pod 'OneSignalXCFramework',
:git => 'https://github.com/your-org/OneSignal-iOS-SDK',
:branch => 'rename-removevaluesforkeys'
Third, source code changes for any module you own. Rename the method, regenerate the bridging header, rebuild the archive, increment CFBundleVersion, and re-upload through Xcode or Transporter. The nm -j check should now return zero hits on the previously flagged selector.
How do you reply to App Review when the selector lives in a pinned dependency?
A reply that names the library, the version, the selector, and a link to upstream source clears most ITMS-90338 cases without a code change. The structure that has worked for developers posting on Apple Developer Forums:
- Library: react-native-onesignal 5.2.6
- Selector: removeValuesForKeys:completion:
- Upstream source: link to the line in the OneSignal-iOS-SDK GitHub repository
- Statement: the symbol shares a name with the private Apple API but does not invoke it; the library uses the selector on its own internal cache type.
The reply lives in App Store Connect, under the rejected build, in the Reply to App Review field. Replies without a named library or without a source link are slower to clear, and some are ignored. Some teams attach a screenshot of the offending source line for clarity. App Review still has discretion to ask for the SDK to be removed, but a well-cited reply is the fastest documented path back to processing. For builders who want an external automated read of their build before resubmission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning of compiled iOS binaries against OWASP MASVS controls, which sits next to but does not replace the ITMS-90338 ingestion check.
What to watch out for
- Treat ITMS-90338 as an ingestion check, not a security audit. It does not measure whether the app leaks secrets, ships hardcoded keys, or uses certificate pinning.
- Flipper pods in release builds are the single most common silent culprit. Especially in projects upgraded from React Native 0.66 or earlier, leftover Flipper code can linger in the release Podfile.
- Patches via patch-package only persist while the upstream version stays pinned. The patch can silently drop when the version is bumped, and the rejection reappears on the next submission.
- Apple's private API list is not published. A clean build today can fail tomorrow if Apple adds a name to the table.
- Some App Review replies copy-paste a request to remove the third-party SDK. Push back with a named library reference and upstream source link if you believe the symbol is a coincidence.
Key takeaways
- ITMS-90338 is a string-table check. The scanner compares the binary's Objective-C selector list against Apple's private API list and rejects exact matches without running the code.
- The selector almost always lives in a third-party pod: Flipper in older React Native templates, async-storage, OneSignal, Auth0, older WebRTC pods, or in-house bridging modules.
- Three repair paths exist in order of labor: upgrade React Native, fork or patch the offending pod to rename the selector, or reply to App Review with a named library and source link.
- The check is not a security audit. A build that passes ITMS-90338 can still ship with hardcoded keys, weak transport, or insecure storage.
- Some teams who want a separate read of the IPA against OWASP MASVS controls run their compiled build through PTKD.com (https://ptkd.com), which surfaces storage, network, and platform findings that ingestion does not look at.



