Add FCM Push (expo-notifications)
Firebase Cloud Messaging — free, unlimited push for Android.
Install
bashnpm exec --no -- expo install expo-notifications expo-device
Firebase project setup
Run set-up-firebase-cli skill first. Result: google-services.json in android/app/.
app.json:
json{ "expo": { "android": { "googleServicesFile": "./google-services.json" }, "notification": { "icon": "./assets/notification-icon.png", "color": "#ffffff", "androidMode": "default" } } }
Request permission (Android 13+ runtime)
tsximport * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; async function registerForPush() { if (!Device.isDevice) return; let { status } = await Notifications.getPermissionsAsync(); if (status !== 'granted') { const r = await Notifications.requestPermissionsAsync(); if (r.status !== 'granted') return null; } const token = (await Notifications.getDevicePushTokenAsync()).data; // FCM token — save to backend return token; }
Custom pre-prompt (boost permission opt-in)
Before calling requestPermissionsAsync, show your own screen:
"Get a daily reminder. We promise no spam." [Sure] [Later]
If "Sure" → trigger system dialog. If "Later" → don't ask again this session.
Notification channels (Android 8+)
tsximport * as Notifications from 'expo-notifications'; Notifications.setNotificationChannelAsync('daily-reminders', { name: 'Daily Reminders', importance: Notifications.AndroidImportance.HIGH, sound: 'default', vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', });
Channels are user-controllable in Settings. Group related notifs.
Foreground handler
tsxNotifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: false, shouldSetBadge: true, }), });
Send from backend
Use Firebase Admin SDK or Expo Push API:
tsfetch('https://exp.host/--/api/v2/push/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: deviceToken, title: 'Reminder', body: 'Time to log today', channelId: 'daily-reminders', }), });
Deep link on tap
tsxconst notificationRoutes = new Set(['/today', '/library']); Notifications.addNotificationResponseReceivedListener((response) => { const screen = response.notification.request.content.data.screen; if (typeof screen === 'string' && notificationRoutes.has(screen)) { router.push(screen); } });
Manufacturer battery optimization (CRITICAL)
Xiaomi (MIUI), Oppo (ColorOS), OnePlus, Samsung kill background apps aggressively. Push works most of the time but for ENGAGEMENT push (reminders), users must:
- Settings → Battery → [Your app] → "Don't optimize"
- Some need: Settings → Apps → [Your app] → Battery saver → "No restriction"
Document this in onboarding for power users.
Pair with
add-onesignal-pushfor advanced campaigns (RC for transactional, OS for marketing)add-deep-linksto handle taps


