You've finished implementing push notifications, run every test you can think of — and then you hand the app to a friend with a newer Android phone and the notifications just... don't arrive.
On my own projects I keep hitting the same trap: everything works on iOS, yet the newest Android phones stay completely silent. The cause is almost always a missing Android 13 notification permission.
A notification that doesn't arrive is the same as no notification at all — and since it feeds straight into retention, this is one corner worth closing carefully.
Since Android 13 (API level 33), Google made a significant change: apps must now explicitly request permission from users before they can send notifications. This is a shift from the old opt-out model to an opt-in model — and it catches a lot of Rork-generated projects off guard.
What Changed in Android 13
On Android 12 and earlier, installing an app was enough. Unless the user actively went into settings and turned off notifications, they'd receive them by default.
Android 13 introduced a new runtime permission called POST_NOTIFICATIONS. Before your app can show any notification, it needs to ask the user:
Android 12 and earlier: Install → notifications work automatically
Android 13 and later: Install → request permission → user approves → notifications work
This applies to devices running API level 33 or higher. Apps targeting API 32 or lower get a temporary exemption, but since Google Play now requires targetSdkVersion 34+, there's no way around implementing this properly.
Common Gaps in Rork-Generated Code
Rork uses expo-notifications under the hood. The generated code handles iOS permissions well, but Android 13's new requirement sometimes slips through. Here are the two patterns to look for:
Pattern 1: Permission request exists, but is iOS-centric with no Android-specific channel setup
// ❌ Works on iOS and Android 12 — silent failure on Android 13+
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') {
alert('Notifications permission required');
return;
}Pattern 2: app.json has no permissions array for Android
// ❌ Missing Android permissions declaration
{
"expo": {
"android": {
"package": "com.yourapp.app"
}
}
}Both issues are easy to miss because iOS simulators and older Android devices don't surface the problem.
Step 1: Add POST_NOTIFICATIONS to app.json
Open app.json and add the permission declaration under the android key:
// ✅ Android 13+ compatible app.json
{
"expo": {
"android": {
"package": "com.yourapp.app",
"permissions": [
"android.permission.POST_NOTIFICATIONS",
"android.permission.VIBRATE",
"android.permission.RECEIVE_BOOT_COMPLETED"
]
},
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#ffffff"
}
]
]
}
}VIBRATE and RECEIVE_BOOT_COMPLETED are also needed for reliable notification delivery, so add them while you're here.
Step 2: Update the Permission Request Code
Here's the corrected implementation that handles both iOS and Android 13+ properly:
// ✅ Handles Android 13+ POST_NOTIFICATIONS correctly
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
async function registerForPushNotificationsAsync() {
// Android: create a notification channel first
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
// Check current status before requesting
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
// Only prompt if we haven't asked yet
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('Push notification permission not granted');
return null;
}
// SDK 52+ requires projectId
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig?.extra?.eas?.projectId,
});
return token.data;
}The key pattern: always call getPermissionsAsync() first, then only call requestPermissionsAsync() if the status is undetermined. If the user has already granted or denied, the request dialog won't appear anyway — but checking first keeps your code predictable.
Step 3: Rebuild with EAS
This is a step that's easy to skip — app.json permission changes require a full EAS Build. They're compiled into AndroidManifest.xml, which means OTA updates via eas update won't apply them.
# Test with a preview build first
eas build --platform android --profile preview
# Once confirmed working, ship the production build
eas build --platform android --profile productionTo test Android 13 behavior specifically, use an emulator running API level 33 or higher, or a physical device running Android 13+. Older emulators won't trigger the permission dialog, which makes it hard to confirm the fix is working.
Verifying the Fix
After installing the new build:
- Launch the app fresh — the "Allow notifications?" system dialog should appear
- Tap "Allow"
- Send a test notification from Expo's Push Notification Tool
- Confirm the notification arrives on the device
You can also verify the grant state directly with adb instead of guessing from the dialog — reading the OS-side state makes isolating the cause much faster.
# Check the notification permission grant state (granted=true means allowed)
adb shell dumpsys notification_manager | grep -A1 com.yourapp.app
# List the permissions declared for the package (is POST_NOTIFICATIONS there?)
adb shell dumpsys package com.yourapp.app | grep POST_NOTIFICATIONSIf the second command shows nothing, it's a sign your app.json change never made it into the build — revisit the EAS Build step.
If the dialog doesn't appear, you can manually enable notifications in Settings → Apps → [Your App] → Notifications to continue testing. But always fix the code — real users won't dig through settings.
A Note on Expo SDK Versions
The projectId parameter in getExpoPushTokenAsync() became required in Expo SDK 52. Without it, you might see token retrieval succeed in development builds but fail in production — which is a frustrating bug to track down.
// If you're on SDK 52+, this is required
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig?.extra?.eas?.projectId,
});If your Rork project was generated on an older template, check whether this parameter is present.
Android's notification permission model will likely keep evolving. POST_NOTIFICATIONS is the current requirement, but staying current with Expo SDK release notes and Google Play policy changes is a worthwhile habit — especially in areas like notifications, payments, and device permissions where the rules change most often.
For related issues, see Push Notifications Not Delivering in Rork Apps and EAS OTA Updates Not Reflecting on Device.