Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/scripts/test-expo-ios-link.sh
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,19 @@ if [[ "$nine322_failed" -ne 0 ]]; then
fi

log "PASS: no RNFB* framework privately duplicates a shared GoogleUtilities class (#9322)"

# test-expo enables the messaging plugin's `ios.notificationServiceExtension`: the extension must be embedded
# in the app and link FirebaseMessaging from Swift Package Manager.
log "--- notification service extension ---"
EXTENSION_BINARY="${BUILT_PRODUCTS_DIR}/testexpo.app/PlugIns/NotificationServiceExtension.appex/NotificationServiceExtension"
if [[ ! -f "$EXTENSION_BINARY" ]]; then
log "ERROR: NotificationServiceExtension.appex is not embedded in testexpo.app (expected ${EXTENSION_BINARY})"
exit 1
fi
# Captured rather than piped: under pipefail, `grep -q` exiting at the first match makes `nm` fail on SIGPIPE.
extension_syms="$(nm "$EXTENSION_BINARY" 2>/dev/null || true)"
if ! grep -qE '_OBJC_CLASS_\$_FIRMessaging$' <<<"$extension_syms"; then
log "ERROR: NotificationServiceExtension does not link FirebaseMessaging (no FIRMessaging class reference)"
exit 1
fi
log "PASS: NotificationServiceExtension.appex is embedded and links FirebaseMessaging"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good build-time check. The CI log shows Firebase's resource bundles being copied into the extension, so FirebaseMessaging is linked statically and the extension is self-contained. Nothing exercises the extension with a notification, though. Could you confirm once, by hand, that an image actually shows? Either a push to a device, or xcrun simctl push on a simulator with "mutable-content": 1 in aps and an image URL (fcm_options.image in the FCM payload). No need to automate it here.

@simondaigre simondaigre Oct 29, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, confirmed by hand on a real app: with the extension added by the plugin, an FCM notification with an image (fcm_options.image) shows the image. It takes a built app receiving a real push: xcrun simctl push doesn't trigger notification service extensions.

31 changes: 31 additions & 0 deletions docs/messaging/ios-notification-images.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ This is a quick guide to display an image in an incoming notification. Android h
**🚨 Before you start**
Be sure you already have Cloud Messaging installed and set up. In case you don't [get started here](/messaging/usage).

## Expo

Expo projects can let the `@react-native-firebase/messaging` config plugin add the extension instead of following the steps below:

```json
{
"expo": {
"plugins": [
[
"@react-native-firebase/messaging",
{
"ios": {
"notificationServiceExtension": true
}
}
]
]
}
}
```

On `npx expo prebuild`, the plugin adds a `NotificationServiceExtension` target embedded in the app, with its source and `Info.plist` in `ios/NotificationServiceExtension/`. The extension links `FirebaseMessaging` through Swift Package Manager at the exact Firebase Apple SDK version of `@react-native-firebase/app`, the same package reference `pod install` links `FirebaseCore` into the app with (see [iOS SPM Support](/ios-spm)). Its bundle identifier is `<ios.bundleIdentifier>.NotificationServiceExtension`, and the plugin declares it to EAS Build (`extra.eas.build.experimental.ios.appExtensions`) so that EAS provisions and signs it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a sentence for apps that already get a notification service extension from another plugin (OneSignal's, for example)? Enabling this option as well gives them two extensions, so the docs should tell people not to combine them, or what to expect if they do.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 5a16d8e: iOS hands an app's notifications to a single notification service extension, so the docs now say not to enable this option when another plugin (OneSignal's, for example) already adds one, and to use one or the other.


To name the target differently, pass `{ "targetName": "ImageNotification" }` instead of `true`. The name is also the last component of the bundle identifier, so it may only contain letters, digits and hyphens.

iOS hands an app's notifications to a single notification service extension. If another config plugin already adds one (OneSignal's, for example), don't enable this option as well: only one of the two would handle your notifications. Use that extension, or this one, alone.

After turning the option off, run `npx expo prebuild --clean`: like other config plugins' changes, prebuild without `--clean` only adds to the native project, and leaves the target in it.

## React Native CLI

**🏁 Ready to start**
The following steps will guide you through how to add a new target to your application to support payloads with an image. Open Xcode and let's get started.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = HelloWorld/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = HelloWorld/Info.plist; sourceTree = "<group>"; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = HelloWorld/AppDelegate.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* HelloWorld */ = {
isa = PBXGroup;
children = (
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
);
name = HelloWorld;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* HelloWorld */,
83CBBA001A601CBA00E9B192 /* Products */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* HelloWorld.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* HelloWorld */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "HelloWorld" */;
buildPhases = (
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = HelloWorld;
productName = HelloWorld;
productReference = 13B07F961A680F5B00A75B9A /* HelloWorld.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "HelloWorld" */;
compatibilityVersion = "Xcode 12.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* HelloWorld */,
);
};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = HelloWorld/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
PRODUCT_BUNDLE_IDENTIFIER = com.example.helloworld;
PRODUCT_NAME = HelloWorld;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = HelloWorld/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
PRODUCT_BUNDLE_IDENTIFIER = com.example.helloworld;
PRODUCT_NAME = HelloWorld;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
SDKROOT = iphoneos;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
SDKROOT = iphoneos;
};
name = Release;
};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "HelloWorld" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "HelloWorld" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}
Loading
Loading