Mobile Service Extensions

Using the iOS and Android Notification Service Extension in your mobile apps.

Android Notification Service Extension

Add the below Notification Extension Code if you want to do one of the following:

  • Receive data in the background with or without displaying a notification.
  • Override specific notification settings depending on client side app logic such as custom accent color, vibration pattern, or other any other NotificationCompat options available. See Android's documentation on the NotificationCompat options.

1. Create a class for the Service Extension

Create a class that extends INotificationServiceExtension and implement the onNotificationReceived method.

The method onNotificationReceived parameter is event of type INotificationReceivedEvent .

package your.package.name

import com.onesignal.notifications.IActionButton;
import com.onesignal.notifications.IDisplayableMutableNotification;
import com.onesignal.notifications.INotificationReceivedEvent;
import com.onesignal.notifications.INotificationServiceExtension;

@Keep // Keep is required to prevent minification from renaming or removing your class
public class NotificationServiceExtension implements INotificationServiceExtension {

   @Override
   public void onNotificationReceived(INotificationReceivedEvent event) {
      IDisplayableMutableNotification notification = event.getNotification();

      if (notification.getActionButtons() != null) {
         for (IActionButton button : notification.getActionButtons()) {
            // you can modify your action buttons here
         }
      }

     // this is an example of how to modify the notification by changing the background color to blue
      notification.setExtender(builder -> builder.setColor(0xFF0000FF));
     
     //If you need to perform an async action or stop the payload from being shown automatically, 
     //use event.preventDefault(). Using event.notification.display() will show this message again.
   }
}

2. Add the following to your AndroidManifest.xml.

Add OneSignal class name and your class value as meta-data within the AndroidManifest.xml file under the application tag. Ignore any "unused" warnings.

<application ...> 
  <!-- name doesn't change, value = your class fully name spaced-->
  <meta-data android:name="com.onesignal.NotificationServiceExtension"
     android:value="com.onesignal.example.NotificationServiceExtension" />
</application>

Getting a payload from a notification

Major Release SDKs (4.x.x) see the OSNotification class


iOS Notification Service Extension

The UNNotificationServiceExtension allows you to modify the content of push notifications before they are displayed to the user and is required for other important features like:

You likely set this up already if you followed our Mobile SDK setup instructions for your app, but this section will explain how to access the OneSignal notification payload data and troubleshoot any issues you might be having.

Getting a payload from an iOS notification

When following the Mobile SDK setup you will get to the section on adding the code to the OneSignalNotificationServiceExtension.

In that code, there is the method OneSignalExtension.didReceiveNotificationExtensionRequest. This is where we pass the bestAttemptContent of the notification to the app before it is displayed to the user. Before this method is called, you can get the notification payload and (if desired) update it before it is displayed to the user.

In this example, we send a notification with the following data:

//Replace with your own app data:
//YOUR_API_KEY, YOUR_APP_ID, SUBSCRIPTION_ID_1

curl --request POST \
     --url 'https://api.onesignal.com/notifications' \
     --header 'Authorization: Key YOUR_API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "app_id": "YOUR_APP_ID",
  "target_channel": "push",
  "headings": {"en": "The message title"},
  "contents": {"en": "The message contents"},
  "data":{"additional_data_key_1":"value_1","additional_data_key_2":"value_2"},
  "include_subscription_ids": [
    "SUBSCRIPTION_ID_1"
  ]
}
'

We can access this additional data within the OneSignalNotificationServiceExtension within OneSignal's custom object using the a parameter.

// Check if `bestAttemptContent` exists
if let bestAttemptContent = bestAttemptContent {

    // Try to retrieve the "custom" data from the notification payload
    if let customData = bestAttemptContent.userInfo["custom"] as? [String: Any],
       let additionalData = customData["a"] as? [String: Any] {

        // Convert the `additionalData` dictionary to a JSON string for logging
        if let jsonData = try? JSONSerialization.data(withJSONObject: additionalData, options: .prettyPrinted),
           let jsonString = String(data: jsonData, encoding: .utf8) {
            // Successfully converted to JSON; log the formatted JSON string
            print("The additionalData dictionary in JSON format:\n\(jsonString)")
        } else {
            // Failed to convert the `additionalData` dictionary to JSON
            print("Failed to convert additionalData to JSON format.")
        }
    }

    // Try to retrieve the "aps" data from the notification payload
    if let messageData = bestAttemptContent.userInfo["aps"] as? [String: Any],
       let apsData = messageData["alert"] as? [String: Any],
       let body = apsData["body"] as? String,  // Extract the "body" of the alert
       let title = apsData["title"] as? String {  // Extract the "title" of the alert
        // Successfully retrieved the body and title; log the values
        print("The message contents is: \(body), message headings is: \(title)")
    } else {
        // Failed to retrieve the "aps" data or its contents
        print("Unable to retrieve apsData")
    }

    // Pass the notification to OneSignal for further processing
    OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest,
                                                              with: bestAttemptContent,
                                                              withContentHandler: self.contentHandler)
}

if (bestAttemptContent) {
    // Retrieve and log customData["a"]
    NSDictionary *customData = bestAttemptContent.userInfo[@"custom"];
    if ([customData isKindOfClass:[NSDictionary class]]) {
        NSDictionary *additionalData = customData[@"a"];
        if ([additionalData isKindOfClass:[NSDictionary class]]) {
            NSError *error;
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:additionalData options:NSJSONWritingPrettyPrinted error:&error];
            if (jsonData) {
                NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
                NSLog(@"The additionalData dictionary in JSON format:\n%@", jsonString);
            } else {
                NSLog(@"Failed to convert additionalData to JSON format: %@", error.localizedDescription);
            }
        }
    }
    
    // Retrieve and log apsData
    NSDictionary *messageData = bestAttemptContent.userInfo[@"aps"];
    if ([messageData isKindOfClass:[NSDictionary class]]) {
        NSDictionary *apsData = messageData[@"alert"];
        if ([apsData isKindOfClass:[NSDictionary class]]) {
            NSString *body = apsData[@"body"];
            NSString *title = apsData[@"title"];
            if ([body isKindOfClass:[NSString class]] && [title isKindOfClass:[NSString class]]) {
                NSLog(@"The message content is: %@, message heading is: %@", body, title);
            }
        } else {
            NSLog(@"Unable to retrieve apsData");
        }
    }

    // Call OneSignalExtension method
    [OneSignalExtension didReceiveNotificationExtensionRequest:self.receivedRequest
                                             withNotification:bestAttemptContent
                                              withContentHandler:self.contentHandler];
}

Example console output:

The additionalData dictionary in JSON format:
{
  "additional_data_key_1" : "value_1",
  "additional_data_key_2" : "value_2"
}
The message contents is: The message contents, message headings is: The message title

Troubleshooting the iOS Notification Service Extension

This guide is for debugging issues with Images, Action Buttons, or Confirmed Deliveries not showing on iOS mobile apps.

Check your Xcode Settings

In General > Targets Make sure your main app target and OneSignalNotificationServiceExtension** make sure you have the correct "Supported Destinations" and your "Minimum Deployment" is set to iOS 14.5 or higher.

  • If you are using Cocoapods make sure these match with your main target in the Podfile to avoid build errors.

Continuing in the OneSignalNotificationServiceExtension > Info tab, expand the NSExtension key. Ensure you see:

Swift

<dict>
	<key>NSExtensionPointIdentifier</key>
	<string>com.apple.usernotifications.service</string>
	<key>NSExtensionPrincipalClass</key>
	<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>

Objective-C

Same as above, except instead of $(PRODUCT_MODULE_NAME).NotificationService Objective-C should be NotificationService.

Turn off "Copy only when installing"

Select your main app target > Build Phases > Embed App Extensions. Ensure "Copy only when installing" is NOT checked. Uncheck it if it is:

How to debug the Notification Service Extension not running

1. Update the OneSignalNotificationServiceExtension code

Open the NotificationService.m or NotificationService.swift and replace the whole file contents with the below code. (The code is the same as our original setup code, just adding some additional logging.

Make sure to replace YOUR_BUNDLE_ID with your actual Bundle ID.

import UserNotifications
import OneSignalExtension
import os.log

class NotificationService: UNNotificationServiceExtension {
    
    var contentHandler: ((UNNotificationContent) -> Void)?
    var receivedRequest: UNNotificationRequest!
    var bestAttemptContent: UNMutableNotificationContent?
    
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.receivedRequest = request
        self.contentHandler = contentHandler
        self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        let userInfo = request.content.userInfo
        let custom = userInfo["custom"]
        print("Running NotificationServiceExtension: userInfo = \(userInfo.description)")
        print("Running NotificationServiceExtension: custom = \(custom.debugDescription)")
        //debug log types need to be enabled in Console > Action > Include Debug Messages
        os_log("%{public}@", log: OSLog(subsystem: "com.onesignal.OneSignal-Swift-Sample", category: "OneSignalNotificationServiceExtension"), type: OSLogType.debug, userInfo.debugDescription)
        
        if let bestAttemptContent = bestAttemptContent {
            /* DEBUGGING: Uncomment the 2 lines below to check this extension is executing
                          Note, this extension only runs when mutable-content is set
                          Setting an attachment or action buttons automatically adds this */
            print("Running NotificationServiceExtension")
            bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
            
            OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
            contentHandler(bestAttemptContent)
        }
    }
}

#import <OneSignalExtension/OneSignalExtension.h>

#import "NotificationService.h"

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNNotificationRequest *receivedRequest;
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.receivedRequest = request;
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    //If your SDK version is < 3.5.0 uncomment and use this code:
    /*
    [OneSignal didReceiveNotificationExtensionRequest:self.receivedRequest
                       withMutableNotificationContent:self.bestAttemptContent];
    self.contentHandler(self.bestAttemptContent);
    */
  
    NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.YOUR_BUNDLE_ID.onesignal"];
    NSLog(@"NSE player_id: %@", [userDefault  stringForKey:@"GT_PLAYER_ID"]);
    NSLog(@"NSE app_id: %@", [userDefault  stringForKey:@"GT_APP_ID"]);
    
    /* DEBUGGING: Uncomment the 2 lines below and comment out the one above to ensure this extension is excuting
                  Note, this extension only runs when mutable-content is set
                  Setting an attachment or action buttons automatically adds this */
    NSLog(@"Running NotificationServiceExtension");
    self.bestAttemptContent.body = [@"[Modified] " stringByAppendingString:self.bestAttemptContent.body];
    
    [OneSignal.Debug setLogLevel:ONE_S_LL_VERBOSE];
   
    [OneSignal didReceiveNotificationExtensionRequest:self.receivedRequest
                       withMutableNotificationContent:self.bestAttemptContent
                                   withContentHandler:self.contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    
    [OneSignal serviceExtensionTimeWillExpireRequest:self.receivedRequest withMutableNotificationContent:self.bestAttemptContent];
    
    self.contentHandler(self.bestAttemptContent);
}

@end

2. Change your Active Scheme

Set your Active Scheme to the OneSignalNotificationServiceExtension.

3. Build and run the project

Build and run the project in Xcode on a real device.

4. Open the Console

In Xcode, select Window > Devices and Simulators.

You should see your device connected. Select Open Console.

5. Check the Console

In the Console:

  • Select Action > Include Debug Messages
  • Search for OneSignalNotificationServiceExtension as the CATEGORY
  • Select Start

Send this device a notification with a message (use contents property if sending from the Create message API). In this example, the payload is:

//Replace with your own app data:
//YOUR_API_KEY, YOUR_APP_ID, SUBSCRIPTION_ID_1

curl --request POST \
     --url 'https://api.onesignal.com/notifications' \
     --header 'Authorization: Key YOUR_API_KEY' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "app_id": "YOUR_APP_ID",
  "target_channel": "push",
  "headings": {"en": "The message title"},
  "contents": {"en": "The message contents"},
  "data":{"additional_data_key_1":"value_1","additional_data_key_2":"value_2"},
  "include_subscription_ids": [
    "SUBSCRIPTION_ID_1"
  ]
}
'

You should see a message logged with the app running and not running.

If you do not see a message, then remove OneSignal from your app and follow our Mobile SDK Setup again to verify you setup OneSignal correctly.