Search code examples
react-nativepush-notificationreact-navigationreact-native-onesignal

RN OneSignal _open Event


OneSignal on notification open event fires after the home screen got launched then it navigates to the desired screen. I want to detect if the app was launched on pressing the notification prior the home screen get rendered so I can navigate to the Second screen directly and avoid unnecessarily calling of apis.

  • "react-native-onesignal": "^3.9.3"
  • "react-navigation": "^4.0.0"

code

   const _opened = openResult => {
      const { additionalData, body } = openResult.notification.payload;
     // how to navigate or set the initial screen depending on the payload
   }

    useEffect(() => {

        onesignal.init();
        onesignal.addEventListener('received', _received);
        onesignal.addEventListener('opened', _opened);
        SplashScreen.hide();

      return () => {
        // unsubscriber
        onesignal.removeEventListener('received', _received);
        onesignal.removeEventListener('opened', _opened);
      }
   }, []);

Debug

enter image description here


Solution

  • your question is how to navigate or set the initial screen depending on the opened notification payload?

    1) - set the initial screen depending on the opened notification payload.

    according to class Lifecycle useEffect runs after the component output has been rendered, so listener in useEffect not listen until the component amounting, and this the reason of logs in home screen shown before logs in useEffect, see this explanation.

    //this the problem (NavigationContainer called before useEffect).
    function App() {
      useEffect(() => {}); //called second.
      return <NavigationContainer>; //called first.
    }
    
    //this the solution (useEffect called Before NavigationContainer).
    function App() {
      const [ready, setReady] = useState(false);
    
      //called second.
      useEffect(() => { 
        //listen here
        setReady(true);
        SplashScreen.hide();
      });
    
      //called first
      //no function or apis run before useEffect here it just view.
      if(!ready) return <></>;// or <LoadingView/>
    
       //called third.
      return <NavigationContainer>;
    }
    
    your code may be like this.
    function App() {
        const [ready, setReady] = useState(false);
    
        const openedNotificationRef = useRef(null);
        
        const _opened = openResult => {
            openedNotificationRef.current = openResult.notification.payload;
        }
    
        const getInitialRouteName = () => {
            if (openedNotificationRef.current) {
                return "second"; //or what you want depending on the notification.
            }
            return "home";
        }
    
    
        useEffect(() => {
            onesignal.addEventListener('opened', _opened);
            //setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
            //this ensure _opened is executed if app is opened from notification
            setTimeout(() => {
                setReady(true);
            }, 0)
        });
    
    
        if(!ready) return <LoadingView/>
    
    
        return (
            <NavigationContainer initialRouteName={getInitialRouteName()}>
            </NavigationContainer>
        );
    
    }
    

    first you need to kown that

    A navigator needs to be rendered to be able to handle actions If you try to navigate without rendering a navigator or before the navigator finishes mounting, it will throw and crash your app if not handled. So you'll need to add an additional check to decide what to do until your app mounts.

    read docs

    
    function App() {
    
        const navigationRef = React.useRef(null);
        
        const openedNotificationRef = useRef(null);
        
        const _opened = openResult => {
            openedNotificationRef.current = openResult.notification.payload;
            //remove loading screen and start with what you want.
            const routes = [
                {name : 'home'}, //recommended add this to handle navigation go back
                {name : 'orders'}, //recommended add this to handle navigation go back
                {name : 'order', params : {id : payload.id}},
            ]
            navigationRef.current.dispatch(
                CommonActions.reset({
                    routes : routes,
                    index: routes.length - 1,
                })
            )
        }
    
        useEffect(() => {
            //don't subscribe to `opened` here
            
            //unsubscribe
            return () => {
                onesignal.removeEventListener('opened', _opened);
            }
        }, []);
    
        //subscribe to `opened` after navigation is ready to can use navigate
        const onReady = () => {
            onesignal.addEventListener('opened', _opened);
            //setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
            //this ensure _opened is executed if app is opened from notification
            setTimeout(() => {
                if (!openedNotificationRef.current) {
                    //remove loading screen and start with home 
                    navigationRef.current.dispatch(
                        CommonActions.reset({
                            routes : [{name : 'home'}],
                            index: 0,
                        })
                    )
                }
            }, 0)
        };
    
    
        return (
            <NavigationContainer
                ref={navigationRef}
                onReady={onReady}
                initialRouteName={"justLoadingScreen"}>
            </NavigationContainer>
        );
    
    }
    
    

    refrences for setTimeout, CommonActions.