← Back to Guides

Push Notifications Complete Guide

📖 17 min read | 📅 January 2025 | 🏷️ Mobile Development

Introduction

Push notifications are essential for mobile app engagement. This guide covers implementation of Firebase Cloud Messaging (FCM) for Android, Apple Push Notification service (APNs) for iOS, and cross-platform solutions.

1. Firebase Cloud Messaging Setup

// React Native - Install dependencies
npm install @react-native-firebase/app @react-native-firebase/messaging

// Android - google-services.json in android/app/
// iOS - GoogleService-Info.plist in ios/

// Request permission
import messaging from '@react-native-firebase/messaging';

async function requestUserPermission() {
  const authStatus = await messaging().requestPermission();
  const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;

  if (enabled) {
    console.log('Authorization status:', authStatus);
    const token = await messaging().getToken();
    console.log('FCM Token:', token);
    // Send token to your server
  }
}

// Get FCM token
const getFCMToken = async () => {
  try {
    const token = await messaging().getToken();
    await saveTokenToServer(token);
    return token;
  } catch (error) {
    console.error('Error getting FCM token:', error);
  }
};

2. Handle Foreground Notifications

// React Native - Foreground handler
useEffect(() => {
  const unsubscribe = messaging().onMessage(async remoteMessage => {
    console.log('Foreground notification:', remoteMessage);
    
    // Show local notification
    Alert.alert(
      remoteMessage.notification?.title || 'New Notification',
      remoteMessage.notification?.body || ''
    );
  });

  return unsubscribe;
}, []);

// Android - Handle foreground notifications
class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        remoteMessage.notification?.let {
            showNotification(it.title, it.body)
        }
        
        remoteMessage.data.isNotEmpty().let {
            handleDataPayload(remoteMessage.data)
        }
    }
    
    private fun showNotification(title: String?, body: String?) {
        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(title)
            .setContentText(body)
            .setSmallIcon(R.drawable.ic_notification)
            .setAutoCancel(true)
            .build()
            
        NotificationManagerCompat.from(this)
            .notify(NOTIFICATION_ID, notification)
    }
}

// iOS - Handle foreground notifications
func userNotificationCenter(_ center: UNUserNotificationCenter,
                           willPresent notification: UNNotification,
                           withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.banner, .sound, .badge])
}

3. Handle Background Notifications

// React Native - Background handler
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Background notification:', remoteMessage);
  
  // Process notification data
  if (remoteMessage.data) {
    await processNotificationData(remoteMessage.data);
  }
});

// Handle notification opened app
messaging().onNotificationOpenedApp(remoteMessage => {
  console.log('Notification opened app:', remoteMessage);
  
  // Navigate to specific screen
  if (remoteMessage.data?.screen) {
    navigation.navigate(remoteMessage.data.screen);
  }
});

// Check initial notification (app opened from quit state)
messaging()
  .getInitialNotification()
  .then(remoteMessage => {
    if (remoteMessage) {
      console.log('Notification caused app to open:', remoteMessage);
    }
  });

// Android - Handle background clicks
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="NOTIFICATION_CLICKED" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    if (intent?.action == "NOTIFICATION_CLICKED") {
        val screen = intent.getStringExtra("screen")
        // Navigate to screen
    }
}

4. iOS APNs Configuration

// Swift - Request authorization
import UserNotifications

func requestNotificationAuthorization() {
    UNUserNotificationCenter.current()
        .requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }
}

// Register for remote notifications
func application(_ application: UIApplication,
                didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    print("APNs Token:", token)
    // Send to server
}

// Handle notification received
func application(_ application: UIApplication,
                didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    
    if let aps = userInfo["aps"] as? [String: Any] {
        if let alert = aps["alert"] as? String {
            print("Notification:", alert)
        }
    }
    
    completionHandler(.newData)
}

5. Notification Channels (Android 8+)

// Create notification channels
class NotificationHelper(private val context: Context) {
    
    companion object {
        const val CHANNEL_GENERAL = "general"
        const val CHANNEL_PROMOTIONS = "promotions"
        const val CHANNEL_ALERTS = "alerts"
    }
    
    fun createNotificationChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channels = listOf(
                NotificationChannel(
                    CHANNEL_GENERAL,
                    "General Notifications",
                    NotificationManager.IMPORTANCE_DEFAULT
                ).apply {
                    description = "General app notifications"
                },
                NotificationChannel(
                    CHANNEL_PROMOTIONS,
                    "Promotions",
                    NotificationManager.IMPORTANCE_LOW
                ).apply {
                    description = "Promotional offers and updates"
                },
                NotificationChannel(
                    CHANNEL_ALERTS,
                    "Important Alerts",
                    NotificationManager.IMPORTANCE_HIGH
                ).apply {
                    description = "Critical alerts"
                    enableVibration(true)
                    enableLights(true)
                }
            )
            
            val manager = context.getSystemService(NotificationManager::class.java)
            channels.forEach { manager.createNotificationChannel(it) }
        }
    }
}

// Use specific channel
val notification = NotificationCompat.Builder(context, CHANNEL_ALERTS)
    .setContentTitle("Important")
    .setContentText("This is an important alert")
    .setSmallIcon(R.drawable.ic_alert)
    .setPriority(NotificationCompat.PRIORITY_HIGH)
    .build()

6. Rich Notifications

// Android - Rich notification with image
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle("New Message")
    .setContentText("You have a new message")
    .setSmallIcon(R.drawable.ic_message)
    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.profile))
    .setStyle(NotificationCompat.BigPictureStyle()
        .bigPicture(BitmapFactory.decodeResource(resources, R.drawable.big_image))
        .bigLargeIcon(null))
    .build()

// iOS - Rich notifications with media
// Create Notification Service Extension
override func didReceive(_ request: UNNotificationRequest, 
                        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
    if let urlString = request.content.userInfo["image_url"] as? String,
       let fileUrl = URL(string: urlString) {
        
        downloadImage(fileUrl: fileUrl) { attachment in
            if let attachment = attachment {
                bestAttemptContent?.attachments = [attachment]
            }
            contentHandler(bestAttemptContent ?? request.content)
        }
    }
}

// Action buttons
// Android
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle("New Message")
    .setContentText("Reply to this message?")
    .addAction(R.drawable.ic_reply, "Reply", pendingIntent)
    .addAction(R.drawable.ic_dismiss, "Dismiss", dismissIntent)
    .build()

// iOS
let replyAction = UNNotificationAction(
    identifier: "REPLY_ACTION",
    title: "Reply",
    options: [.foreground]
)

let category = UNNotificationCategory(
    identifier: "MESSAGE_CATEGORY",
    actions: [replyAction],
    intentIdentifiers: []
)

UNUserNotificationCenter.current().setNotificationCategories([category])

7. Topic Messaging

// Subscribe to topics
await messaging().subscribeToTopic('news');
await messaging().subscribeToTopic('sports');

// Unsubscribe from topics
await messaging().unsubscribeFromTopic('sports');

// Send to topic from server
const message = {
  notification: {
    title: 'Breaking News',
    body: 'Important update available'
  },
  topic: 'news'
};

await admin.messaging().send(message);

// Conditional topic targeting
const message = {
  notification: {
    title: 'Sports Update',
    body: 'Your team won!'
  },
  condition: "'sports' in topics && 'soccer' in topics"
};

await admin.messaging().send(message);

8. Server-Side Implementation

// Node.js - Send notification with FCM
const admin = require('firebase-admin');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

async function sendNotification(token, title, body, data = {}) {
  const message = {
    token: token,
    notification: {
      title: title,
      body: body
    },
    data: data,
    android: {
      priority: 'high',
      notification: {
        sound: 'default',
        channelId: 'default'
      }
    },
    apns: {
      payload: {
        aps: {
          sound: 'default',
          badge: 1
        }
      }
    }
  };

  try {
    const response = await admin.messaging().send(message);
    console.log('Successfully sent:', response);
    return response;
  } catch (error) {
    console.error('Error sending notification:', error);
    throw error;
  }
}

// Send to multiple devices
async function sendToMultipleDevices(tokens, title, body) {
  const message = {
    notification: { title, body },
    tokens: tokens // Array of tokens
  };

  const response = await admin.messaging().sendMulticast(message);
  console.log(`${response.successCount} messages sent successfully`);
  
  // Handle failed tokens
  if (response.failureCount > 0) {
    response.responses.forEach((resp, idx) => {
      if (!resp.success) {
        console.error(`Failed token: ${tokens[idx]}`);
        // Remove invalid token from database
      }
    });
  }
}

9. Local Notifications

// React Native - Schedule local notification
import notifee from '@notifee/react-native';

async function scheduleLocalNotification() {
  // Create channel
  const channelId = await notifee.createChannel({
    id: 'default',
    name: 'Default Channel',
  });

  // Schedule notification
  await notifee.displayNotification({
    title: 'Reminder',
    body: 'Time to check your app!',
    android: {
      channelId,
      smallIcon: 'ic_launcher',
      pressAction: {
        id: 'default',
      },
    },
  });
}

// Android - Local notification
fun showLocalNotification(context: Context, title: String, message: String) {
    val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
    val notification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setContentTitle(title)
        .setContentText(message)
        .setSmallIcon(R.drawable.ic_notification)
        .setAutoCancel(true)
        .build()
    
    notificationManager.notify(Random.nextInt(), notification)
}

// iOS - Local notification
let content = UNMutableNotificationContent()
content.title = "Reminder"
content.body = "Time to check your app!"
content.sound = .default

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

UNUserNotificationCenter.current().add(request) { error in
    if let error = error {
        print("Error scheduling notification:", error)
    }
}

10. Best Practices

// Token refresh handling
messaging().onTokenRefresh(async token => {
  console.log('New FCM token:', token);
  await updateTokenOnServer(token);
});

// Badge management
// iOS
UIApplication.shared.applicationIconBadgeNumber = 0

// Android
NotificationManagerCompat.from(context).cancelAll()

// Notification analytics
const trackNotificationOpened = (notificationId, campaignId) => {
  analytics().logEvent('notification_opened', {
    notification_id: notificationId,
    campaign_id: campaignId,
    timestamp: Date.now()
  });
};

// Notification preferences
interface NotificationPreferences {
  enabled: boolean;
  categories: {
    news: boolean;
    promotions: boolean;
    updates: boolean;
  };
  quietHours: {
    enabled: boolean;
    start: string;
    end: string;
  };
}

async function updateNotificationPreferences(prefs: NotificationPreferences) {
  if (!prefs.enabled) {
    await messaging().deleteToken();
  } else {
    // Subscribe/unsubscribe from topics
    for (const [category, enabled] of Object.entries(prefs.categories)) {
      if (enabled) {
        await messaging().subscribeToTopic(category);
      } else {
        await messaging().unsubscribeFromTopic(category);
      }
    }
  }
}
💡 Best Practices:

Conclusion

Push notifications are powerful engagement tools when implemented correctly. Follow these patterns to create effective notification strategies that provide value to users and drive app engagement.