Skip to content

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 expo

Firebase with Rich Media

bash
/template setup-push-notifications firebase --rich-media --analytics

Full Setup with Categories

bash
/template setup-push-notifications expo --categories --local --analytics

What 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 class

Notification 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

  1. Enable Push Notifications in Xcode
  2. 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

  1. Request Permissions Thoughtfully: Explain why you need permissions
  2. Handle All States: Account for denied permissions
  3. Test on Real Devices: Simulators have limitations
  4. Respect User Preferences: Allow notification customization
  5. 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,
  });
}

Built with ❤️ for the AI Coding community, by Praney Behl