From harness-claude
Implements push notifications in React Native apps using Expo Notifications, Firebase Cloud Messaging, and APNs. Handles device registration, token management, and notification interactions.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Implement push notifications with Expo Notifications, Firebase Cloud Messaging, and Apple Push Notification Service
Implements push notifications for iOS, Android, React Native, and web using Firebase Cloud Messaging and native services. Handles permissions, tokens, background/foreground messages, and channels.
Implements push notifications for iOS/Android using Firebase Cloud Messaging, APNs. Covers React Native Firebase, Swift, Kotlin, Flutter with backend token handling and best practices.
Implements push notifications in Capacitor apps for iOS/Android using Firebase Cloud Messaging (FCM) and APNs. Covers plugin setup, permissions, token registration, event handling, and platform configs.
Share bugs, ideas, or general feedback.
Implement push notifications with Expo Notifications, Firebase Cloud Messaging, and Apple Push Notification Service
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',
},
};
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;
}
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 }),
});
}
Notifications.setNotificationHandler({
handleNotification: async (notification) => {
// Return how to display notifications when the app is in the foreground
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
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}`);
}
}
});
}, []);
}
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,
});
}
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',
},
});
// 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);
}
}
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:
https://docs.expo.dev/push-notifications/overview/