Skip to content

[Feedback]: Communication Notification Implementation With OneSignal #1991

Description

@YasirNaeem25

What's on your mind?

iOS Communication Notifications

A practical guide for adding WhatsApp-style iOS notifications to a React Native app that already uses OneSignal.

When a chat or group message arrives, iOS can show the sender or group avatar instead of only the app icon. That UI is an Apple Communication Notification. OneSignal still delivers the push. The Notification Service Extension converts the payload into an incoming INSendMessageIntent.

Backend
  → OneSignal
  → APNs
  → Notification Service Extension
  → Read custom data
  → Create incoming INSendMessageIntent
  → Download avatar
  → notificationContent.updating(from: intent)
  → Communication Notification UI

This is not a normal notification attachment. Attaching an image to UNNotificationContent.attachments does not create Communication Notification UI. You must use updating(from:).

Test on a real iPhone. Simulator is not reliable for remote push or Communication Notification presentation.


1. Requirements

  • iOS 15+
  • An existing iOS Notification Service Extension
  • Push Notifications capability on the main app
  • Communication Notifications capability on the main app
  • mutable-content: 1 in the APNs payload (OneSignal sets this when an NSE exists)
  • Avatar URLs in the push custom data

You do not need:

  • A second Notification Service Extension
  • A new bundle ID
  • An App Group just for this feature
  • Replacing OneSignal with native APNs

An App Group is only needed if the app and extension must share storage. Communication Notifications themselves do not require one.

Apple allows one Notification Service Extension per app. If two NSEs are embedded, iOS may not run the one you modified.


2. Inspect the existing project first

Do not assume payload keys, target names, or entitlements. Find the real ones.

Check:

  1. Main app target and bundle ID
  2. Notification Service Extension target and bundle ID
  3. NotificationService.swift
  4. Existing OneSignal NSE integration
  5. Existing INSendMessageIntent usage (Siri / Share Sheet)
  6. Main app Info.plistNSUserActivityTypes
  7. Main app entitlements
  8. NSE entitlements
  9. App Groups, if any
  10. OneSignal additionalData shape used by the JS/native click handler

If Siri Suggestions or Share Sheet already donate INSendMessageIntent, keep that outgoing flow. Incoming notifications use the same intent type with interaction.direction = .incoming.


3. Xcode configuration

Main app target

Enable:

  • Push Notifications
  • Communication Notifications
  • Siri, only if the app already uses Siri / Share Sheet suggestions

Confirm the main app entitlements contain:

<key>com.apple.developer.usernotifications.communication</key>
<true/>

Confirm Info.plist includes:

<key>NSUserActivityTypes</key>
<array>
  <string>INSendMessageIntent</string>
</array>

Do not duplicate INSendMessageIntent if it is already there.

Notification Service Extension

Use the existing NSE. Typical bundle ID:

com.yourapp.bundleid.OneSignalNotificationServiceExtension

Link Intents.framework to the NSE target.

Do not add Communication Notifications capability again on the NSE unless Apple’s signing setup specifically requires it. The containing app entitlement is the one that matters.

Keep mutable-content enabled. OneSignal does this automatically when a Notification Service Extension is present.


4. Payload

Read OneSignal custom data from the NSE userInfo dictionary. OneSignal stores additional data here:

userInfo["custom"]["a"]

Also handle fallbacks:

  • userInfo["os_data"]["custom"]["a"]
  • userInfo["additionalData"]
  • top-level keys, if additional_data_is_root_payload is enabled

Do not invent keys. Use the keys your backend already sends.

Recommended additional data

1-to-1

{
  "type": "single_chat",
  "id": "chat_123",
  "sender": {
    "id": "user_456",
    "name": "Nicole Fuentes",
    "profile": "https://cdn.example.com/nicole.jpg"
  }
}

Group

{
  "type": "group_chat",
  "group": {
    "id": "group_123",
    "title": "Family",
    "image": "https://cdn.example.com/family.jpg"
  },
  "sender": {
    "id": "user_456",
    "name": "Nicole Fuentes",
    "profile": "https://cdn.example.com/nicole.jpg"
  }
}

The APNs alert title and body can still be used as fallbacks for name and message text.

Fields that matter

Purpose Typical keys
Chat vs group vs other type
1:1 conversation id id, chat_id
Group conversation id group.id, group_id
Group name group.title, group.name
Group avatar group.image
Sender id sender.id, sender_id
Sender name sender.name
Sender avatar sender.profile, sender.image

If group.image is missing, iOS can still show Communication Notification layout, but it cannot show that group’s own avatar.

Apply Communication Notifications only to chat types. Leave invitations, missed calls, marketing, and other pushes unchanged.


5. Notification Service Extension flow

Keep OneSignal processing first. Then apply Communication Notification handling. Never replace OneSignal.

Pattern:

OneSignalExtension.didReceiveNotificationExtensionRequest(
    receivedRequest,
    with: bestAttemptContent,
    withContentHandler: { onesignalContent in
        CommunicationNotificationProcessor.process(content: onesignalContent) { processed in
            contentHandler(processed)
        }
    }
)

Call contentHandler only once. If avatar download is still running when the extension is about to expire, deliver the best content you already have.

override func serviceExtensionTimeWillExpire() {
    OneSignalExtension.serviceExtensionTimeWillExpireRequest(receivedRequest, with: bestAttemptContent)
    contentHandler(bestAttemptContent)
}

Never drop the notification because an image failed.


6. Create the incoming intent

Shared pieces

  1. Parse custom data.
  2. Decide 1:1 vs group vs ignore.
  3. Download avatars with a short timeout.
  4. Create INPerson for the sender.
  5. Create INSendMessageIntent.
  6. Set INInteraction.direction = .incoming.
  7. Update content with try content.updating(from: intent).

1-to-1

  • sender = the other person
  • speakableGroupName = nil
  • recipients = nil
  • conversationIdentifier = chat id
  • avatar comes from the sender INPerson image
let sender = INPerson(
    personHandle: INPersonHandle(value: senderId, type: .unknown),
    nameComponents: nil,
    displayName: senderName,
    image: senderImage,
    contactIdentifier: nil,
    customIdentifier: senderId,
    isMe: false,
    suggestionType: .none
)

let intent = INSendMessageIntent(
    recipients: nil,
    outgoingMessageType: .outgoingMessageText,
    content: message,
    speakableGroupName: nil,
    conversationIdentifier: conversationId,
    serviceName: "YourApp",
    sender: sender,
    attachments: nil
)

Group

  • speakableGroupName = group name
  • conversationIdentifier = group id
  • sender = the person who wrote the message
  • group avatar is set on speakableGroupName, not as a normal notification attachment

iOS is more reliable about showing the group avatar when the intent has at least two recipients.

intent.setImage(groupImage, forParameterNamed: \.speakableGroupName)

If the group image download fails, fall back to the sender image, then to no image. Still show the notification.

Incoming direction

let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)

let updated = try content.updating(from: intent)
contentHandler(updated)

updating(from:) is what turns the banner into a Communication Notification.

If the app already donates outgoing INSendMessageIntent for Siri / Share Sheet, reuse the same conversationIdentifier values. Do not rewrite the outgoing donation path unless it is broken.


7. Avatar download

Download inside the NSE with URLSession. Do not add a third-party image library unless the extension already has one that is safe to use there.

Rules:

  • Use a short timeout (about 8–10 seconds). NSE execution time is limited.
  • Accept only http / https URLs.
  • Handle invalid URLs, HTTP failures, and decode failures.
  • Resize the image (around 256px) before creating INImage.
  • Cache in the extension’s own Caches directory. No App Group required.
  • If download fails, continue without an avatar.
let image = INImage(imageData: jpegData)

8. What not to do

  • Do not create another Notification Service Extension.
  • Do not create another bundle ID.
  • Do not replace OneSignal.
  • Do not treat a notification attachment as a Communication Notification.
  • Do not apply this UI to every push type.
  • Do not block forever waiting for an image.
  • Do not silently drop a notification on timeout.
  • Do not duplicate Communication Notifications capability if it is already enabled.
  • Do not create an App Group unless shared storage is actually required.
  • Do not break existing Siri Suggestions / Share Sheet intent donations.

9. Testing

Use a physical iPhone.

  1. Build and run the main app on device so the NSE is installed with it.
  2. Put the app in the background or lock the phone.
  3. Send a 1:1 chat push with sender.profile.
  4. Confirm the banner shows the sender name and sender avatar.
  5. Send a group chat push with group.image and sender.profile.
  6. Confirm the banner shows the group name and group avatar.
  7. Send a non-chat push and confirm it still looks like a normal notification.
  8. Confirm Siri / Share Sheet chat suggestions still work, if the app has them.

If the NSE is not running, Communication Notification UI will never appear. Confirm mutable-content is set and that only one NSE is embedded.


10. Troubleshooting

Symptom Likely cause
Still seeing only the app icon updating(from:) was not used, or NSE did not run
NSE never runs Missing mutable-content, or a second NSE is embedded
Group name shows, but app icon remains group.image missing, download failed, or setImage was not applied to speakableGroupName
Chat notifications work, other pushes broke Communication handling was applied to every notification type
Notification never appears contentHandler was not called on timeout or error
Share Sheet / Siri suggestions broke Outgoing INSendMessageIntent donation was changed
Works in code, fails in Simulator Expected. Test on a real device

11. This repository

This project implements the feature in the existing OneSignal NSE:

File Role
ios/OneSignalNotificationServiceExtension/NotificationService.swift OneSignal first, then Communication Notification processing, timeout-safe delivery
ios/OneSignalNotificationServiceExtension/CommunicationNotificationProcessor.swift Payload parsing, INPerson, incoming INSendMessageIntent, updating(from:)
ios/OneSignalNotificationServiceExtension/AvatarImageDownloader.swift Timed avatar download, decode, resize, local cache

Existing config reused:

  • Main app bundle ID: com.networkmessenger.network
  • NSE bundle ID: com.networkmessenger.network.OneSignalNotificationServiceExtension
  • Communication Notifications entitlement already on the main app
  • INSendMessageIntent already in NSUserActivityTypes
  • Outgoing Siri / Share Sheet donations left unchanged

Conversation IDs match existing donations:

  • 1:1 → chat id
  • Group → group.id
  • Event chat → event.id

Chat types handled:

  • single_chat
  • single_chat_reaction
  • group_chat
  • group_chat_reaction
  • event_chat
  • event_chat_reaction

12. Apple references

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions