diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index 35b3a059..80e6a1b8 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -378,6 +378,7 @@ public void addNotificationForegroundLifecycleListener() { public void onWillDisplay(INotificationWillDisplayEvent event) { if (!this.hasAddedNotificationForegroundListener) { event.getNotification().display(); + return; } INotification notification = event.getNotification(); @@ -392,10 +393,26 @@ public void onWillDisplay(INotificationWillDisplayEvent event) { try { synchronized (event) { - while (preventDefaultCache.containsKey(notificationId)) { - event.wait(); + // Wait while notification is still in cache (JS hasn't responded yet) + // Use timeout of 25 seconds to prevent infinite wait if JS doesn't respond + long timeout = 25000; // 25 seconds + long startTime = System.currentTimeMillis(); + while (notificationWillDisplayCache.containsKey(notificationId)) { + long elapsed = System.currentTimeMillis() - startTime; + long remaining = timeout - elapsed; + if (remaining <= 0) { + // Timeout: remove from cache and display by default + notificationWillDisplayCache.remove(notificationId); + break; + } + event.wait(remaining); } } + + // If notification wasn't prevented, display it automatically + if (!preventDefaultCache.containsKey(notificationId)) { + event.getNotification().display(); + } } catch (InterruptedException e) { Logging.error("InterruptedException: " + e.toString(), null); } @@ -407,11 +424,31 @@ public void onWillDisplay(INotificationWillDisplayEvent event) { @ReactMethod private void displayNotification(String notificationId) { INotificationWillDisplayEvent event = notificationWillDisplayCache.get(notificationId); + + // If not found in notificationWillDisplayCache, check preventDefaultCache + // This handles the case where preventDefault() was called first + if (event == null) { + event = preventDefaultCache.get(notificationId); + if (event != null) { + Logging.debug( + "displayNotification called after preventDefault for notification with id: " + notificationId + + ". Displaying notification anyway.", null); + } + } + if (event == null) { Logging.error( "Could not find onWillDisplayNotification event for notification with id: " + notificationId, null); return; } + + // Notify waiting thread and clean up caches + synchronized (event) { + preventDefaultCache.remove(notificationId); + notificationWillDisplayCache.remove(notificationId); + event.notify(); + } + event.getNotification().display(); } @@ -424,7 +461,13 @@ private void preventDefault(String notificationId) { return; } event.preventDefault(); - this.preventDefaultCache.put(notificationId, event); + + // Add to cache and notify waiting thread + synchronized (event) { + this.preventDefaultCache.put(notificationId, event); + notificationWillDisplayCache.remove(notificationId); + event.notify(); + } } @ReactMethod diff --git a/examples/RNOneSignalTS/OSDemo.tsx b/examples/RNOneSignalTS/OSDemo.tsx index 3b3813f5..90076107 100644 --- a/examples/RNOneSignalTS/OSDemo.tsx +++ b/examples/RNOneSignalTS/OSDemo.tsx @@ -1,7 +1,6 @@ import { useFocusEffect } from '@react-navigation/native'; import React, { useCallback, useEffect, useState } from 'react'; import { - Alert, ScrollView, StyleSheet, Text, @@ -9,7 +8,11 @@ import { TouchableOpacity, View, } from 'react-native'; -import { LogLevel, OneSignal } from 'react-native-onesignal'; +import { + LogLevel, + NotificationWillDisplayEvent, + OneSignal, +} from 'react-native-onesignal'; import { SafeAreaView } from 'react-native-safe-area-context'; import OSButtons from './OSButtons'; import OSConsole from './OSConsole'; @@ -36,143 +39,112 @@ const OSDemo: React.FC = () => { }); }, []); - const onForegroundWillDisplay = useCallback( - (event: unknown) => { - OSLog('OneSignal: notification will show in foreground:', event); - const notif = ( - event as { getNotification: () => { title: string } } - ).getNotification(); - - const cancelButton = { - text: 'Cancel', - onPress: () => { - (event as { preventDefault: () => void }).preventDefault(); - }, - style: 'cancel' as const, - }; + useEffect(() => { + console.log('Initializing OneSignal'); + OneSignal.initialize(APP_ID); + OneSignal.Debug.setLogLevel(LogLevel.Debug); + }, []); - const completeButton = { - text: 'Display', - onPress: () => { - (event as { getNotification: () => { display: () => void } }) - .getNotification() - .display(); - }, - }; + useFocusEffect( + useCallback(() => { + console.log('Setting up event listeners'); - Alert.alert( - 'Display notification?', - notif.title, - [cancelButton, completeButton], - { - cancelable: true, - }, - ); - }, - [OSLog], - ); + const onForegroundWillDisplay = (event: NotificationWillDisplayEvent) => { + OSLog( + 'OneSignal: notification will show in foreground:', + event.getNotification().title, + ); - const onNotificationClick = useCallback( - (event: unknown) => { - OSLog('OneSignal: notification clicked:', event); - }, - [OSLog], - ); + OSLog('Should show after 25 seconds:'); + let i = 1; + const interval = setInterval(() => { + OSLog('Seconds passed:', i); + i++; + if (i > 25) { + clearInterval(interval); + } + }, 1000); + // OSLog('Preventing display'); + // event.preventDefault(); - const onIAMClick = useCallback( - (event: unknown) => { - OSLog('OneSignal IAM clicked:', event); - }, - [OSLog], - ); + // OSLog('Displaying notification'); + // event.getNotification().display(); + }; - const onIAMWillDisplay = useCallback( - (event: unknown) => { - OSLog('OneSignal: will display IAM: ', event); - }, - [OSLog], - ); + const onNotificationClick = (event: unknown) => { + OSLog('OneSignal: notification clicked:', event); + }; - const onIAMDidDisplay = useCallback( - (event: unknown) => { - OSLog('OneSignal: did display IAM: ', event); - }, - [OSLog], - ); + const onIAMClick = (event: unknown) => { + OSLog('OneSignal IAM clicked:', event); + }; - const onIAMWillDismiss = useCallback( - (event: unknown) => { - OSLog('OneSignal: will dismiss IAM: ', event); - }, - [OSLog], - ); + const onIAMWillDisplay = (event: unknown) => { + OSLog('OneSignal: will display IAM: ', event); + }; - const onIAMDidDismiss = useCallback( - (event: unknown) => { - OSLog('OneSignal: did dismiss IAM: ', event); - }, - [OSLog], - ); + const onIAMDidDisplay = (event: unknown) => { + OSLog('OneSignal: did display IAM: ', event); + }; - const onSubscriptionChange = useCallback( - (subscription: unknown) => { - OSLog('OneSignal: subscription changed:', subscription); - }, - [OSLog], - ); + const onIAMWillDismiss = (event: unknown) => { + OSLog('OneSignal: will dismiss IAM: ', event); + }; - const onPermissionChange = useCallback( - (granted: unknown) => { - OSLog('OneSignal: permission changed:', granted); - }, - [OSLog], - ); + const onIAMDidDismiss = (event: unknown) => { + OSLog('OneSignal: did dismiss IAM: ', event); + }; - const onUserChange = useCallback( - (event: unknown) => { - OSLog('OneSignal: user changed: ', event); - }, - [OSLog], - ); + const onSubscriptionChange = (subscription: unknown) => { + OSLog('OneSignal: subscription changed:', subscription); + }; - useEffect(() => { - OneSignal.initialize(APP_ID); - OneSignal.Debug.setLogLevel(LogLevel.None); - }, []); + const onPermissionChange = (granted: unknown) => { + OSLog('OneSignal: permission changed:', granted); + }; - useFocusEffect( - useCallback(() => { - console.log('Setting up event listeners'); + const onUserChange = (event: unknown) => { + OSLog('OneSignal: user changed: ', event); + }; const setup = async () => { - OneSignal.LiveActivities.setupDefault(); + // OneSignal.login('fadi-rna-11'); + const onesignalID = await OneSignal.User.getOnesignalId(); + console.log('OneSignal ID:', onesignalID); + const externalID = await OneSignal.User.getExternalId(); + console.log('External ID:', externalID); + const pushID = await OneSignal.User.pushSubscription.getIdAsync(); + console.log('Push ID:', pushID); + OneSignal.Notifications.addEventListener( 'foregroundWillDisplay', onForegroundWillDisplay, ); - OneSignal.Notifications.addEventListener('click', onNotificationClick); - OneSignal.InAppMessages.addEventListener('click', onIAMClick); - OneSignal.InAppMessages.addEventListener( - 'willDisplay', - onIAMWillDisplay, - ); - OneSignal.InAppMessages.addEventListener('didDisplay', onIAMDidDisplay); - OneSignal.InAppMessages.addEventListener( - 'willDismiss', - onIAMWillDismiss, - ); - OneSignal.InAppMessages.addEventListener('didDismiss', onIAMDidDismiss); - OneSignal.User.pushSubscription.addEventListener( - 'change', - onSubscriptionChange, - ); - OneSignal.Notifications.addEventListener( - 'permissionChange', - onPermissionChange, - ); - OneSignal.User.addEventListener('change', onUserChange); + // OneSignal.LiveActivities.setupDefault(); + // OneSignal.Notifications.addEventListener('click', onNotificationClick); + // OneSignal.InAppMessages.addEventListener('click', onIAMClick); + // OneSignal.InAppMessages.addEventListener( + // 'willDisplay', + // onIAMWillDisplay, + // ); + // OneSignal.InAppMessages.addEventListener('didDisplay', onIAMDidDisplay); + // OneSignal.InAppMessages.addEventListener( + // 'willDismiss', + // onIAMWillDismiss, + // ); + // OneSignal.InAppMessages.addEventListener('didDismiss', onIAMDidDismiss); + // OneSignal.User.pushSubscription.addEventListener( + // 'change', + // onSubscriptionChange, + // ); + // OneSignal.Notifications.addEventListener( + // 'permissionChange', + // onPermissionChange, + // ); + // OneSignal.User.addEventListener('change', onUserChange); }; + console.log('Setup'); setup(); return () => { @@ -214,18 +186,7 @@ const OSDemo: React.FC = () => { ); OneSignal.User.removeEventListener('change', onUserChange); }; - }, [ - onForegroundWillDisplay, - onNotificationClick, - onIAMClick, - onIAMWillDisplay, - onIAMDidDisplay, - onIAMWillDismiss, - onIAMDidDismiss, - onSubscriptionChange, - onPermissionChange, - onUserChange, - ]), + }, [OSLog]), ); const inputChange = useCallback((text: string) => { diff --git a/examples/RNOneSignalTS/package.json b/examples/RNOneSignalTS/package.json index 244cef81..f1890622 100644 --- a/examples/RNOneSignalTS/package.json +++ b/examples/RNOneSignalTS/package.json @@ -9,6 +9,8 @@ "preios": "bun run setup && bun run update:pod", "android": "react-native run-android", "ios": "react-native run-ios", + "log:android": "react-native log-android", + "log:ios": "react-native log-ios", "start": "react-native start" }, "dependencies": { diff --git a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m index 93abb6fe..a2191d48 100644 --- a/ios/RCTOneSignal/RCTOneSignalEventEmitter.m +++ b/ios/RCTOneSignal/RCTOneSignalEventEmitter.m @@ -389,12 +389,45 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { if (!strongSelf) return; - strongSelf->_notificationWillDisplayCache[event.notification.notificationId] = - event; + NSString *notificationId = event.notification.notificationId; + strongSelf->_notificationWillDisplayCache[notificationId] = event; [event preventDefault]; [RCTOneSignalEventEmitter sendEventWithName:@"OneSignal-notificationWillDisplayInForeground" withBody:[event.notification jsonRepresentation]]; + + // Wait for JS to respond (preventDefault or displayNotification) + // Use timeout slightly less than OneSignal SDK's 25s timeout (similar to Android's 25s with 30s SDK timeout) + // If JS doesn't respond within the timeout, display automatically + const NSTimeInterval kNotificationDisplayTimeout = 24.0; // Slightly less than SDK's 25s timeout + + // Capture notificationId in a local variable for the block + __block NSString *blockNotificationId = [notificationId copy]; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kNotificationDisplayTimeout * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + __weak RCTOneSignalEventEmitter *weakSelf = strongSelf; + RCTOneSignalEventEmitter *strongSelf = weakSelf; + if (!strongSelf) + return; + + // Check if notification is still in cache (JS didn't call displayNotification) + OSNotificationWillDisplayEvent *cachedEvent = + strongSelf->_notificationWillDisplayCache[blockNotificationId]; + if (cachedEvent) { + // Check if it wasn't prevented + if (!strongSelf->_preventDefaultCache[blockNotificationId]) { + // Display automatically if not prevented + // This matches Android: after wait loop exits, if not prevented, display + dispatch_async(dispatch_get_main_queue(), ^{ + [cachedEvent.notification display]; + }); + [strongSelf->_notificationWillDisplayCache + removeObjectForKey:blockNotificationId]; + } + } + }); } RCT_EXPORT_METHOD(preventDefault : (NSString *)notificationId) { @@ -412,8 +445,11 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { notificationId]]; return; } - strongSelf->_preventDefaultCache[event.notification.notificationId] = event; [event preventDefault]; + + // Add to preventDefaultCache and remove from notificationWillDisplayCache + strongSelf->_preventDefaultCache[event.notification.notificationId] = event; + [strongSelf->_notificationWillDisplayCache removeObjectForKey:notificationId]; } RCT_EXPORT_METHOD(clearAllNotifications) { [OneSignal.Notifications clearAll]; } @@ -573,6 +609,21 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { RCT_EXPORT_METHOD(displayNotification : (NSString *)notificationId) { OSNotificationWillDisplayEvent *event = _notificationWillDisplayCache[notificationId]; + + // If not found in notificationWillDisplayCache, check preventDefaultCache + // This handles the case where preventDefault() was called first + if (!event) { + event = _preventDefaultCache[notificationId]; + if (event) { + [OneSignalLog + onesignalLog:ONE_S_LL_DEBUG + message:[NSString + stringWithFormat: + @"OneSignal (objc): displayNotification called after preventDefault for notification with id: %@. Displaying notification anyway.", + notificationId]]; + } + } + if (!event) { [OneSignalLog onesignalLog:ONE_S_LL_ERROR @@ -583,6 +634,7 @@ + (void)sendEventWithName:(NSString *)name withBody:(NSDictionary *)body { notificationId]]; return; } + dispatch_async(dispatch_get_main_queue(), ^{ [event.notification display]; });