git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/mobile-push-notifications" ~/.claude/skills/intense-visions-harness-engineering-mobile-push-notifications-34ffef && rm -rf "$T"
agents/skills/codex/mobile-push-notifications/SKILL.mdPush Notifications
Implement push notifications with Expo Notifications, Firebase Cloud Messaging, and Apple Push Notification Service
When to Use
- Adding push notifications to a React Native app
- Registering devices and managing push tokens
- Handling notification interactions (tap to open specific screen)
- Scheduling local notifications
- Setting up notification channels for Android
Instructions
- Install and configure expo-notifications.
npx expo install expo-notifications expo-device expo-constants
// app.config.ts export default { plugins: [ [ 'expo-notifications', { icon: './assets/notification-icon.png', color: '#ffffff', sounds: ['./assets/notification-sound.wav'], }, ], ], android: { googleServicesFile: './google-services.json', }, };
- Register for push notifications and get the device token.
import * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; import Constants from 'expo-constants'; async function registerForPushNotifications(): Promise<string | null> { if (!Device.isDevice) { console.warn('Push notifications require a physical device'); return null; } const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { return null; } const token = await Notifications.getExpoPushTokenAsync({ projectId: Constants.expoConfig?.extra?.eas?.projectId, }); return token.data; }
- Send the token to your backend for server-side push delivery.
async function saveTokenToServer(pushToken: string, userId: string) { await fetch(`${API_URL}/users/${userId}/push-token`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: pushToken, platform: Platform.OS }), }); }
- Configure notification behavior (foreground display).
Notifications.setNotificationHandler({ handleNotification: async (notification) => { // Return how to display notifications when the app is in the foreground return { shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }; }, });
- Handle notification interactions (taps).
function useNotificationHandler() { const router = useRouter(); useEffect(() => { // Handle notification tap when app is already running const subscription = Notifications.addNotificationResponseReceivedListener((response) => { const data = response.notification.request.content.data; if (data.screen === 'order') { router.push(`/orders/${data.orderId}`); } }); return () => subscription.remove(); }, [router]); useEffect(() => { // Handle notification that launched the app (cold start) Notifications.getLastNotificationResponseAsync().then((response) => { if (response) { const data = response.notification.request.content.data; if (data.screen === 'order') { router.push(`/orders/${data.orderId}`); } } }); }, []); }
- Set up Android notification channels for categorized notifications.
if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('orders', { name: 'Order Updates', importance: Notifications.AndroidImportance.HIGH, vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', sound: 'notification-sound.wav', }); await Notifications.setNotificationChannelAsync('promotions', { name: 'Promotions', importance: Notifications.AndroidImportance.DEFAULT, }); }
- Schedule local notifications.
await Notifications.scheduleNotificationAsync({ content: { title: 'Reminder', body: 'Your order will arrive in 30 minutes', data: { screen: 'order', orderId: '123' }, sound: 'notification-sound.wav', categoryIdentifier: 'order-update', }, trigger: { seconds: 1800, // 30 minutes channelId: 'orders', }, });
- Send push notifications from your server using the Expo Push API.
// Server-side (Node.js) async function sendPushNotification(pushToken: string, title: string, body: string, data: object) { const response = await fetch('https://exp.host/--/api/v2/push/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: pushToken, title, body, data, sound: 'default', channelId: 'orders', }), }); const result = await response.json(); if (result.data?.status === 'error') { console.error('Push failed:', result.data.message); } }
Details
Expo Push vs. direct FCM/APNs: Expo Push API is a wrapper around FCM (Android) and APNs (iOS) that simplifies token management and payload format. For most apps, Expo Push is sufficient. Use direct FCM/APNs when you need advanced features (silent pushes, data-only messages, topic subscriptions).
Token lifecycle: Push tokens can change when the app is reinstalled, the OS is updated, or the user restores from backup. Re-register the token on every app launch and update it on your server.
Notification categories (iOS): Define action buttons that appear on the notification without opening the app.
await Notifications.setNotificationCategoryAsync('order-update', [ { identifier: 'track', buttonTitle: 'Track Order', isDestructive: false }, { identifier: 'dismiss', buttonTitle: 'Dismiss', isDestructive: true }, ]);
Common mistakes:
- Not requesting permissions before trying to get the push token
- Not handling permission denial gracefully (user may have declined)
- Testing push notifications on simulators (only works on physical devices)
- Not updating the push token on your server when it changes
- Sending too many notifications (users will disable them)
Source
https://docs.expo.dev/push-notifications/overview/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.