Fixing the "android:exported Needs to Be Explicitly Specified" Manifest Merger Error in React Native Builds
An Android build that worked fine last month suddenly fails with a manifest merger error naming a specific activity, service, or receiver and demanding an android:exported attribute nobody remembers needing before. It isn't a regression in the project's own code — it's a platform-level requirement that only applies once a component declares an intent filter, surfaced by an SDK upgrade rather than caused by one.
The Problem
A React Native Android build that compiled and ran without issue previously starts failing at the manifest-merging step, with an error naming a specific <activity>, <service>, or <receiver> and stating that android:exported needs to be explicitly specified for it. Nothing in the app's own AndroidManifest.xml appears to have changed recently, and the named component is often one the project didn't even declare directly — it came from a third-party native module's own manifest, merged in automatically as part of the build.
Why It Happens
Android 12 (API level 31) made android:exported mandatory for any component with an intent filter, with no implicit default
Before this change, a component with an <intent-filter> but no explicit android:exported attribute defaulted to exported (accessible to other apps) if it had an intent filter, and non-exported otherwise — an implicit, filter-dependent default. Starting with apps that target API 31 or higher, Android requires every component with an intent filter to state android:exported explicitly, as a deliberate security improvement that removes the ambiguity in that implicit rule rather than leaving developers to discover the default through documentation alone.
Raising targetSdkVersion — often as a routine dependency or React Native upgrade — is what actually triggers this requirement
The requirement doesn't activate because of anything specific to a given component; it activates because the app's targetSdkVersion crossed the API 31 threshold, which commonly happens as a side effect of upgrading React Native itself, upgrading the Android Gradle Plugin, or simply raising the target SDK to satisfy a newer Google Play Console requirement. A project that hasn't touched its own manifest in months can still hit this error the moment any of those routine upgrades lands, because the trigger is the SDK level, not a change to the component in question.
The flagged component frequently belongs to a third-party library, not the project's own manifest
Many native modules — push notification SDKs, deep-link handlers, analytics libraries — declare their own activities, services, or receivers with intent filters inside their own bundled manifest, which Android's manifest merger combines into the final app manifest at build time. When the error names a component the project never directly wrote, it's almost always one of these library-contributed entries, and the fix has to be applied through the main app's manifest using a manifest-merger override rather than by editing a file that doesn't belong to the project.
The Fix
1. Identify whether the flagged component is declared in the app's own manifest or contributed by a dependency
# Inspect the fully-merged manifest to see where a component actually comes from:
cd android && ./gradlew :app:processDebugMainManifest
# The merged manifest is typically written to:
# android/app/build/intermediates/merged_manifest/debug/AndroidManifest.xml
grep -B5 "ComponentNameFromError" app/build/intermediates/merged_manifest/debug/AndroidManifest.xml
Checking the merged manifest output, rather than only the project's own hand-written AndroidManifest.xml, reveals whether the flagged component is something the project declared directly or something a dependency's own manifest contributed — which determines whether the fix belongs in the app's manifest directly or needs a merger-override approach.
2. For a component the project declared directly, add the explicit android:exported attribute based on its actual intended visibility
<activity
android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
Setting the attribute to true for a component that genuinely needs to be reachable from outside the app — a deep-link activity, for instance — or false for one that should only ever be triggered internally, makes the previously implicit decision explicit and resolves the error for that specific entry, while also forcing a deliberate security review of whether the previous implicit default actually matched what the component needs.
3. For a component contributed by a dependency's manifest, override it from the app's own manifest using tools:node="merge"
<!-- In android/app/src/main/AndroidManifest.xml, with the merge tooling namespace declared on the manifest root -->
<manifest xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name="com.somelibrary.SomeActivity"
android:exported="false"
tools:node="merge" />
</application>
</manifest>
Since the actual component definition lives inside a dependency and isn't directly editable, declaring the same component name in the app's own manifest with tools:node="merge" tells the manifest merger to combine the app's override with the library's definition rather than replace it outright, which is the supported way to add the missing attribute to a library-owned component without forking or patching that library.
4. Update the specific library if a newer release already ships the fix, rather than patching around an outdated one indefinitely
npm view <the-flagged-library> versions --json | tail -5
Because this requirement has been in place since Android 12 shipped, an actively maintained library has usually already updated its own manifest to declare android:exported correctly in a newer release, making a dependency upgrade a cleaner long-term fix than carrying a manual manifest override indefinitely for a library that will eventually publish the fix itself.
Why This Works
Each step addresses a different point in how the error actually arose. Inspecting the merged manifest replaces guessing about where a flagged component comes from with a concrete answer. Adding the attribute directly to a project-owned component resolves the error at its actual source while forcing a deliberate security decision instead of relying on the old implicit default. The tools:node="merge" override provides the correct mechanism for components that live inside a dependency's own manifest, which can't be edited directly. And checking for a newer library version addresses the case where the real fix is simply upgrading a dependency that has already caught up with this platform requirement.
Conclusion
A manifest merger error demanding android:exported isn't a regression introduced by recent changes to the project — it's Android 12's removal of an implicit, filter-dependent default for any component with an intent filter, surfaced the moment the app's targetSdkVersion crosses API 31 through what's often an otherwise routine upgrade. Check the merged manifest to see whether the flagged component belongs to the app or a dependency, add the attribute directly with a deliberate true/false choice for project-owned components, use a tools:node="merge" override for library-owned ones that can't be edited directly, and check whether a dependency upgrade already ships the fix rather than maintaining a manual override indefinitely.
