wix / react-native-notifications

React Native Notifications
MIT License
3.25k stars 765 forks source link

Foreground notification event is fired twice on iOS 18 #1051

Closed jcloquell closed 1 month ago

jcloquell commented 2 months ago

We noticed that since the recent iOS 18 update, the foreground notification event is being fired twice.

Notifications.events().registerNotificationReceivedForeground callback gets called one time initially, when receiving the actual push notification, and then it's called again after exactly 30 seconds have passed.

This only happens on iOS devices running iOS 18, it seems to work fine on Android and older iOS versions.

EnricoMazzu commented 1 month ago

This is an ios 18 bug.

https://forums.developer.apple.com/forums/thread/762126

My suggestion (and also the feedback from Apple) : use the identifier fields to implement an app filter and skip the duplicated events.

import { Notification } from "react-native-notifications"
import { Platform } from "react-native"

export interface PushNotificationRegistry {
     shouldFilterNotification(notification: Notification): Boolean
     clear(): void
}

class IdBasedFilterPushRegistry implements PushNotificationRegistry {
    private ids: Array<string>
    private currentIndex = 0
    private capacity: number

    constructor(capacity: number = 10) {
        this.capacity = capacity
        this.ids = []
    }

    shouldFilterNotification(notification: Notification): Boolean {
        if(!notification || !notification.identifier){
            return false
        }
        const alreadyConsumed = !this.isIdentifierAlreadyPresent(notification.identifier) 
        this.storeIdAsConsumed(notification.identifier) 
        return alreadyConsumed
    }

    clear(): void {
        this.currentIndex = 0
        this.ids = []
    }

    private storeIdAsConsumed(identifier: string) {
        // use a circular array approach to prevent memory increase
        this.ids[this.currentIndex] = identifier
        this.currentIndex = (this.currentIndex + 1) % this.capacity
    }

    private isIdentifierAlreadyPresent(id: string) {
        return this.ids.find(item => item == id)
    }
}

class NoopIdFilter implements PushNotificationRegistry {

    shouldFilterNotification(notification: Notification): Boolean {
        return false; 

    }
    clear(): void {

    } 
}

export function createDefaultNotificationRegistry(enableIdFilter: Boolean = false): PushNotificationRegistry {
    return enableIdFilter ? new IdBasedFilterPushRegistry() : new NoopIdFilter()
}

Usage (on foreground events:

const shouldFilter:Boolean =  Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 18;
const registry = createDefaultNotificationRegistry(shouldFilter)

...
const notificationReceivedForegroundCallback = Notifications.events().registerNotificationReceivedForeground((notification: Notification, completion: (response: NotificationCompletion) => void) => { 

          if(registry.shouldFilterNotification(notification)){
                console.log(">>> Blocked by app filter logic")
               ....
                return;
         }
         processNotification(....)
}
jcloquell commented 1 month ago

Thanks a lot for the heads up @EnricoMazzu! We were already implementing a similar workaround on our side 👍 I'll close the issue then, as it's on Apple's hands to fix it.