Repository navigation
feat(messaging, ios): add a notification service extension from the Expo config plugin - #9401
simondaigre wants to merge 3 commits into
Conversation
…xpo config plugin `ios.notificationServiceExtension` adds a Notification Service Extension target that displays the image of FCM notifications. The extension links FirebaseMessaging through Swift Package Manager at the exact Firebase Apple SDK version of @react-native-firebase/app, reusing the firebase-ios-sdk package reference that pod install links FirebaseCore into the app with, since the CocoaPods recipe of the docs no longer works with Firebase Apple SDK 13+. The extension is declared to EAS Build so that it is provisioned and signed. test-expo enables it, and its iOS link check asserts the extension is embedded and links FirebaseMessaging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@CLAassistant doesnt work |
317fb7f to
d3926e4
Compare
|
Hey @simondaigre, thanks for this, and for the tests, docs and Before a full review, two things need to be sorted out:
Some early questions from a first read, so you can look at them while CI runs:
I'll follow up with a full review once CI has run. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9401 +/- ##
============================================
- Coverage 75.11% 71.80% -3.30%
+ Complexity 2299 2298 -1
============================================
Files 620 445 -175
Lines 41232 25691 -15541
Branches 5242 4393 -849
============================================
- Hits 30966 18445 -12521
+ Misses 8689 5950 -2739
+ Partials 1577 1296 -281
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
…n off; drop the appleTeamId cast Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review and for approving the runs! CLA: I tried the recheck link again, but the CLA assistant page loads without anything on it: no agreement to sign and no button, as in the screenshot from my earlier comment. Is there another way I can sign it? Turning the option off: that's intended, as prebuild without Versions: yes. The plugin declares the extension in Cast: right, removed. |
| } | ||
|
|
||
| // An `app_extension` target is embedded in the first target, the app target of an Expo project. | ||
| const target = project.addTarget(targetName, 'app_extension', targetName, bundleIdentifier); |
There was a problem hiding this comment.
addTarget(…, 'app_extension', …) (from the xcode npm package @expo/config-plugins uses) adds the .appex to the first phase named "Copy Files" anywhere in the project, not the one it just created: it looks the phase up by the new target's uuid, which has no phases yet. Reproduced with that package directly: if the app already has a Copy Files phase with another destination (e.g. Frameworks), the .appex goes there instead of PlugIns/. It needs another plugin's phase to hit, so not a blocker, but could the plugin make sure the .appex ends up in a dstSubfolderSpec = 13 phase on the app target it already finds?
There was a problem hiding this comment.
Good catch, thanks. Fixed in 5a16d8e: after addTarget, the plugin moves the .appex into the app target's dstSubfolderSpec = 13 phase (adding one if the app has none) and removes it from every other Copy Files phase. It runs on each prebuild, so a project generated before the fix gets corrected too.
| log "ERROR: NotificationServiceExtension does not link FirebaseMessaging (no FIRMessaging class reference)" | ||
| exit 1 | ||
| fi | ||
| log "PASS: NotificationServiceExtension.appex is embedded and links FirebaseMessaging" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| } | ||
| ``` | ||
|
|
||
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| extension: NotificationServiceExtension, | ||
| ): ExpoConfig { | ||
| const eas = config.extra?.eas ?? {}; | ||
| const appExtensions: { targetName: string }[] = eas.build?.experimental?.ios?.appExtensions ?? []; |
There was a problem hiding this comment.
Nit: no change needed: Expo still labels extra.eas.build.experimental.ios.appExtensions as experimental, so if it gets renamed we'll need to follow. I don't see a better hook today.
| }); | ||
|
|
||
| describe('addNotificationServiceExtension', () => { | ||
| it('adds an app extension target embedded in the app target', () => { |
There was a problem hiding this comment.
To go with the addTarget comment: could you add a case where the app target already has a Copy Files phase with another destination (e.g. project.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Copy Files', appUuid, 'frameworks') on the existing fixture before calling addNotificationServiceExtension), and assert that the .appex is in a dstSubfolderSpec = 13 phase on the app target and in no other phase?
There was a problem hiding this comment.
Added in 5a16d8e: the fixture gets a Copy Files phase to Frameworks on the app target first, and the test asserts the .appex is in exactly one phase, a dstSubfolderSpec = 13 one on the app target, and that the app has only one such phase. It failed before the fix (the product landed in the Frameworks phase, dstSubfolderSpec = 10). The second-prebuild test also checks the app embeds the product once.
…app's PlugIns phase `addTarget` puts the product in the project's first "Copy Files" phase, whatever its target and destination. Also document not combining it with another plugin's notification service extension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Description
Displaying the image of an FCM notification on iOS takes a Notification Service Extension that calls
Messaging.serviceExtension().populateNotificationContent. The docs explain how to add one in Xcode, but an Expo project regeneratesios/on each prebuild, so it needs another config plugin or a hand-written one. The CocoaPods recipe of the docs also no longer applies with Firebase Apple SDK 13+, which is SPM-only.This adds an
ios.notificationServiceExtensionoption to the messaging config plugin:On prebuild, the plugin:
ios/NotificationServiceExtension/: the Swift service callingpopulateNotificationContent, and anInfo.plistwith the app's version and build number;<ios.bundleIdentifier>.NotificationServiceExtensionand the app's deployment target and device family;FirebaseMessagingto the extension through Swift Package Manager at the exact Firebase Apple SDK version of@react-native-firebase/app(sdkVersions.ios.firebase), reusing thefirebase-ios-sdkpackage reference thatpod installlinksFirebaseCoreinto the app with: the app and the extension share one package, at one version. The CocoaPods hooks leave the extension alone;extra.eas.build.experimental.ios.appExtensions), so that EAS provisions and signs it.{ "targetName": "…" }instead oftruenames the target differently (also its folder and the last component of its bundle identifier, e.g. to keep the identifier of an extension added before). A new prebuild updates the existing target rather than adding another one.The Expo section of
ios-notification-images.mdxdocuments it.test-expoenables it, andtest-expo-ios-link.shnow checks that the built app embedsNotificationServiceExtension.appexand that the extension linksFirebaseMessaging, so the iOS CI builds it.Related issues
Picks up the idea of #8191 (closed as stale), which added the target through CocoaPods, without tests or docs.
Release Summary
feat(messaging, ios): the Expo config plugin can add a Notification Service Extension that displays the image of FCM notifications (
ios.notificationServiceExtension).Checklist
AndroidiOSOther(macOS, web)e2etests added or updated inpackages/\*\*/e2ejesttests added or updated inpackages/\*\*/__tests__Test Plan
notificationServiceExtension.test.ts(18 new tests): the files written, the target and its build settings, the embedding in the app, the SPM package reference reused (or added when missing) and theFirebaseMessagingproduct,targetName, the EAS declaration, and a second prebuild updating the target instead of duplicating it. The messaging plugin's 46 tests pass, and so doesyarn lint:js.yarn test-expo:ios:linkon macOS: the prebuild generates the target, the build succeeds, and the existing linkage checks plus the new one pass ("NotificationServiceExtension.appex is embedded and links FirebaseMessaging", confirmed withnmon the extension's binary). The app launches on an iOS 27 simulator.