Skip to main content

🔔 Push Notifications

Shipnative provides comprehensive support for push notifications, enabling you to engage your users with timely and relevant updates. This guide covers both local and remote notifications, deep linking, and platform-specific configurations for iOS (APNs) and Android (FCM).

Overview

Key features of Shipnative’s push notification system:
  • Local Notifications: Schedule and deliver notifications directly on the user’s device.
  • Remote Notifications: Support for Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android.
  • Deep Linking: Navigate users directly to specific screens within your app when they tap a notification.
  • Mock Mode: Develop and test notification flows without requiring live credentials.
  • Permission Handling: Streamlined process for requesting and managing user notification permissions.
  • Customization: Notification badges, sounds, and categories.

Configuration

iOS Setup (APNs)

For remote push notifications on iOS, you’ll primarily use Apple Push Notification service (APNs). When building with EAS (Expo Application Services), much of this is handled automatically.
  1. Enable Push Notifications Capability:
    • In your Apple Developer account, navigate to “Certificates, Identifiers & Profiles”.
    • Select your App ID and ensure the “Push Notifications” capability is enabled.
    • EAS Build will typically handle the necessary certificates for you.
  2. Update app.json: Ensure your app.json includes the UIBackgroundModes for remote notifications and the expo-notifications plugin configuration:
    {
      "ios": {
        "infoPlist": {
          "UIBackgroundModes": ["remote-notification"]
        }
      },
      "plugins": [
        [
          "expo-notifications",
          {
            "icon": "./assets/notification-icon.png", // Optional: Custom notification icon
            "color": "#000000", // Optional: Accent color for notification icon
            "sounds": ["./assets/notification-sound.wav"] // Optional: Custom notification sound
          }
        ]
      ]
    }
    
  3. Build with EAS: EAS Build simplifies the process by managing APNs certificates and provisioning profiles.
    eas build --platform ios --profile production
    

Android Setup (FCM)

For remote push notifications on Android, you’ll use Firebase Cloud Messaging (FCM).
  1. Create Firebase Project:
    • Go to the Firebase Console.
    • Create a new project or select an existing one.
    • Add an Android app to your Firebase project, ensuring the package name matches your app’s package name in app.json.
  2. Download google-services.json:
    • From your Firebase project settings, download the google-services.json file.
    • Place this file in your apps/app/ directory. EAS Build will automatically detect and use it.
  3. Obtain FCM Server Key:
    • In the Firebase Console, navigate to Project Settings > Cloud Messaging.
    • Copy your Server key.
  4. Configure Environment Variable: Add your FCM Server Key to your apps/app/.env file:
    EXPO_PUBLIC_FCM_SERVER_KEY=your-fcm-server-key
    
  5. Update app.json: Ensure your app.json references the googleServicesFile and includes necessary permissions:
    {
      "android": {
        "googleServicesFile": "./google-services.json",
        "permissions": [
          "RECEIVE_BOOT_COMPLETED",
          "VIBRATE"
        ]
      }
    }
    
For more detailed information on configuring push notifications with Expo, refer to the official Expo Notifications documentation.

Usage

Shipnative provides a useNotificationStore and utility functions to manage notifications.

Requesting Permission

It’s best practice to request notification permission when the user understands the value they will receive.
import { useNotificationStore } from '@/stores/notificationStore'import { View, Text, Button, Alert, Linking } from 'react-native'

function NotificationPermissionPrompt() {
  const { requestPermission, permissionStatus } = useNotificationStore()
  
  const handleRequestPermission = async () => {
    // Explain the benefit before asking
    Alert.alert(
      'Enable Notifications',
      'Get timely updates and reminders to enhance your experience!',
      [
        { text: 'Not Now', style: 'cancel' },
        { 
          text: 'Enable',
          onPress: async () => {
            const granted = await requestPermission()
            if (granted) {
              console.log('Notifications enabled!')
            } else {
              // Guide user to settings if denied
              Alert.alert(
                'Permission Denied',
                'To enable notifications, please go to your device settings.',
                [
                  { text: 'Cancel', style: 'cancel' },
                  { text: 'Open Settings', onPress: () => Linking.openSettings() }
                ]
              )
            }
          }
        }
      ]
    )
  }
  
  return (
    <View>
      <Text>Notification Status: {permissionStatus}</Text>
      {permissionStatus === 'undetermined' && (
        <Button onPress={handleRequestPermission} title="Enable Notifications" />
      )}
    </View>
  )
}

Scheduling Local Notifications

Local notifications are delivered directly by the device.
import { scheduleNotification } from '@/services/notifications'
// Schedule a notification for 1 hour from now
await scheduleNotification({
  title: 'Reminder',
  body: 'Time to check your progress!',
  data: { screen: 'Profile' }, // Custom data for deep linking
  trigger: {
    seconds: 3600, // 1 hour
  },
})

// Schedule a daily notification at 8 PM
await scheduleNotification({
  title: 'Daily Summary',
  body: 'Your daily stats are ready!',
  trigger: {
    hour: 20, // 8 PM
    minute: 0,
    repeats: true,
  },
})

Sending Remote Notifications (Backend)

Remote notifications are sent from your backend server to APNs (iOS) or FCM (Android) using the Expo Push Notification Service.
  1. Obtain Push Token: Your app needs to register for push notifications and send the pushToken to your backend. The useNotificationStore handles obtaining this token.
    import { useNotificationStore } from '@/stores/notificationStore'
    const { pushToken } = useNotificationStore()
    // Send this pushToken to your backend to store it for the user
    
  2. Send from Backend: Use the expo-server-sdk in your backend to send notifications.
    // Example Node.js backend code
    import { Expo } from 'expo-server-sdk';
    
    const expo = new Expo();
    
    // Assuming you have stored the user's pushToken on your backend
    const pushToken = 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]'; 
    
    const messages = [{
      to: pushToken,
      sound: 'default',
      title: 'New Message',
      body: 'You have a new message from John',
      data: { 
        screen: 'Messages', // For deep linking
        messageId: '123'
      },
    }];
    
    (async () => {
      const chunks = expo.chunkPushNotifications(messages);
      for (const chunk of chunks) {
        try {
          const ticketChunk = await expo.sendPushNotificationsAsync(chunk);
          console.log('Sent:', ticketChunk);
        } catch (error) {
          console.error('Error sending push notification:', error);
        }
      }
    })();
    
For more details on sending notifications from your backend, refer to the Expo Push Notification Service documentation.

Handling Notification Taps (Deep Linking)

When a user taps a notification, you can navigate them to a specific screen using deep linking.
import { useEffect } from 'react'
import * as Notifications from 'expo-notifications'
import { router } from 'expo-router' // Assuming Expo Router is used

function AppNotificationHandler() {
  useEffect(() => {
    // This listener is fired whenever a user taps on or interacts with a notification
    const subscription = Notifications.addNotificationResponseReceivedListener(response => {
      const data = response.notification.request.content.data
      
      // Check if the notification contains a 'screen' property for deep linking
      if (data.screen) {
        router.push(`/${data.screen}`) // Navigate to the specified screen
      }
    })
    return () => Notifications.removeNotificationSubscription(subscription)
  }, [])
  
  return null // This component doesn't render anything
}

API Reference

useNotificationStore

The Zustand store for managing notification state and actions.
import { useNotificationStore } from '@/stores/notificationStore'

const {
  // State
  permissionStatus,    // 'granted' | 'denied' | 'undetermined'
  isPushEnabled,       // User preference toggle (e.g., from settings)
  pushToken,           // The Expo push token for this device
  notifications,       // Array of recently received notifications
  unreadCount,         // Number of unread notifications
  
  // Actions
  togglePush,          // Toggles user's push notification preference
  requestPermission,   // Requests notification permission from the OS
  registerForPushNotifications, // Registers device for push and gets token
  scheduleNotification, // Schedules a local notification
  cancelNotification,  // Cancels a scheduled local notification
  cancelAllNotifications, // Cancels all scheduled local notifications
  setBadgeCount,       // Sets the app icon badge number
  clearBadge,          // Clears the app icon badge
  markAsRead,          // Marks a specific notification as read
} = useNotificationStore()

notifications Service

Utility functions for direct interaction with expo-notifications.
import {
  requestPermission,
  registerForPushNotifications,
  scheduleNotification,
  cancelNotification,
  cancelAllNotifications,
  setBadgeCount,
  addNotificationReceivedListener,
  addNotificationResponseReceivedListener,
} from '@/services/notifications'

// Request permission
const { status } = await requestPermission()

// Get push token
const token = await registerForPushNotifications()

// Schedule notification
const notificationId = await scheduleNotification({
  title: 'Hello',
  body: 'World',
  trigger: { seconds: 60 },
})

// Cancel notification
await cancelNotification(notificationId)

// Listen for notifications received while app is foregrounded
addNotificationReceivedListener(notification => {
  console.log('Received:', notification)
})

Advanced Usage

Notification Categories

Define interactive notification categories with custom actions (e.g., “Reply”, “Mark as Read”).
import * as Notifications from 'expo-notifications'

// Define a category for messages
Notifications.setNotificationCategoryAsync('message', [
  {
    identifier: 'reply',
    buttonTitle: 'Reply',
    options: { opensAppToForeground: true },
  },
  {
    identifier: 'mark-as-read',
    buttonTitle: 'Mark as Read',
    options: { opensAppToForeground: false }, // Action without opening app
  },
])

// Send a notification with this category
await scheduleNotification({
  title: 'New Message',
  body: 'John sent you a message',
  categoryIdentifier: 'message',
  trigger: null, // Immediate delivery
})

// Handle action when user taps a button in the notification
Notifications.addNotificationResponseReceivedListener(response => {
  if (response.actionIdentifier === 'reply') {
    // Logic to open a reply screen
  } else if (response.actionIdentifier === 'mark-as-read') {
    // Logic to mark message as read in background
  }
})

Notification Badges

Control the number displayed on your app icon.
import { setBadgeCount } from '@/services/notifications'

// Set the app icon badge to 5
await setBadgeCount(5)

// Clear the app icon badge
await setBadgeCount(0)

Testing

Mock Mode Testing

  1. Ensure no EXPO_PUBLIC_FCM_SERVER_KEY is set in your apps/app/.env file.
  2. Run your app (yarn ios or yarn android).
  3. Check your console for “Mock Notifications: ENABLED”.
  4. Schedule a test local notification (e.g., with a 5-second trigger).
  5. Verify the notification appears on your device/simulator.

Production Testing (iOS)

  1. Build your app for a physical device using EAS (e.g., eas build --platform ios --profile development:device). Push notifications generally do not work reliably on iOS simulators.
  2. Install the app on a physical iOS device.
  3. Run the app and grant notification permissions.
  4. Send a test remote notification from your backend.
  5. Verify the notification appears on the device.

Production Testing (Android)

  1. Build your app for a device using EAS (e.g., eas build --platform android --profile development).
  2. Install the APK on a physical Android device or emulator.
  3. Run the app and grant notification permissions.
  4. Send a test remote notification from the Firebase Console or your backend.
  5. Verify the notification appears on the device.

Troubleshooting

Notifications Not Appearing

  • Check Permissions: Ensure the app has been granted notification permissions. You can check the status using useNotificationStore().permissionStatus.
  • Check Push Token: Verify that a push token is being generated and sent to your backend.
  • iOS Specific: Remote notifications require a physical device. Ensure APNs certificates are correctly configured (EAS usually handles this).
  • Android Specific: Ensure google-services.json is correctly placed in apps/app/ and your EXPO_PUBLIC_FCM_SERVER_KEY is correct.
  • Backend Issues: If sending remote notifications, check your backend logs for errors when calling the Expo Push Notification Service.
  • Check Data Payload: Ensure your notification’s data payload correctly specifies the screen and any necessary parameters.
  • Verify Router Paths: Confirm that the paths used in your deep linking logic (e.g., router.push('/${data.screen}')) match your expo-router configuration.
  • Test Manually: You can test deep links manually using xcrun simctl openurl booted "your-scheme://your-path" on iOS simulators.

Mock mode in production

Problem: Your app is using mock notification services in production. Solution:
  • Verify that your apps/app/.env file exists and is correctly configured with EXPO_PUBLIC_FCM_SERVER_KEY (for Android).
  • Ensure your environment variables are prefixed with EXPO_PUBLIC_.
  • Restart your Metro bundler with yarn app:start --clear to ensure environment variables are reloaded.
  • Review your build configuration to ensure .env variables are correctly bundled.