Apple Push Notification in Node JS

M.RAGHURAMM.RAGHURAM
12 min read

Certainly! Let's break down the provided Node.js code line by line:

const admin = require('firebase-admin');
const apn = require('apn');
const path = require('path');
require('dotenv').config();
  • Import necessary modules:

    • firebase-admin: Used for interacting with Firebase services.

    • apn: A Node.js module for working with Apple Push Notification service (APNs).

    • path: The built-in Node.js module for handling file paths.

    • dotenv: Used to load environment variables from a .env file into process.env.

// Path to your service account JSON file
const serviceAccount = require('../../config/firebase_config.json');
  • Specify the path to the service account JSON file for Firebase authentication.
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });
}
  • Check if the Firebase admin app is not already initialized, and if not, initialize it with the provided service account credentials.
// APNs initialization
const apnKeyPath = path.resolve(__dirname, 'AuthKey_5B2FFG64P6.p8');
const apnProvider = new apn.Provider({
  token: {
    key: apnKeyPath,
    keyId: process.env.APN_KEY_ID,
    teamId: process.env.APN_TEAM_ID,
  },
  production: true // Set to true for production
});
  • Set up APNs (Apple Push Notification service) initialization:

    • apnKeyPath: Specify the path to the authentication key file for APNs.

    • apnProvider: Create a new instance of the APNs Provider using the specified key, key ID, team ID, and set the environment to production.

// Function to send push notifications
const sendPushNotification = async (req, res) => {
  try {
    const { apnDeviceToken } = req.body;

    if (!apnDeviceToken) {
      return res.status(400).json({ error: 'APNs device token is missing in the request body' });
    }
  • Define an asynchronous function sendPushNotification that takes a request and response as parameters.

  • Extract the apnDeviceToken from the request body and check if it's missing, returning a 400 error if so.

    // Send message via APNs directly
    const apnNote = new apn.Notification({
      alert: 'Hello, World!',
      sound: 'default',
      topic: 'com.example.prayojanaNew' // Replace with your app's bundle identifier
    });
  • Create an APNs Notification object (apnNote) with a simple message and sound. The topic should be replaced with your app's bundle identifier.
    const apnResponse = await apnProvider.send(apnNote, apnDeviceToken);
    console.log('APNs Response:', apnResponse, 'for device:', apnDeviceToken);
  • Send the APNs notification using the send method of the APNs provider and log the response.
    // Log the failed devices
    if (apnResponse.failed && apnResponse.failed.length > 0) {
      const failedDevice = apnResponse.failed[0];
      console.error('Failed devices:', apnResponse.failed);

      if (failedDevice.response && failedDevice.response.reason === 'BadDeviceToken') {
        console.log('Device token is invalid. Update your records accordingly.');
      }
    }
  • If there are failed devices in the response, log the information. If the failure reason is 'BadDeviceToken', log a message indicating that the device token is invalid.
    return res.json({ message: 'Notification sent successfully' });
  } catch (error) {
    console.error('Error sending notification:', error);
    return res.status(500).json({ error: 'Failed to send notification' });
  }
};
  • If the notification is sent successfully, respond with a success message. If there's an error during the process, log the error and respond with a 500 status and an error message.
// Export the function
module.exports = { sendPushNotification };
  • Export the sendPushNotification function to make it available for use in other parts of your application.

This code essentially sets up a Node.js server function to send push notifications using Firebase for authentication and the APNs service for iOS devices.

COMPLETE CODE

Certainly! Below is the code for sending APNs (Apple Push Notification Service) notifications in Node.js using the apn library. I've also added comments to explain each part of the code:

// Import necessary libraries and modules
const admin = require('firebase-admin');
const apn = require('apn');
const path = require('path');
require('dotenv').config(); // Load environment variables from .env file

// Path to your Firebase service account JSON file
const serviceAccount = require('../../config/firebase_config.json');

// Initialize Firebase Admin SDK
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });
}

// Path to your APNs authentication key file
const apnKeyPath = path.resolve(__dirname, 'AuthKey_5B2FFG64P6.p8');

// Initialize APNs provider
const apnProvider = new apn.Provider({
  token: {
    key: apnKeyPath,
    keyId: process.env.APN_KEY_ID,
    teamId: process.env.APN_TEAM_ID,
  },
  production: true, // Set to true for production
});

// Function to send push notifications via APNs
const sendPushNotification = async (req, res) => {
  try {
    // Extract APNs device token from the request body
    const { apnDeviceToken } = req.body;

    // Check if the device token is provided
    if (!apnDeviceToken) {
      return res.status(400).json({ error: 'APNs device token is missing in the request body' });
    }

    // Create an APNs notification with a default sound and alert message
    const apnNote = new apn.Notification({
      alert: 'Hello, World!',
      sound: 'default', // You can replace 'default' with the name of a specific sound file in your app
      topic: 'com.example.prayojanaNew', // Replace with your app's bundle identifier
    });

    // Send the APNs notification to the specified device token
    const apnResponse = await apnProvider.send(apnNote, apnDeviceToken);

    // Log the APNs response and the device token
    console.log('APNs Response:', apnResponse, 'for device:', apnDeviceToken);

    // Log the failed devices, if any
    if (apnResponse.failed && apnResponse.failed.length > 0) {
      const failedDevice = apnResponse.failed[0];
      console.error('Failed devices:', apnResponse.failed);

      // Check if the failure reason is a bad device token
      if (failedDevice.response && failedDevice.response.reason === 'BadDeviceToken') {
        console.log('Device token is invalid. Update your records accordingly.');
      }
    }

    // Return a success message in the response
    return res.json({ message: 'Notification sent successfully' });
  } catch (error) {
    // Handle errors and return an error response
    console.error('Error sending notification:', error);
    return res.status(500).json({ error: 'Failed to send notification' });
  }
};

// Export the function for use in other modules
module.exports = { sendPushNotification };

This code sets up a Node.js server route to handle incoming requests and send push notifications using the APNs service. It uses the apn library for APNs communication and assumes you have set up the necessary environment variables for the APNs key information (APN_KEY_ID and APN_TEAM_ID). Make sure to replace the placeholder values with your actual bundle identifier and adjust the code as needed for your specific use case.

Here is the complete code Certainly! Let's go into more detail, providing line-by-line explanations for the code:

const admin = require('firebase-admin');
const apn = require('apn');
const pool = require('../../config/db');
const serviceAccount = require('../../config/firebase_config.json');
const path = require('path');
require('dotenv').config();
  • Imports: Import necessary libraries and modules. admin is the Firebase Admin SDK for interacting with Firebase services, apn is the Apple Push Notification Service library, pool is likely a PostgreSQL connection pool, and serviceAccount is the configuration file for Firebase. path is a Node.js module for handling file paths, and dotenv is used for loading environment variables from a .env file.
const apnProvider = new apn.Provider({
  token: {
    key: 'config/AuthKey_5B2FFG64P6.p8',
    keyId: process.env.APN_KEY_ID,
    teamId: process.env.APN_TEAM_ID,
  },
  production: true, // Set to true for production
});
  • APN Initialization: Create a new instance of the APN Provider using the provided authentication information. The APN key file, key ID, and team ID are loaded from environment variables. The production flag is set based on the environment.
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });
}
  • Firebase Admin Initialization: Initialize the Firebase Admin SDK if it hasn't been initialized already. It uses the service account credentials from the provided file.
const sendPushNotification = async (req, res) => {
  try {
    // Functionality to send push notifications...
  } catch (error) {
    console.error('Error occurred:', error);
    res.status(500).json({ message: 'Internal Server Error' });
  }
};

module.exports = sendPushNotification;
  • Function Declaration: Define the sendPushNotification function as an asynchronous function. This function will handle the logic for sending push notifications.

Now, let's go into the sendPushNotification function:

const { user_id, event_type, ref_id } = req.body;
const { processedData } = req;
  • Request Parsing: Extract relevant data from the request body, assuming it's a JSON payload containing user_id, event_type, and ref_id. Additionally, it expects processedData in the request, likely processed by middleware.
// Get APN registration tokens
const apnTokensResult = await pool.query('SELECT reg_id FROM notification_devices WHERE user_id = $1 AND is_not_expired = true', [user_id]);
const apnTokens = apnTokensResult.rows.map((row) => row.reg_id);
  • Database Query (APN): Retrieve Apple Push Notification (APN) registration tokens from the database for the specified user_id, where tokens are not expired.
// Get FCM registration tokens
const fcmTokensResult = await pool.query('SELECT reg_id FROM notification_devices WHERE user_id = $1 AND is_not_expired = true AND device = $2', [user_id, 'Android']);
const fcmTokens = fcmTokensResult.rows.map((row) => row.reg_id);
  • Database Query (FCM): Retrieve Firebase Cloud Messaging (FCM) registration tokens for Android devices from the database for the specified user_id, where tokens are not expired.
if (apnTokens.length === 0 && fcmTokens.length === 0) {
  return res.status(404).json({ message: 'Registration IDs not found' });
}
  • Token Validation: If no APN or FCM tokens are found, return a 404 status with a corresponding message.
const messages = [];
const invalidTokens = [];
  • Arrays Initialization: Initialize arrays to store push notification messages and invalid tokens.
const generateNotificationMessage = (event, processedData) => {
  let title = '';
  let message = '';
  switch (event) {
    // Cases for different events...
  }
  return { title, message };
};
  • Message Generation Function: Define a function to generate notification messages based on the event type and processed data.
// Generate APN notifications
apnTokens.forEach((token) => {
  const { title, message } = generateNotificationMessage(event_type, processedData);
  const apnNote = new apn.Notification({
    alert: {
      title: title,
      body: message,
    },
    sound: 'default',
    topic: 'com.example.prayojanaNew', // Replace with your app's bundle identifier
    payload: {
      ref_id: '' + ref_id,
      type: event_type,
    },
  });
  messages.push({ apnNote, token, platform: 'apn' });
});
  • APN Message Generation: Iterate over APN tokens, generate APN notification objects, and push them to the messages array.
// Generate FCM notifications
fcmTokens.forEach((token) => {
  const { title, message } = generateNotificationMessage(event_type, processedData);
  const fcmMessage = {
    notification: {
      title: title,
      body: message,
    },
    data: {
      ref_id: '' + ref_id,
      type: event_type,
    },
    token: token,
  };
  messages.push({ fcmMessage, token, platform: 'fcm' });
});
  • FCM Message Generation: Iterate over FCM tokens, generate FCM notification objects, and push them to the messages array.
const responses = await Promise.allSettled(
  messages.map(async (message) => {
    if (message.platform === 'apn') {


      return apnProvider.send(message.apnNote, message.token);
    } else {
      return admin.messaging().send(message.fcmMessage);
    }
  })
);
  • Concurrent Message Sending: Use Promise.allSettled to send APN and FCM messages concurrently. Handle responses based on the platform.
responses.forEach((response, index) => {
  if (response.status === 'rejected') {
    invalidTokens.push(messages[index].token);
  }
});
  • Handle Responses: Iterate over responses, collect invalid tokens for rejected notifications.
if (invalidTokens.length > 0) {
  await pool.query('UPDATE notification_devices SET is_not_expired = false WHERE reg_id = ANY($1)', [invalidTokens]);
}
  • Update Database: If there are invalid tokens, update the database to mark them as expired.
res.status(200).json({ message: 'Notifications sent successfully', reg_id: invalidTokens });
  • Response: Send a success response with a message and information about invalid tokens.

This documentation provides a detailed explanation of each part of the code. To execute this code:

  1. Set up a PostgreSQL database and configure the connection in config/db.js.

  2. Create the necessary tables and data in the database.

  3. Configure the paths and environment variables in the code.

  4. Install required packages: npm install apn firebase-admin pg dotenv.

  5. Run the application using Node.js.

Make sure to handle secure storage of sensitive information like API keys and credentials. Additionally, ensure that the necessary certificates and configurations are set up for APN and FCM.

To combine the FCM and APN functionality into a single route, you can create a unified route handler that calls both notify and sendPushNotification. Here's an example of how you can achieve this:

const admin = require('firebase-admin');
const apn = require('apn');
const pool = require('../../config/db');
const serviceAccount = require('../../config/firebase_config.json');
const path = require('path');
require('dotenv').config();

// APNs initialization
const apnProvider = new apn.Provider({
  token: {
    key: 'config/AuthKey_5B2FFG64P6.p8',
    keyId: process.env.APN_KEY_ID,
    teamId: process.env.APN_TEAM_ID,
  },
  production: true, // Set to true for production
});

if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  });
}

const sendCombinedNotifications = async (req, res) => {
  try {
    const { user_id, event_type, ref_id } = req.body;
    const { processedData } = req;

    // Get APN registration tokens
    const apnTokensResult = await pool.query('SELECT reg_id FROM notification_devices WHERE user_id = $1 AND is_not_expired = true', [user_id]);
    const apnTokens = apnTokensResult.rows.map((row) => row.reg_id);

    // Get FCM registration tokens
    const fcmTokensResult = await pool.query('SELECT reg_id FROM notification_devices WHERE user_id = $1 AND is_not_expired = true AND device = $2', [user_id, 'Android']);
    const fcmTokens = fcmTokensResult.rows.map((row) => row.reg_id);

    if (apnTokens.length === 0 && fcmTokens.length === 0) {
      return res.status(404).json({ message: 'Registration IDs not found' });
    }

    const messages = [];
    const invalidTokens = [];

    const generateNotificationMessage = (event, processedData) => {
      let title = '';
      let message = '';
      switch (event) {
        // Cases for different events
        case 'assign_member':
          title = 'NEW MEMBER ';
          message = `You have been assigned a new member ${processedData.memberName} by ${processedData.userName}.`;
          break;

        case 'update_member_details':
          title = 'MEMBER DETAILS ';
          message = `Your member ${processedData.memberName} details have been updated by the ${processedData.userName}.`;
          break;

        case 'switch_member':
          title = 'MEMBER SWITCHED';
          message = `Your member has been switched to a new care buddy ${processedData.carebuddy_id}.`;
          break;

        // notification for tasks
        case 'created_task':
          title = 'NEW TASK ';
          message = `You have a new task ${processedData.taskName} assigned by ${processedData.userName}.`;
          break;

        case 'updated_task':
          title = 'TASK UPDATED';
          message = `Your task ${processedData.taskName} is updated by ${processedData.userName}.`;
          break;

        // notification for interactions
        case 'created_interaction':
          title = 'NEW INTERACTION';
          message = `You have a new interaction ${processedData.interactionTitle} assigned by ${processedData.userName}.`;
          break;

        case 'updated_interaction':
          title = 'INTERACTION UPDATED';
          message = `Your interaction ${processedData.interactionTitle} is updated by ${processedData.userName}.`;
          break;

        default:
          title = 'DEFAULT TITLE';
          message = 'Default notification message.';
      }
      return { title, message };
    };

    // Generate APN notifications
    apnTokens.forEach((token) => {
      const { title, message } = generateNotificationMessage(event_type, processedData);
      const apnNote = new apn.Notification({
        alert: {
          title: title,
          body: message,
          sound: 'default',
        },
        topic: 'com.example.prayojanaNew', // Replace with your app's bundle identifier
        payload: {
          ref_id: '' + ref_id,
          type: event_type,
        },
      });
      messages.push({ apnNote, token, platform: 'apn' });
    });

    // Generate FCM notifications
    fcmTokens.forEach((token) => {
      const { title, message } = generateNotificationMessage(event_type, processedData);
      const fcmMessage = {
        notification: {
          title: title,
          body: message,
        },
        data: {
          ref_id: '' + ref_id,
          type: event_type,
        },
        token: token,
      };
      messages.push({ fcmMessage, token, platform: 'fcm' });
    });

    // Send APN and FCM notifications concurrently
    const responses = await Promise.allSettled(
      messages.map(async (message) => {
        if (message.platform === 'apn') {
          return apnProvider.send(message.apnNote, message.token);
        } else {
          return admin.messaging().send(message.fcmMessage);
        }
      })
    );

    // Handle responses
    responses.forEach((response, index) => {
      if (response.status === 'rejected') {
        invalidTokens.push(messages[index].token);
      }
    });

    // Update expired tokens in the database
    if (invalidTokens.length > 0) {
      await pool.query('UPDATE notification_devices SET is_not_expired = false WHERE reg_id = ANY($1)', [invalidTokens]);
    }

    res.status(200).json({ message: 'Notifications sent successfully', reg_id: invalidTokens });
  } catch (error) {
    console.error('Error occurred:', error);
    res.status(500).json({ message: 'Internal Server Error' });
  }
};

module.exports = sendCombinedNotifications;

This code combines both APN and FCM functionality into a single route handler called sendCombinedNotifications. It retrieves registration tokens for both APN and FCM separately and then generates and sends notifications concurrently. The responses are handled collectively, and expired tokens are updated in the database. The final response includes a message and the list of invalid tokens.

2
Subscribe to my newsletter

Read articles from M.RAGHURAM directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

M.RAGHURAM
M.RAGHURAM