Back to Blog

Push Notifications Implementation Guide 2026: iOS, Android & Cross-Platform

Complete guide to implementing push notifications in mobile apps. Learn FCM, APNs, rich notifications, and best practices for iOS and Android.

Hevcode Team
January 22, 2026

Push notifications are essential for user engagement and retention. This guide covers implementation for iOS, Android, React Native, and Flutter, including rich notifications and best practices.

How Push Notifications Work

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Your App   │───▶│ Your Server │───▶│  FCM/APNs   │
│  (Client)   │    │  (Backend)  │    │  (Gateway)  │
└─────────────┘    └─────────────┘    └──────┬──────┘
                                             │
                                             ▼
                                      ┌─────────────┐
                                      │   Device    │
                                      └─────────────┘
  1. App registers with FCM/APNs
  2. App receives device token
  3. Token sent to your backend
  4. Backend sends notification via FCM/APNs
  5. Device receives and displays notification

Firebase Cloud Messaging (FCM)

FCM works for both Android and iOS.

Setup

React Native:

npm install @react-native-firebase/app @react-native-firebase/messaging

Flutter:

flutter pub add firebase_core firebase_messaging

Request Permission

React Native:

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

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

  if (enabled) {
    console.log('Notification permission granted');
    return true;
  }
  return false;
}

Flutter:

import 'package:firebase_messaging/firebase_messaging.dart';

Future<bool> requestPermission() async {
  final messaging = FirebaseMessaging.instance;

  final settings = await messaging.requestPermission(
    alert: true,
    badge: true,
    sound: true,
    provisional: false,
  );

  return settings.authorizationStatus == AuthorizationStatus.authorized;
}

Get Device Token

React Native:

async function getToken() {
  // Get token
  const token = await messaging().getToken();
  console.log('FCM Token:', token);

  // Save to backend
  await saveTokenToServer(token);

  // Listen for token refresh
  messaging().onTokenRefresh(async newToken => {
    await saveTokenToServer(newToken);
  });

  return token;
}

Flutter:

Future<String?> getToken() async {
  final token = await FirebaseMessaging.instance.getToken();
  print('FCM Token: $token');

  // Save to backend
  await saveTokenToServer(token);

  // Listen for token refresh
  FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
    saveTokenToServer(newToken);
  });

  return token;
}

Handle Notifications

React Native:

import { useEffect } from 'react';
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance } from '@notifee/react-native';

// Background handler (must be outside component)
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Background message:', remoteMessage);
  // Handle background notification
});

function App() {
  useEffect(() => {
    // Foreground messages
    const unsubscribe = messaging().onMessage(async remoteMessage => {
      // Display notification using notifee
      await notifee.displayNotification({
        title: remoteMessage.notification?.title,
        body: remoteMessage.notification?.body,
        android: {
          channelId: 'default',
          importance: AndroidImportance.HIGH,
          pressAction: { id: 'default' },
        },
      });
    });

    // Notification opened app (background)
    messaging().onNotificationOpenedApp(remoteMessage => {
      console.log('Notification opened app:', remoteMessage);
      handleNotificationNavigation(remoteMessage.data);
    });

    // Check if app opened from notification (quit state)
    messaging()
      .getInitialNotification()
      .then(remoteMessage => {
        if (remoteMessage) {
          handleNotificationNavigation(remoteMessage.data);
        }
      });

    return unsubscribe;
  }, []);

  return <YourApp />;
}

Flutter:

import 'package:firebase_messaging/firebase_messaging.dart';

// Background handler (must be top-level)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print('Background message: ${message.messageId}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

class NotificationService {
  void initialize() {
    // Foreground messages
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Foreground message: ${message.notification?.title}');
      // Show local notification
      _showLocalNotification(message);
    });

    // Notification opened app
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('Notification opened app: ${message.data}');
      _handleNotificationNavigation(message.data);
    });

    // Check initial notification
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        _handleNotificationNavigation(message.data);
      }
    });
  }
}

Backend Implementation

Node.js with Firebase Admin SDK

const admin = require('firebase-admin');

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

// Send to single device
async function sendNotification(token, title, body, data = {}) {
  const message = {
    notification: {
      title,
      body,
    },
    data,
    token,
  };

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

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

  const response = await admin.messaging().sendEachForMulticast(message);
  console.log(`Success: ${response.successCount}, Failure: ${response.failureCount}`);

  // Handle failed tokens
  if (response.failureCount > 0) {
    const failedTokens = [];
    response.responses.forEach((resp, idx) => {
      if (!resp.success) {
        failedTokens.push(tokens[idx]);
      }
    });
    // Remove failed tokens from database
    await removeInvalidTokens(failedTokens);
  }

  return response;
}

// Send to topic
async function sendToTopic(topic, title, body, data = {}) {
  const message = {
    notification: { title, body },
    data,
    topic,
  };

  return admin.messaging().send(message);
}

// Send with condition
async function sendWithCondition(condition, title, body) {
  // Condition example: "'news' in topics && 'sports' in topics"
  const message = {
    notification: { title, body },
    condition,
  };

  return admin.messaging().send(message);
}

Rich Notifications (Images, Actions)

// Android rich notification
const androidMessage = {
  notification: {
    title: 'New Photo',
    body: 'Someone shared a photo with you',
    imageUrl: 'https://example.com/image.jpg',
  },
  android: {
    notification: {
      channelId: 'photos',
      priority: 'high',
      defaultSound: true,
      defaultVibrateTimings: true,
      imageUrl: 'https://example.com/image.jpg',
      actions: [
        {
          action: 'like',
          title: 'Like',
          icon: 'ic_like',
        },
        {
          action: 'reply',
          title: 'Reply',
          icon: 'ic_reply',
        },
      ],
    },
  },
  token: deviceToken,
};

// iOS rich notification
const iosMessage = {
  notification: {
    title: 'New Photo',
    body: 'Someone shared a photo with you',
  },
  apns: {
    payload: {
      aps: {
        'mutable-content': 1,
        'content-available': 1,
        sound: 'default',
        badge: 1,
        category: 'PHOTO_CATEGORY',
      },
    },
    fcm_options: {
      image: 'https://example.com/image.jpg',
    },
  },
  token: deviceToken,
};

// Combined platform message
const message = {
  notification: {
    title: 'New Message',
    body: 'You have a new message',
  },
  android: {
    notification: {
      channelId: 'messages',
      priority: 'high',
      imageUrl: 'https://example.com/image.jpg',
    },
  },
  apns: {
    payload: {
      aps: {
        'mutable-content': 1,
        sound: 'default',
        badge: 1,
      },
    },
    fcm_options: {
      image: 'https://example.com/image.jpg',
    },
  },
  data: {
    type: 'message',
    chatId: '123',
    senderId: '456',
  },
  token: deviceToken,
};

Android Notification Channels

Android 8.0+ requires notification channels:

React Native (with Notifee):

import notifee, { AndroidImportance } from '@notifee/react-native';

async function createChannels() {
  // Messages channel (high priority)
  await notifee.createChannel({
    id: 'messages',
    name: 'Messages',
    description: 'Chat message notifications',
    importance: AndroidImportance.HIGH,
    sound: 'message_sound',
    vibration: true,
  });

  // Updates channel (default priority)
  await notifee.createChannel({
    id: 'updates',
    name: 'Updates',
    description: 'App updates and news',
    importance: AndroidImportance.DEFAULT,
  });

  // Promotions channel (low priority)
  await notifee.createChannel({
    id: 'promotions',
    name: 'Promotions',
    description: 'Deals and offers',
    importance: AndroidImportance.LOW,
  });
}

Flutter:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

Future<void> createNotificationChannels() async {
  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  const messagesChannel = AndroidNotificationChannel(
    'messages',
    'Messages',
    description: 'Chat message notifications',
    importance: Importance.high,
    playSound: true,
  );

  const updatesChannel = AndroidNotificationChannel(
    'updates',
    'Updates',
    description: 'App updates and news',
    importance: Importance.defaultImportance,
  );

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(messagesChannel);

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(updatesChannel);
}

iOS-Specific Configuration

APNs Setup in Xcode

  1. Enable Push Notifications capability
  2. Enable Background Modes > Remote notifications
  3. Add APNs key to Firebase Console

iOS Notification Categories (Actions)

Swift (iOS Native):

import UserNotifications

func registerNotificationCategories() {
    let likeAction = UNNotificationAction(
        identifier: "LIKE_ACTION",
        title: "Like",
        options: []
    )

    let replyAction = UNTextInputNotificationAction(
        identifier: "REPLY_ACTION",
        title: "Reply",
        options: [],
        textInputButtonTitle: "Send",
        textInputPlaceholder: "Type a message..."
    )

    let messageCategory = UNNotificationCategory(
        identifier: "MESSAGE_CATEGORY",
        actions: [likeAction, replyAction],
        intentIdentifiers: [],
        options: .customDismissAction
    )

    UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}

Topic Subscriptions

// React Native - Subscribe to topic
await messaging().subscribeToTopic('news');
await messaging().subscribeToTopic('sports');

// Unsubscribe
await messaging().unsubscribeFromTopic('news');
// Flutter
await FirebaseMessaging.instance.subscribeToTopic('news');
await FirebaseMessaging.instance.unsubscribeFromTopic('news');

Scheduled Notifications

For scheduled notifications, use local notifications:

React Native (Notifee):

import notifee, { TimestampTrigger, TriggerType } from '@notifee/react-native';

async function scheduleNotification(date, title, body) {
  const trigger: TimestampTrigger = {
    type: TriggerType.TIMESTAMP,
    timestamp: date.getTime(),
  };

  await notifee.createTriggerNotification(
    {
      title,
      body,
      android: {
        channelId: 'reminders',
      },
    },
    trigger
  );
}

// Schedule for specific time
await scheduleNotification(
  new Date(Date.now() + 3600000), // 1 hour from now
  'Reminder',
  "Don't forget to check your tasks!"
);

// Repeating notification
const intervalTrigger = {
  type: TriggerType.INTERVAL,
  interval: 24,
  timeUnit: TimeUnit.HOURS,
};

Best Practices

1. Request Permission at Right Time

// Don't request immediately on app launch
// Instead, request when user understands the value

function showNotificationPrompt() {
  Alert.alert(
    'Stay Updated',
    'Enable notifications to get real-time updates about your orders and messages.',
    [
      { text: 'Not Now', style: 'cancel' },
      {
        text: 'Enable',
        onPress: async () => {
          await requestPermission();
        },
      },
    ]
  );
}

2. Handle Token Changes

// Save token with user data
async function saveTokenToServer(token) {
  const userId = getCurrentUserId();
  if (!userId) return;

  await api.post('/users/push-token', {
    userId,
    token,
    platform: Platform.OS,
    deviceId: getUniqueDeviceId(),
  });
}

// Remove token on logout
async function onLogout() {
  const token = await messaging().getToken();
  await api.delete('/users/push-token', { token });
  await messaging().deleteToken();
}

3. Group Notifications

// Backend - Send with group key
const message = {
  notification: {
    title: 'New Message',
    body: 'You have 5 new messages',
  },
  android: {
    notification: {
      tag: 'messages', // Groups notifications with same tag
      notificationCount: 5,
    },
  },
  token: deviceToken,
};

4. Silent Notifications for Data Sync

// Send data-only notification
const dataMessage = {
  data: {
    type: 'sync',
    entity: 'messages',
    timestamp: Date.now().toString(),
  },
  // No notification field = silent
  token: deviceToken,
};

// Handle in app
messaging().onMessage(async message => {
  if (message.data?.type === 'sync') {
    // Trigger background sync
    await syncData(message.data.entity);
  }
});

5. Analytics and Testing

// Track notification metrics
async function trackNotificationMetrics(notificationId, action) {
  await analytics.logEvent('notification_interaction', {
    notification_id: notificationId,
    action, // 'received', 'opened', 'dismissed'
    timestamp: Date.now(),
  });
}

Troubleshooting

Common Issues

  1. Token not received

    • Check internet connection
    • Verify Firebase configuration
    • iOS: Check APNs setup
  2. Notification not displayed

    • Android: Create notification channel
    • iOS: Check permission status
    • Foreground: Implement local notification display
  3. Background handler not called

    • Must be top-level function
    • Check Firebase configuration
  4. Invalid token errors

    • Token expired - refresh
    • App uninstalled - remove from DB

Conclusion

Push notifications are crucial for user engagement. Start with basic notifications, then add rich media, actions, and scheduling. Always respect user preferences and provide value with each notification.

Need help implementing push notifications? Contact Hevcode for professional mobile app development with push notification integration.

Related Articles

Tags:Push NotificationsFCMAPNsMobile DevelopmentTutorial

Need help with your project?

We've helped 534+ clients build successful apps. Let's discuss yours.

Ready to Build Your App?

534+ projects delivered • 4.9★ rating • 6+ years experience

Let's discuss your project — no obligations, just a straightforward conversation.