From harness-claude
Implements push notifications in React Native apps using Expo Notifications, Firebase Cloud Messaging, and APNs. Covers device registration, token management, foreground handling, and tap interactions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:mobile-push-notificationsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Implement push notifications with Expo Notifications, Firebase Cloud Messaging, and Apple Push Notification Service
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/
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeImplements 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 in Capacitor apps for iOS/Android using Firebase Cloud Messaging (FCM) and APNs. Covers plugin setup, permissions, token registration, event handling, and platform configs.
Implements and debugs push notifications in iOS/macOS apps — local, remote (APNs), rich notifications, actions, silent pushes, and extensions.