setup-push-notifications
Configure push notifications for iOS and Android with Firebase or Expo.
Overview
The setup-push-notifications command implements push notifications in your React Native app, supporting both Firebase Cloud Messaging (FCM) and Expo Push Notifications, with features like rich notifications, categories, and analytics.
Usage
bash
/template setup-push-notifications <provider> [options]Parameters
<provider>- Notification provider:expo,firebase,onesignal
Options
--rich-media- Enable images and attachments--categories- Set up notification categories/actions--analytics- Track notification metrics--local- Include local notifications
Examples
Expo Push Notifications
bash
/template setup-push-notifications expoFirebase with Rich Media
bash
/template setup-push-notifications firebase --rich-media --analyticsFull Setup with Categories
bash
/template setup-push-notifications expo --categories --local --analyticsWhat It Creates
Notification Structure
src/
├── services/
│ ├── notifications/
│ │ ├── config.ts # Provider configuration
│ │ ├── handlers.ts # Notification handlers
│ │ ├── permissions.ts # Permission management
│ │ ├── categories.ts # Action categories
│ │ ├── analytics.ts # Tracking utilities
│ │ └── types.ts # TypeScript types
│ └── NotificationService.ts # Main service classNotification Service
typescript
// src/services/NotificationService.ts
import * as Notifications from 'expo-notifications';
export class NotificationService {
private static instance: NotificationService;
static getInstance() {
if (!this.instance) {
this.instance = new NotificationService();
}
return this.instance;
}
async initialize() {
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
// Request permissions
const hasPermission = await this.requestPermissions();
if (!hasPermission) return;
// Get push token
const token = await this.registerForPushNotifications();
if (token) {
await this.savePushToken(token);
}
// Set up listeners
this.setupNotificationListeners();
}
async requestPermissions() {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
return finalStatus === 'granted';
}
async registerForPushNotifications() {
const token = await Notifications.getExpoPushTokenAsync();
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token.data;
}
}Notification Handlers
typescript
// src/services/notifications/handlers.ts
export function setupNotificationHandlers() {
// Handle received notifications
Notifications.addNotificationReceivedListener((notification) => {
console.log('Notification received:', notification);
// Track analytics
trackNotificationReceived(notification);
// Update app state if needed
updateBadgeCount();
});
// Handle notification taps
Notifications.addNotificationResponseReceivedListener((response) => {
const { notification, actionIdentifier } = response;
// Track interaction
trackNotificationOpened(notification, actionIdentifier);
// Handle navigation
handleNotificationNavigation(notification.request.content.data);
// Handle action
if (actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) {
handleNotificationAction(actionIdentifier, notification);
}
});
}Platform Configuration
iOS Setup
Capabilities
- Enable Push Notifications in Xcode
- Add Background Modes > Remote notifications
Rich Notifications (iOS)
swift
// ios/NotificationService/NotificationService.swift
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return
}
// Download and attach media
if let urlString = bestAttemptContent.userInfo["image"] as? String,
let url = URL(string: urlString) {
downloadAndAttachMedia(url: url, to: bestAttemptContent) { content in
contentHandler(content)
}
} else {
contentHandler(bestAttemptContent)
}
}
}Android Setup
Firebase Configuration
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color" />Notification Categories
Define Categories
typescript
// src/services/notifications/categories.ts
export async function setupNotificationCategories() {
await Notifications.setNotificationCategoryAsync('message', [
{
identifier: 'reply',
buttonTitle: 'Reply',
options: {
opensAppToForeground: false,
isAuthenticationRequired: false,
},
textInput: {
submitButtonTitle: 'Send',
placeholder: 'Type your reply...',
},
},
{
identifier: 'mark_read',
buttonTitle: 'Mark as Read',
options: {
opensAppToForeground: false,
},
},
]);
await Notifications.setNotificationCategoryAsync('order', [
{
identifier: 'track',
buttonTitle: 'Track Order',
options: {
opensAppToForeground: true,
},
},
{
identifier: 'contact',
buttonTitle: 'Contact Support',
options: {
opensAppToForeground: true,
},
},
]);
}Handle Category Actions
typescript
export function handleNotificationAction(
actionId: string,
notification: Notification
) {
switch (actionId) {
case 'reply':
const text = notification.request.content.data.userText;
sendReply(notification.request.content.data.conversationId, text);
break;
case 'mark_read':
markAsRead(notification.request.content.data.messageId);
break;
case 'track':
navigation.navigate('OrderTracking', {
orderId: notification.request.content.data.orderId,
});
break;
}
}Local Notifications
Schedule Local Notifications
typescript
// src/services/notifications/local.ts
export async function scheduleLocalNotification(
title: string,
body: string,
trigger: NotificationTrigger,
data?: any
) {
const id = await Notifications.scheduleNotificationAsync({
content: {
title,
body,
data,
sound: 'default',
badge: 1,
},
trigger,
});
return id;
}
// Daily reminder
await scheduleLocalNotification(
'Daily Reminder',
'Don\'t forget to check in!',
{
hour: 9,
minute: 0,
repeats: true,
}
);
// Delayed notification
await scheduleLocalNotification(
'Task Reminder',
'Your task is due soon',
{
seconds: 60 * 30, // 30 minutes
}
);Rich Media Notifications
Send with Images
typescript
// Server-side
const message = {
to: pushToken,
sound: 'default',
title: 'New Product!',
body: 'Check out our latest arrival',
data: {
productId: '123',
image: 'https://example.com/product.jpg',
},
ios: {
attachments: [{
url: 'https://example.com/product.jpg',
}],
},
android: {
imageUrl: 'https://example.com/product.jpg',
},
};Display Rich Content
typescript
// Client-side handling
export function handleRichNotification(notification: Notification) {
const { title, body, data } = notification.request.content;
if (data.image) {
// Show in-app notification with image
showInAppNotification({
title,
body,
image: data.image,
onPress: () => navigateToProduct(data.productId),
});
}
}Analytics & Tracking
Track Notification Metrics
typescript
// src/services/notifications/analytics.ts
export function trackNotificationReceived(notification: Notification) {
analytics.track('notification_received', {
id: notification.request.identifier,
title: notification.request.content.title,
category: notification.request.content.categoryIdentifier,
data: notification.request.content.data,
});
}
export function trackNotificationOpened(
notification: Notification,
action?: string
) {
analytics.track('notification_opened', {
id: notification.request.identifier,
action: action || 'tap',
timeToOpen: Date.now() - notification.date,
});
}
export function trackNotificationDismissed(notification: Notification) {
analytics.track('notification_dismissed', {
id: notification.request.identifier,
});
}Testing Notifications
Test Push Notifications
typescript
// Development testing
export async function sendTestNotification() {
const token = await AsyncStorage.getItem('push_token');
const response = await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: token,
title: 'Test Notification',
body: 'This is a test push notification',
data: { test: true },
}),
});
return response.json();
}Unit Tests
typescript
// __tests__/notifications.test.ts
describe('NotificationService', () => {
it('requests permissions on initialization', async () => {
const service = NotificationService.getInstance();
const spy = jest.spyOn(Notifications, 'requestPermissionsAsync');
await service.initialize();
expect(spy).toHaveBeenCalled();
});
it('handles notification tap correctly', async () => {
const mockNotification = createMockNotification({
data: { screen: 'Product', productId: '123' },
});
handleNotificationNavigation(mockNotification.request.content.data);
expect(mockNavigation.navigate).toHaveBeenCalledWith('Product', {
productId: '123',
});
});
});Best Practices
- Request Permissions Thoughtfully: Explain why you need permissions
- Handle All States: Account for denied permissions
- Test on Real Devices: Simulators have limitations
- Respect User Preferences: Allow notification customization
- Clean Up: Cancel scheduled notifications when appropriate
Common Patterns
Silent Notifications
typescript
// Update app content without alerting user
export async function handleSilentNotification(data: any) {
if (data.type === 'sync') {
await syncDataInBackground();
} else if (data.type === 'update_badge') {
await Notifications.setBadgeCountAsync(data.count);
}
}Notification Preferences
typescript
// User preference management
export async function updateNotificationPreferences(prefs: Preferences) {
await AsyncStorage.setItem('notification_prefs', JSON.stringify(prefs));
// Update server
await api.updateUserPreferences({
notifications: prefs,
});
}Related Commands
add-screen- Create screens to navigate toimplement-deep-linking- Deep links in notificationssetup-crash-reporting- Track notification errors
