[expo-notifications] `threadIdentifier` is parsed from local notification input but never applied to `UNMutableNotificationContent` (iOS)
Fabien Bavent
## Summary
NotificationContentRecord
(the native Swift record backing scheduleNotificationAsync
's content
) declares threadIdentifier
as a @Field
, so it's correctly parsed out of the JS-supplied object and even read back out when serializing a received
notification. But toUNMutableNotificationContent()
— the method that builds the actual outgoing UNMutableNotificationContent
for a scheduled
local notification — never assigns it. Every other field on that record (title
, body
, categoryIdentifier
, sound
, attachments
, interruptionLevel
, ...) is copied over; threadIdentifier
is the one silent exception.Net effect: passing
threadIdentifier
when scheduling a local notification compiles, is accepted by the native module, and does... nothing. iOS falls back to its own default "Automatic" notification grouping instead of the app-authored one that was requested._Posted here, it's kind of a bug, but not an error. But I can't, neither I will, create an full application to trigger the issue_
## Where
packages/expo-notifications/ios/ExpoNotifications/Notifications/NotificationRecords.swift
```swift
@Field public
var categoryIdentifier: String?
@Field
var threadIdentifier: String? // <- declared, parsed from input, read back on output...
...
func toUNMutableNotificationContent() -> UNMutableNotificationContent {
let content = UNMutableNotificationContent()
...
if let categoryIdentifier = categoryIdentifier {
content.categoryIdentifier = categoryIdentifier
}
// ...threadIdentifier is never assigned to
content
anywhere in this function.if let sound = sound {
...
## Fix
```swift
if let threadIdentifier = threadIdentifier {
content.threadIdentifier = threadIdentifier
}
placed alongside the
categoryIdentifier
assignment. One-line native fix. The public NotificationContentInput
TS type should also expose threadIdentifier
as an input field — right now it's only documented on NotificationContentIos
, the read/output-only type (For the input type, see NotificationContentInput
), so even a caller who wanted to use this had no typed way to do it.```ts
/**
* iOS notification-thread identifier, used to group related notifications together in
* Notification Center / on the Lock Screen.
* @platform ios
*/
threadIdentifier?: string;
## Reference implementation
I already have a working `patch-package` fix for both changes above, build but not tested against `expo-notifications@57.0.7` (don't have iOS dev license yet). Happy to open this as a PR if useful — let me know.
## How I found this
I was investigating explicit notification grouping on Android (see Support explicit Android notification grouping (`setGroup`/`setGroupSummary`) for local `scheduleNotificationAsync`) and, before assuming iOS was fine by default, went looking for the iOS equivalent of Android's `setGroup`/`setGroupSummary`. `threadIdentifier` is the iOS mechanism for that, and tracing it through the native module is how I found it's a no-op today.
To be clear, this is **not** the same failure mode as the Android issue: iOS doesn't need an app-authored group summary to keep each notification's actions reachable (grouped notifications stay individually addressable via long-press regardless of `threadIdentifier`), so nothing is *broken* by this bug today for apps that don't rely on `threadIdentifier`. It's a silently-dead input, not a crash or a regression — but anyone trying to use it will see it fail with no error or explanation.
## Environment
expo: ~57.0.8
expo-notifications: ~57.0.7
react-native: 0.86.0
Platform: iOS
```
## Related issues
- #28580 — grouping for pushnotifications (android.group/threadIdin the FCM/APNs payload). Different code path (remote push, not localscheduleNotificationAsync); the reporter there also triedthreadIdon iOS without success, which is consistent with what I found, but that issue is scoped to push and doesn't diagnose the local-notification root cause.
Searched the repo for existing issues about
threadIdentifier
being dropped/ignored before filing — didn't find one covering this specific gap.