Integration while using Expo tools - infobip/mobile-messaging-react-native-plugin GitHub Wiki

Integration while using Expo tools (iOS)

Prerequisites

  • Expo SDK 54 (with React Native 0.81.x)
  • iOS deployment target 15.1 or higher
  • Xcode 16+
  • CocoaPods
  • Infobip Mobile Messaging React Native plugin version 14.6.0

Step 1: Install the plugin

Add the Infobip Mobile Messaging plugin to the dependencies section of your package.json:

{
  "dependencies": {
    "infobip-mobile-messaging-react-native-plugin": "14.6.0"
  }
}

Then install dependencies:

npm install

Step 2: Add google-services.json in the root of your project

Go to the Firebase Console, and download the google-services.json file for your app and place it in the root of your project.

Step 3: Initialize mobile messaging plugin in your app

Please follow the guidelines in the Quick start guide. You will need to get the Application Code from the Infobip Portal.

Note: Starting from Android 13 you will manually have to request push notification permission from the user. For more details please check the Android 13 notification permission handling guide.

Step 4: Configure app.json

Add the following iOS and Android configuration to your app.json. This sets up the background mode for remote notifications, the Apple App Group ID for communication with the Notification Service Extension, and the required entitlements for push notifications.

{
  "expo": {
    ...,
    "newArchEnabled": true,
    "ios": {
      "bundleIdentifier": "your.bundle.identifier",
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification"],
        "com.mobilemessaging.app_group": "group.your.app.group.id"
      },
      "entitlements": {
        "aps-environment": "development",
        "com.apple.security.application-groups": ["group.your.app.group.id"]
      }
    },
    "android": {
      "package": "your.package.name", // the package name must match the one inside google-services.json file
      "googleServicesFile": "./google-services.json"
    }
  }
}

Replace group.your.app.group.id with your actual App Group ID (registered at Apple Developer Portal).

For production builds, change aps-environment to production.

If you want to use frameworks for iOS project, you will need to install Expo BuildProperties plugin. After you have installed the plugin following the official Expo documentation, add the following configuration to your app.json:

{
  "expo": {
    ...,
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": {
            "useFrameworks": "static" // valid values are "static" or "dynamic" depending on if you want to use dynamic or static linkage
          }
        }
      ]
    ]
  }
}

Step 5: Generate native projects

Since the Infobip plugin requires native code modifications, you need a bare workflow (prebuild). Run the following command to generate the native iOS and Android projects:

npx expo prebuild

Step 6: iOS Integration

6.1 Install pods

Position yourself in the iOS directory and run pod install to download and link the MobileMessaging SDK before modifying native code:

cd ios && pod install

6.2 Only if you are using iOS dynamic frameworks

Modify Infobip Mobile Messaging React Native plugin podspec to support dynamic frameworks. Open node_modules/infobip-mobile-messaging-react-native-plugin/infobip-mobile-messaging-react-native-plugin.podspec and replace the content with the following:

require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))
mmVersion = "14.4.3"

Pod::Spec.new do |s|
  s.name         = "infobip-mobile-messaging-react-native-plugin"
  s.version      = package["version"]
  s.summary      = package["description"]
  s.description  = <<-DESC
                  infobip-mobile-messaging-react-native-plugin
                   DESC
  s.homepage     = "https://github.com/infobip/mobile-messaging-react-native-plugin"
  s.license      = "MIT"
  s.authors      = { "Infobip" => "[email protected]" }
  s.platforms    = { :ios => "15.0" }
  s.source       = { :git => 'https://github.com/infobip/mobile-messaging-react-native-plugin.git', :tag => s.version}
  s.swift_version = '5.5'
  s.source_files = "ios/**/*.{h,m,swift}"
  s.requires_arc = true

  s.dependency "React-Core"
  s.dependency "React-Core-prebuilt"

  s.dependency "MobileMessaging/Core", mmVersion
  s.dependency "MobileMessaging/InAppChat", mmVersion
  s.dependency "MobileMessaging/Inbox", mmVersion
  if defined?($WebRTCUIEnabled)
    s.dependency "MobileMessaging/WebRTCUI", mmVersion
  end
end

Run pod update inside iOS directory to apply the changes.

6.3 Modify AppDelegate

...
import MobileMessaging
...

  // Push: forward device token to MobileMessaging
  public override func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
  ) {
    MobileMessaging.didRegisterForRemoteNotificationsWithDeviceToken(deviceToken)
    super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
  }
  
    // Push: forward remote notifications to MobileMessaging
  public override func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
  ) {
    MobileMessaging.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler)
    super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
  }
  ...

6.4 Build and run the app

To ensure that push can be received please check your Capabilities in Xcode and make sure that "Push Notifications" and "Background Modes > Remote notifications" are enabled for your app target. You need to enable it for both Debug and Release configurations. Also check that App Group is set up for Debug and Release configurations.

Main app target capabilities

Please manage signing of the app and run it through Xcode or using Expo CLI:

npx expo run:ios

On the Infobip Portal you can verify that integration was successful by checking that new person profile with push registration has been created on Infobip Portal. To learn more about installations and person profiles, check the Users and installations section of the documentation. You can now also try sending a test push notification from the Infobip Portal using Broadcast or Flow to verify that everything is working correctly. Another way to test the integration is to use the Push HTTP API to send a push notification directly to the device.

Note: If you will make a clean prebuild of the app using Expo CLI (e.g., npx expo prebuild --clean), you will need to re-apply all the native code changes (AppDelegate modifications, Notification Service Extension setup) as the prebuild process will regenerate the native projects from scratch. Infobip is in the process of making an Expo plugin to automate these steps, but for now, you will need to manually re-apply the changes after a clean prebuild.

6.5 Integrate Notification Service Extension to support rich notifications and delivery reporting

The Notification Service Extension enables delivery reporting and rich content notifications on iOS. Notification service Extension is crucial for receiving push notifications while the app is in the background or killed, as it allows the system to wake up the extension to process the incoming notification and report delivery to Infobip.

6.5.1 Check that Info.plist includes the App Group ID

Check that the main app target's Info.plist includes the App Group ID for communication with the Notification Service Extension:

<key>com.mobilemessaging.app_group</key>
<string>group.your.app.group.id</string>

6.5.2 Create the Notification Service Extension target

In Xcode, create a new target:

NSE Target Creation Step 1

6.5.3 Provide a name for the Extension and appropriate settings:

Make sure to set the Embed in Application option to your main app target (e.g., YourApp) to ensure the extension is properly embedded in the app bundle. This is crucial for the extension to be recognized and run by the system. When asked to activate the scheme, click "Don't Activate".

NSE Target Creation Step 2

6.5.4 Set the minimum iOS deployment target to be the same as the main app target (e.g., iOS 15.1) to ensure compatibility.

6.5.5 Set up signing for the extension target

6.5.6 Add App Group capability to the extension target and use the same App Group ID as the main app target (e.g., group.your.app.group.id):

Make sure you set it up for both Debug and Release configurations. NSE App Group Setup

6.5.7 Modify NotificationService.swift to forward notifications to Infobip MobileMessaging SDK:

Note after this step you will get errors because the Infobip MobileMessaging SDK is not yet linked to the extension target. We will link it in the next steps.

//
//  NotificationService.swift
//  MobileMessagingReactNative
//
//  Copyright (c) 2016-2026 Infobip Limited
//  Licensed under the Apache License, Version 2.0
//

import UserNotifications
import MobileMessaging

class NotificationService: UNNotificationServiceExtension {

  var contentHandler: ((UNNotificationContent) -> Void)?
  var originalContent: UNNotificationContent?

  override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    self.originalContent = request.content

    // Check if notification is from Infobip
    if MM_MTMessage.isCorrectPayload(request.content.userInfo) {
        MobileMessagingNotificationServiceExtension.didReceive(request, withContentHandler: contentHandler)
    } else {
        // Pass through non-Infobip notifications
        contentHandler(request.content)
    }
  }

  override func serviceExtensionTimeWillExpire() {
    MobileMessagingNotificationServiceExtension.serviceExtensionTimeWillExpire()
    if let originalContent = originalContent {
      contentHandler?(originalContent)
    }
  }
}

Option A: Dynamic Frameworks

Use this if you are using dynamic linkage in your Podfile.

Select the Extension target in Xcode. Go to build settings and modify Runpath Search Paths to have exactly these 3 entries for both Debug and Release configurations:

$(inherited)
@executable_path/Frameworks
@executable_path/../../Frameworks
NSE Dynamic Frameworks Step 1

Select the Extension target in Xcode. Go to build settings and modify Framework Search Paths for both Debug and Release configurations to be:

$(BUILT_PRODUCTS_DIR)/MobileMessaging
NSE Dynamic Frameworks Step 2

Option B: Static Frameworks

Use this if you are using static linkage in your Podfile.

Select the Extension target in Xcode. Go to build settings and modify Framework Search Paths for both Debug and Release configurations to be:

$(BUILT_PRODUCTS_DIR)/MobileMessaging
NSE Dynamic Frameworks Step 2

Select the Extension target in Xcode. Go to build settings and modify Other Linker Flags for both Debug and Release configurations to be:

-framework
MobileMessaging
NSE Linker Flags

Option C: Static Libraries

Use this if you are not using any type of frameworks.

Select the Extension target in Xcode. Go to General tab and add libMobileMessaging.a from the list of libraries to the Frameworks and Libraries section:

NSE Static Libraries Step 1

Select the Extension target in Xcode. Go to build settings and modify Header Search Paths for both Debug and Release configurations to be:

$(SRCROOT)/Pods/Headers/Public/MobileMessaging
NSE Static Libraries Step 2

Select the Extension target in Xcode. Go to build settings and modify Library Search Paths for both Debug and Release configurations to be:

$(SDKROOT)/usr/lib/swift"$(inherited)"
$(BUILT_PRODUCTS_DIR)/MobileMessaging
NSE Static Libraries Step 3

Select the Extension target in Xcode. Go to build settings and modify Module Import Paths for both Debug and Release configurations to be:

$(BUILT_PRODUCTS_DIR)/MobileMessaging
NSE Static Libraries Step 4

Select the Extension target in Xcode. Go to build settings and modify Other Swift Flags for both Debug and Release configurations to be:

-Xcc
-fmodule-map-file=$(BUILT_PRODUCTS_DIR)/MobileMessaging/MobileMessaging.modulemap
NSE Static Libraries Step 5

6.5.8 Troubleshooting Notification Service Extension

Cycle Inside... building could produce unreliable results.

If you encounter a "Cycle Inside..." error during build, try the following steps:

  1. Select the Main App target in Xcode. Go to Build Phases and drag the step Embed Foundation Extensions to be right after [CP] Check Pods Manifest.lock: NSE Troubleshooting
  2. Optional, if step 1 doesn't resolve the issue: Select the Main App target in Xcode. Go to Build Phases and select Embed Foundation Extensions. Turn on/off the 'Copy only when installing' option and try building again.

Image not displaying when push arrives.

In Xcode in Folder view go to Pods > MobileMessaging > Core > RichNotificationsExtensions.swift (full path is: ios/Pods/MobileMessaging/Classes/MobileMessaging/RichNotifications/RichNotificationsExtensions.swift) and replace the file content with the following:

import Foundation
  import UserNotifications

  extension MobileMessaging {
      public func withAppGroupId(_ appGroupId: String) -> MobileMessaging {
          NSLog("[MM-NSE] withAppGroupId called with: %@", appGroupId)
          self.appGroupId = appGroupId
          self.sharedNotificationExtensionStorage = DefaultSharedDataStorage(applicationCode: applicationCode, appGroupId: appGroupId)
          return self
      }
  }

  extension MM_MTMessage {

      @discardableResult func downloadImageAttachment(appGroupId: String? = nil, completion: @escaping (URL?, Error?) -> Void) -> RetryableDownloadTask? {
          guard let contentURL = contentUrl?.safeUrl else {
              NSLog("[MM-NSE] downloadImageAttachment: no contentUrl, skipping download")
              logDebug("could not init content url to download")
              completion(nil, nil)
              return nil
          }

          NSLog("[MM-NSE] downloadImageAttachment: downloading from %@", contentURL.absoluteString)

          let destinationResolver: (URL, URLResponse) -> URL = { _, _ -> URL in
              let ret = URL.attachmentDownloadDestinatioUrl(sourceUrl: contentURL, appGroupId: nil)
              NSLog("[MM-NSE] downloadImageAttachment: destination URL = %@", ret.absoluteString)
              return ret
          }

          let task = RetryableDownloadTask(attemptsCount: 3, contentUrl: contentURL, destinationResolver: destinationResolver, completion: { url, error in
              if let error = error {
                  NSLog("[MM-NSE] downloadImageAttachment: FAILED with error: %@", error.localizedDescription)
              } else if let url = url {
                  NSLog("[MM-NSE] downloadImageAttachment: SUCCESS, file at %@", url.absoluteString)
              } else {
                  NSLog("[MM-NSE] downloadImageAttachment: completed with no URL and no error")
              }
              completion(url, error)
          })
          logDebug("downloading rich content for message...")
          task.resume()

          return task
      }

  }

  @objcMembers
  final public class MobileMessagingNotificationServiceExtension: NSObject, NamedLogger {

      @available(*, deprecated, message: "The function is deprecated. You can safely delete the invocation from your code.")
      public class func startWithApplicationCode(_ code: String, appGroupId: String) { }

      @available(*, deprecated, message: "The function is deprecated. You can safely delete the invocation from your code.")
      public class func startWithApplicationCode(_ applicationCode: String) { }

      public class func didReceive(_ request: UNNotificationRequest,
                                   withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
          NSLog("[MM-NSE] didReceive(request) called, delegating to didReceive(content)")
          logDebug("did receive request \(request)")
          didReceive(content: request.content, withContentHandler: contentHandler)
      }

      public class func didReceive(content: UNNotificationContent,
                                   withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
          NSLog("[MM-NSE] didReceive(content) called")
          NSLog("[MM-NSE] sharedInstance is %@", sharedInstance == nil ? "nil" : "exists")

          if (sharedInstance == nil) {
              NSLog("[MM-NSE] initializing sharedInstance...")

              let applicationCode = MobileMessaging.keychain.applicationCode
              NSLog("[MM-NSE] applicationCode from keychain: %@", applicationCode ?? "nil ❌")

              guard let applicationCode = applicationCode else {
                  NSLog("[MM-NSE] ❌ FATAL: ApplicationCode not found in keychain. Extension CANNOT proceed.")
                  NSLog("[MM-NSE] ❌ contentHandler will NOT be called — notification will timeout after 30s")
                  return
              }

              let appGroupId = Bundle.mainAppBundle.appGroupId
              NSLog("[MM-NSE] appGroupId from mainAppBundle info.plist: %@", appGroupId ?? "nil ❌")

              guard let appGroupId = appGroupId else {
                  NSLog("[MM-NSE] ❌ FATAL: AppGroupId not defined in info.plist (key: com.mobilemessaging.app_group)")
                  NSLog("[MM-NSE] ❌ contentHandler will NOT be called — notification will timeout after 30s")
                  return
              }

              if sharedInstance == nil {
                  NSLog("[MM-NSE] creating new sharedInstance with appCode=%@ appGroupId=%@", applicationCode, appGroupId)
                  sharedInstance = MobileMessagingNotificationServiceExtension(appCode: applicationCode, appGroupId: appGroupId)
              }

              let storage = DefaultSharedDataStorage(applicationCode: applicationCode, appGroupId: appGroupId)
              NSLog("[MM-NSE] sharedNotificationExtensionStorage initialized: %@", storage != nil ? "YES" : "NO ❌")
              sharedInstance?.sharedNotificationExtensionStorage = storage
          }

          var result = content

          guard let sharedInstance = sharedInstance else {
              NSLog("[MM-NSE] ❌ sharedInstance is nil after init attempt")
              contentHandler(result)
              return
          }

          guard let mtMessage = MM_MTMessage(payload: result.userInfo,
                                      deliveryMethod: .push,
                                      seenDate: nil,
                                      deliveryReportDate: nil,
                                      seenStatus: .NotSeen,
                                      isDeliveryReportSent: false) else {
              NSLog("[MM-NSE] ❌ could not create MM_MTMessage from payload")
              logDebug("could not recognize message")
              contentHandler(result)
              return
          }

          NSLog("[MM-NSE] ✅ MM_MTMessage created, messageId: %@", mtMessage.messageId)
          NSLog("[MM-NSE] contentUrl: %@", mtMessage.contentUrl ?? "nil")

          let handlingGroup = DispatchGroup()

          NSLog("[MM-NSE] starting delivery report...")
          handlingGroup.enter()
          sharedInstance.reportDelivery(mtMessage) { error in
              if let error = error {
                  NSLog("[MM-NSE] delivery report FAILED: %@", error.localizedDescription)
              } else {
                  NSLog("[MM-NSE] ✅ delivery report succeeded")
              }
              mtMessage.isDeliveryReportSent = error == nil
              mtMessage.deliveryReportedDate = mtMessage.isDeliveryReportSent ? MobileMessaging.date.now : nil
              NSLog("[MM-NSE] saving message to shared storage...")
              logDebug("saving message to shared storage \(sharedInstance.sharedNotificationExtensionStorage.orNil)")
              sharedInstance.sharedNotificationExtensionStorage?.save(message: mtMessage)
              NSLog("[MM-NSE] message saved to shared storage")
              handlingGroup.leave()
          }

          NSLog("[MM-NSE] starting content retrieval (rich media download)...")
          handlingGroup.enter()
          sharedInstance.retrieveNotificationContent(for: mtMessage, originalContent: result) { updatedContent in
              NSLog("[MM-NSE] content retrieval completed")
              NSLog("[MM-NSE] attachments count: %d", updatedContent.attachments.count)
              result = updatedContent
              handlingGroup.leave()
          }

          handlingGroup.notify(queue: DispatchQueue.main) {
              NSLog("[MM-NSE] ✅ all async work done, calling contentHandler")
              NSLog("[MM-NSE] final title: %@", result.title)
              NSLog("[MM-NSE] final body: %@", result.body)
              logDebug("message handling finished")
              contentHandler(result)
          }
      }

      public class func serviceExtensionTimeWillExpire() {
          NSLog("[MM-NSE] ⚠️ serviceExtensionTimeWillExpire called!")
          sharedInstance?.currentTask?.cancel()
      }

      //MARK: Internal
      let sessionManager: DynamicBaseUrlHTTPSessionManager
      static var sharedInstance: MobileMessagingNotificationServiceExtension?
      init(appCode: String, appGroupId: String) {
          self.applicationCode = appCode
          self.appGroupId = appGroupId
          self.sessionManager = DynamicBaseUrlHTTPSessionManager(baseURL: URL(string: Consts.APIValues.prodDynamicBaseURLString)!, sessionConfiguration: MobileMessaging.urlSessionConfiguration, appGroupId:
  appGroupId)
          NSLog("[MM-NSE] sessionManager created with baseURL: %@", Consts.APIValues.prodDynamicBaseURLString)
      }

      private func retrieveNotificationContent(for message: MM_MTMessage, originalContent: UNNotificationContent, completion: @escaping (UNNotificationContent) -> Void) {
          NSLog("[MM-NSE] retrieveNotificationContent called")

          currentTask = message.downloadImageAttachment(appGroupId: appGroupId) { (downloadedFileUrl, error) in
              guard let downloadedFileUrl = downloadedFileUrl,
                  let mContent = (originalContent.mutableCopy() as? UNMutableNotificationContent),
                  let attachment = try? UNNotificationAttachment(identifier: downloadedFileUrl.absoluteString.sha256(), url: downloadedFileUrl, options: nil)
                  else
              {
                  if let error = error {
                      NSLog("[MM-NSE] retrieveNotificationContent: attachment creation failed with error: %@", error.localizedDescription)
                  } else {
                      NSLog("[MM-NSE] retrieveNotificationContent: no downloadedFileUrl or could not create attachment")
                  }
                  self.logDebug("rich content downloading completed, could not init content attachment")
                  completion(originalContent)
                  return
              }

              mContent.attachments = [attachment]
              NSLog("[MM-NSE] ✅ rich content attachment created successfully")
              self.logDebug("rich content downloading completed succesfully")
              completion((mContent.copy() as? UNNotificationContent) ?? originalContent)
          }
      }

      private func reportDelivery(_ message: MM_MTMessage, completion: @escaping (NSError?) -> Void) {
          NSLog("[MM-NSE] reportDelivery called for messageId: %@", message.messageId)
          deliveryReporter.report(applicationCode: applicationCode, messageIds: [message.messageId], completion: completion)
      }

      let appGroupId: String
      let applicationCode: String
      var currentTask: RetryableDownloadTask?
      var sharedNotificationExtensionStorage: AppGroupMessageStorage?
      lazy var deliveryReporter: DeliveryReporting! = DeliveryReporter()
  }

  protocol DeliveryReporting {
      func report(applicationCode: String, messageIds: [String], completion: @escaping (NSError?) -> Void)
  }

  class DeliveryReporter: DeliveryReporting, NamedLogger {
      func report(applicationCode: String, messageIds: [String], completion: @escaping (NSError?) -> Void) {
          NSLog("[MM-NSE] DeliveryReporter: reporting for messageIds: %@", messageIds.joined(separator: ", "))
          logDebug("reporting delivery for message ids \(messageIds)")
          guard let extensionInstance = MobileMessagingNotificationServiceExtension.sharedInstance, !messageIds.isEmpty else
          {
              NSLog("[MM-NSE] DeliveryReporter: ❌ no sharedInstance or empty messageIds")
              logDebug("[Notification Extension] could not report delivery")
              completion(nil)
              return
          }

          let pushRegId = MobileMessaging.keychain.pushRegId
          NSLog("[MM-NSE] DeliveryReporter: pushRegId = %@", pushRegId ?? "nil")
          logDebug("[Notification Extension] using pushRegId: \(pushRegId ?? "nil") for delivery report")

          let request = DeliveryReportRequest(applicationCode: applicationCode, pushRegistrationId: pushRegId, body: [Consts.DeliveryReport.dlrMessageIds: messageIds])

          extensionInstance.sessionManager.getDataResponse(request, queue: DispatchQueue.global(), completion: { (response, error) in
              if let error = error {
                  NSLog("[MM-NSE] DeliveryReporter: ❌ request failed: %@", error.localizedDescription)
              } else {
                  NSLog("[MM-NSE] DeliveryReporter: ✅ request succeeded")
              }
              completion(error)
          })
      }

  }

  protocol AppGroupMessageStorage {
      init?(applicationCode: String, appGroupId: String)
      func save(message: MM_MTMessage)
      func retrieveMessages() -> [MM_MTMessage]
      func cleanupMessages()
  }

  class DefaultSharedDataStorage: AppGroupMessageStorage {
      let applicationCode: String
      let appGroupId: String
      let storage: UserDefaults
      required init?(applicationCode: String, appGroupId: String) {
          guard let ud = UserDefaults.init(suiteName: appGroupId) else {
              NSLog("[MM-NSE] DefaultSharedDataStorage: ❌ could not init UserDefaults for suite: %@", appGroupId)
              return nil
          }
          NSLog("[MM-NSE] DefaultSharedDataStorage: ✅ UserDefaults initialized for suite: %@", appGroupId)
          self.appGroupId = appGroupId
          self.applicationCode = applicationCode
          self.storage = ud
      }

      func save(message: MM_MTMessage) {
          NSLog("[MM-NSE] DefaultSharedDataStorage: saving message %@", message.messageId)
          var savedMessageDicts = retrieveSavedPayloadDictionaries()
          var msgDict: MMStringKeyPayload = ["p": message.originalPayload, "dlr": message.isDeliveryReportSent]
          msgDict["dlrd"] = message.deliveryReportedDate
          savedMessageDicts.append(msgDict)
          if let data = try? NSKeyedArchiver.archivedData(withRootObject: savedMessageDicts, requiringSecureCoding: true) {
              storage.set(data, forKey: applicationCode)
              storage.synchronize()
              NSLog("[MM-NSE] DefaultSharedDataStorage: ✅ message saved, total messages: %d", savedMessageDicts.count)
          } else {
              NSLog("[MM-NSE] DefaultSharedDataStorage: ❌ failed to archive message data")
          }
      }

      func retrieveMessages() -> [MM_MTMessage] {
          let messageDataDicts = retrieveSavedPayloadDictionaries()
          let messages = messageDataDicts.compactMap({ messageDataTuple -> MM_MTMessage? in
              guard let payload = messageDataTuple["p"] as? MMStringKeyPayload, let dlrSent =  messageDataTuple["dlr"] as? Bool else
              {
                  return nil
              }
              let newMessage = MM_MTMessage(payload: payload,
                                         deliveryMethod: .local,
                                         seenDate: nil,
                                         deliveryReportDate: messageDataTuple["dlrd"] as? Date,
                                         seenStatus: .NotSeen,
                                         isDeliveryReportSent: dlrSent)
              return newMessage
          })
          NSLog("[MM-NSE] DefaultSharedDataStorage: retrieved %d messages", messages.count)
          return messages
      }

      func cleanupMessages() {
          storage.removeObject(forKey: applicationCode)
          NSLog("[MM-NSE] DefaultSharedDataStorage: messages cleaned up")
      }

      private func retrieveSavedPayloadDictionaries() -> [MMStringKeyPayload] {
          var payloadDictionaries = [MMStringKeyPayload]();
          if let data = storage.object(forKey: applicationCode) as? Data,
             let dictionaries = unarchiveMessageDictionaries(fromeData: data) {
              payloadDictionaries = dictionaries
          } else if let dictionaries = storage.object(forKey: applicationCode) as? [MMStringKeyPayload] {
              payloadDictionaries = dictionaries
          }
          return payloadDictionaries
      }

      private func unarchiveMessageDictionaries(fromeData data: Data) -> [MMStringKeyPayload]? {
          do {
              return try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, NSDictionary.self, NSNull.self, NSString.self, NSNumber.self, NSDate.self], from: data) as? [MMStringKeyPayload]
          } catch {
              NSLog("[MM-NSE] DefaultSharedDataStorage: ❌ unarchive failed: %@", error.localizedDescription)
              MMLogError("[AppGroupMessageStorage] couldn't unarchive message objects with error: \(error)")
          }
          return nil
      }
  }

Now uninstall the app on the iPhone, run it again. Open Console.app on Mac, stream logs from the device and filter logs with [MM-NSE] keyword to see the logs from the Notification Service Extension. Send a push notification with image to the new installation and look carefully for logs. If you notice any failure logs, please check if you have correctly set up the App Group and if the App Group ID is consistent across the main app target, the extension target and that it is present in Info.plist file of the main app target. If it is try removing the Extension target and follow the steps once more. After successful troubleshooting, you can revert this file back to the original version from the SDK.

Step 7: Android Integration

Android integration is simple in comparison to iOS. After you have added the google-services.json file in the root of your project and configured app.json as described in the previous steps, double check that all steps regarding Android setup are performed automatically by Expo:

7.1 Check that google-services.json file has been copied to android/app directory by Expo CLI.

7.2 Check that 'com.google.gms:google-services' has been added to android/build.gradle file:

buildscript {
    dependencies {
        // ...
        // GMS Gradle plugin
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

7.3 Check that apply plugin: 'com.google.gms.google-services' has been added at the end of your android/app/build.gradle.

7.4 Run the app using Expo CLI or Android Studio and try sending a test push notification.

7.5 Android Troubleshooting

It is possible you encounter AndroidManifest merging issues. To resolve them, figure out which elements are conflicting and use tools:replace to resolve the conflicts. First, make sure the tools namespace is declared in your <manifest> tag:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

For example, if both your app and the SDK define the android:allowBackup attribute, you can use tools:replace on the <application> tag in your AndroidManifest.xml:

<application
    android:name=".MainApplication"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:allowBackup="false"
    android:theme="@style/AppTheme"
    android:supportsRtl="true"
    tools:replace="android:allowBackup">

Next steps

Once you have successfully tested push notifications on both platforms, we recommend exploring the full capabilities of the Infobip Mobile Messaging SDK:

  • Browse the Wiki documentation to learn about features such as in-app chat, inbox, user personalization and more.
  • Visit the official Infobip Mobile Push documentation for detailed guides on sending push notifications, managing audiences, and configuring campaigns through the Infobip Portal.
⚠️ **GitHub.com Fallback** ⚠️