Fixing React Native Push Notifications That Silently Fail to Deliver on Android 13+ (Runtime Permission and Notification Channel Misconfiguration)
The FCM token registers fine, the server reports a successful send, and the notification still never appears on the device. On Android 13+, that's almost never a backend problem — it's a missing runtime permission or a notification channel the app never actually created. Here's how to find which one, and fix it for good.
The Problem
A React Native app registers for push notifications, the FCM (or APNs-via-FCM) token comes back valid, and the backend's send call returns success — but the notification never shows up on the device. Foreground handling works fine when the app is open and a listener logs the payload; it's specifically the system tray notification that never appears when the app is backgrounded or killed. Nothing in the client-side error logs points to a failure, because from the messaging layer's point of view, nothing failed.
Why It Happens
Android 13 made notification permission opt-in, not implicit
Before Android 13 (API 33), any app could post notifications without asking. Android 13 introduced POST_NOTIFICATIONS as a runtime permission the user must explicitly grant, the same way camera or location access works. An app that never requests it — because it shipped before Android 13 existed, or because the request was added to the manifest but never actually triggered at runtime — has messages arriving and being processed correctly, with the OS silently dropping the final "show this to the user" step because permission was never granted.
A notification channel the app assumes exists but never created
Since Android 8 (API 26), every notification must belong to a channel, and each channel carries its own user-controlled importance level, sound, and visibility settings. If the client never calls the code that creates the channel — often because it was added to the native Android project but the JS side references a different channelId than the one actually registered, or the creation code only runs on a cold app start that hasn't happened since the change shipped — Android has nowhere valid to route the notification and drops it, again without raising a client-visible error.
The channel exists, but the user (or a device OEM battery optimizer) muted it
Once a channel is created with a given importance level, the app itself can no longer change that level — only the user can, in system settings. A channel accidentally created with IMPORTANCE_LOW or below suppresses the heads-up notification permanently for that channel's ID, and reusing the same channel ID in a later app update won't fix it, because Android treats channel settings as sticky once a channel ID has been created on a device.
The Fix
1. Explicitly request POST_NOTIFICATIONS at runtime on Android 13+
import { PermissionsAndroid, Platform } from "react-native";
async function ensureNotificationPermission() {
if (Platform.OS === "android" && Platform.Version >= 33) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true; // pre-Android 13: no runtime prompt needed
}
Call this before relying on notifications to be visible, and treat a denial as a real state to handle in the UI (a settings deep link, or a re-prompt at a sensible later moment) rather than assuming the permission dialog was accepted.
2. Create the notification channel explicitly, and log its exact ID
import notifee from "@notifee/react-native";
async function ensureChannel() {
const channelId = await notifee.createChannel({
id: "default",
name: "Default Notifications",
importance: 4, // IMPORTANCE_HIGH — required for heads-up display
});
return channelId;
}
Confirm the channelId used when displaying a notification is character-for-character identical to the one used at creation time — a mismatch here fails silently rather than throwing, since Android just treats an unrecognized channel reference as "notification has no valid channel" and drops it.
3. Inspect existing channel state on a real device instead of guessing
On the test device, go to Settings → Apps → [app] → Notifications and check the channel's actual importance level. If it shows as muted or low-importance from earlier testing, the fix isn't in the app's code at all — the channel ID needs to change (a fresh ID forces Android to recreate it with the new importance default) since existing channel settings on a device can't be overwritten programmatically.
4. Verify with a real background/killed-app send, not just a foreground test
A notification received while the app is in the foreground and handled by a JS listener proves the network and payload delivery work — it proves nothing about whether the system notification itself would have displayed, because foreground handling in most RN notification libraries bypasses the system tray entirely. Test explicitly with the app backgrounded and force-killed before considering the fix verified.
Why This Works
Every failure mode here shares the same shape: the message genuinely arrives at the device, and Android's notification manager silently declines to display it because one of the OS-level prerequisites — runtime permission, a valid channel, sufficient channel importance — wasn't actually satisfied, not because delivery failed. Requesting permission explicitly, creating and referencing a consistent channel ID, and testing against a real channel's importance setting on a device closes each of those gaps directly, rather than working around a delivery problem that was never the real cause.
Conclusion
A React Native push notification that "sends successfully" but never appears is almost always an Android 13+ permission or notification-channel issue, not a messaging failure — the FCM layer did its job; the OS-level display step is what got silently skipped. Request POST_NOTIFICATIONS explicitly, create and consistently reference a notification channel, and confirm its real importance level on a physical device before assuming the backend or the messaging SDK is at fault.
