I have a React Native app, where it is possible to deep link into content, from push notifications, external links etc.
When the app is already running, it works as expected, and the app goes to the screen expected from the URL, by adding that screen to the navigation stack of the relevant navigation tab.
But when the app is not running, and the deep links intiate the lauch, the screen is added as the root element of the stack of the NotificationsNavigator
.
This means that the user cannot go back to the overview of notifications, and when selecting the Notifications tab in the bottom menu, the initial screen from the deeplink is shown, not the intialRouteName
.
It should ALWAYS push the NotificationsScreen
as the first element of the stack in NotificationsNavigator
, before pushing the route that has been configured in the deeplinks configuration.
How to achieve that? I already tried lazy: false
on the TabNavigator, no change. I also tried detachInactiveScreens={false}
on my NotificationsNavigator
with no luck.
What is the trick?
The URLs I use are like
bosjapp-dev://notifications/bulletinboard/post?boardId=b97ca72f-bd5c-455b-972a-5848f3c801a2&postId=925ebb95-2b7e-48d1-985e-766e365f38b7
bosjapp-dev://notifications/news/article?articleUrl=https%3A%2F%2Fbosj.dk%2Fnyhedsoversigt%2Fvideo-beboere-fejrer-afslutning-paa-deres-renovering%2F%3Falttemplate%3Dnewsalttemplate&date=2022-10-24&title=Video
RootNavigator.tsx:
const deepLinksConf = {
screens: {
loginOIDCWait: {
path: 'login/oidc',
},
frontPageNavigator: {
screens: {
newsArticle: {
path: 'frontpage/news/article',
parse: {
// This is done because `react-navigation` for some reason does not decode URL query parameters.
articleUrl: (articleUrl: string) => decodeURIComponent(articleUrl),
},
},
bulletinBoard: 'frontpage/bulletinboard',
bulletinBoardPost: 'frontpage/bulletinboard/post',
enquiriesEnquiryDetails: 'frontpage/enquiry',
},
},
notificationsNavigator: {
screens: {
newsArticle: {
path: 'notifications/news/article',
parse: {
// This is done because `react-navigation` for some reason does not decode URL query parameters.
articleUrl: (articleUrl: string) => decodeURIComponent(articleUrl),
},
},
bulletinBoard: 'notifications/bulletinboard',
bulletinBoardPost: 'notifications/bulletinboard/post',
enquiriesEnquiryDetails: 'notifications/enquiry',
},
},
},
}
// https://medium.com/cybermonkey/deep-linking-push-notifications-with-react-navigation-5fce260ccca2
const getLinkingOptions: (notificationsStore: INotificationsStore) => LinkingOptions = (notificationsStore) => ({
prefixes: [`${URL_SCHEME}://`],
config: deepLinksConf,
async getInitialURL() {
// Check if app was opened from a deep link
const url = await Linking.getInitialURL()
if (url !== null) {
return url
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification()
// Get deep link from data
// if this is undefined, the app will open the default/home page
return message?.data?.link
},
subscribe(listener) {
const onReceiveURL = ({ url }: { url: string }) => listener(url)
// Listen to incoming links from deep linking
const urlReceiveEventListener = Linking.addEventListener('url', onReceiveURL)
// Listen to firebase push notifications
const unsubscribeNotification = messaging().onNotificationOpenedApp((message) => {
const url = message?.data?.link
if (message?.data?.notificationId !== undefined) {
notificationsStore.markAsReadById(message.data.notificationId)
}
if (url !== undefined) {
// Any custom logic to check whether the URL needs to be handled
// Call the listener to let React Navigation handle the URL
listener(url)
}
})
return () => {
// Clean up the event listeners
urlReceiveEventListener.remove()
unsubscribeNotification()
}
},
})
const RootStack = observer(() => {
const { authStore, entitiesStore, systemStore } = useStores()
const { notificationsStore } = entitiesStore
if (systemStore.isShowingPlacard) {
return <InitialNavigator />
}
switch (authStore.authState) {
case 'pending':
// if auth state is not determined yet, because a biometric prompt is pending,
// then show the initial navigator
return <InitialNavigator />
case 'unauthorized':
// if unauthorized then show auth navigator
return <AuthNavigator />
case 'authorized':
// Check if user has already completed onboarding
if (!authStore.hasCompletedOnboarding()) {
return <OnboardingNavigator />
}
// if authorized show the tab navigator
return <TabNavigator hasNewNotifications={notificationsStore.unreadCount > 0} />
default:
return <InitialNavigator />
}
})
export const RootNavigator = React.forwardRef<
NavigationContainerRef,
Partial<React.ComponentProps<typeof NavigationContainer>>
>((props, ref) => {
const { entitiesStore } = useStores()
const { notificationsStore } = entitiesStore
return (
<NavigationContainer {...props} ref={ref} linking={getLinkingOptions(notificationsStore)}>
<RootStack />
</NavigationContainer>
)
})
RootNavigator.displayName = 'RootNavigator'
NotificationsNavigator.tsx
type ParamList = {
notifications: undefined
settings: { mode: 'onboarding' | 'default' }
newsArticle: INewsArticle
bulletinBoardPost: { boardId: string; postId: string; shouldAddComment?: boolean }
enquiriesEnquiryDetails: { enquiryId: string }
}
const Stack = createStackNavigator<ParamList>()
export function getNotificationsTabBarStyle(
route: Partial<Route<string>> & {
state?: PartialState<NavigationState>
}
): StyleProp<ViewStyle> {
const routeName = getFocusedRouteNameFromRoute(route)
switch (routeName) {
case 'newsArticle':
case 'settings':
case 'bulletinBoardPost':
case 'enquiriesEnquiryDetails':
return { display: 'none' }
default:
return {}
}
}
export type NotificationsNavigationProps<K extends Extract<keyof ParamList, string>> = {
route: RouteProp<ParamList, K>
navigation: StackNavigationProp<ParamList, K>
}
export const NotificationsNavigator = () => (
<Stack.Navigator
initialRouteName='notifications'
screenOptions={{
headerShown: true,
headerBackTitle: '',
gestureEnabled: true,
}}
>
<Stack.Screen
component={NotificationsScreen}
name='notifications'
options={{
...defaultScreenStyle,
title: t('notificationsScreen.title'),
}}
/>
<Stack.Screen
component={NewsArticleScreen}
name='newsArticle'
options={{
...defaultScreenStyle,
headerShown: false,
}}
/>
<Stack.Screen
component={SettingsScreen}
name='settings'
options={{
...defaultScreenStyle,
title: t('tabs.settings'),
}}
/>
<Stack.Screen
component={BulletinBoardPostScreen}
name='bulletinBoardPost'
options={{ ...defaultScreenStyle, title: '' }}
/>
<Stack.Screen
component={EnquiriesEnquiryDetailsScreen}
name='enquiriesEnquiryDetails'
options={{ ...defaultScreenStyle }}
/>
</Stack.Navigator>
)
TabNavigator.tsx
export type TabNavigationProps<K extends Extract<keyof TabParamList, string>> = {
route: RouteProp<TabParamList, K>
navigation: BottomTabNavigationProp<TabParamList>
}
const Tab = createBottomTabNavigator<TabParamList>()
type Props = {
hasNewNotifications: boolean
}
export const TabNavigator = ({ hasNewNotifications }: Props) => (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarActiveTintColor: color.tint,
tabBarInactiveTintColor: color.disabled,
tabBarHideOnKeyboard: true,
tabBarActiveBackgroundColor: color.elementBackground,
tabBarInactiveBackgroundColor: color.elementBackground,
}}
tabBar={(props) => (
<TabBar
maxItems={5}
moreIcon={({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-more.png')} style={{ tintColor: color }} />
{hasNewNotifications && <View style={notificationTabStyles.indicatorDot} />}
</View>
)}
moreTitle={t('tabs.more')}
{...props}
/>
)}
>
<Tab.Screen
component={FrontPageNavigator}
name='frontPageNavigator'
options={({ route }) => ({
tabBarStyle: getFrontPageTabBarStyle(route),
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-front-page.png')} style={{ tintColor: color }} />
</View>
),
title: t('tabs.frontPage'),
})}
/>
<Tab.Screen
component={NewsNavigator}
name='newsNavigator'
options={{
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-news.png')} style={{ tintColor: color }} />
</View>
),
title: t('tabs.news'),
}}
/>
<Tab.Screen
component={BulletinBoardNavigator}
name='bulletinBoardNavigator'
options={({ route }) => ({
tabBarStyle: getBulletinBoardTabBarStyle(route),
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-bulletin-board.png')} style={{ tintColor: color }} />
</View>
),
title: t('tabs.bulletinBoard'),
})}
/>
<Tab.Screen
component={EnquiriesNavigator}
name='enquiriesNavigator'
options={({ route }) => ({
tabBarStyle: getEnquiriesTabBarStyle(route),
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-enquiries.png')} style={{ tintColor: color }} />
</View>
),
title: t('tabs.enquiries'),
})}
/>
<Tab.Screen
component={NotificationsNavigator}
name='notificationsNavigator'
options={({ route }) => ({
tabBarStyle: getNotificationsTabBarStyle(route),
tabBarIcon: ({ color, focused, size }) => (
<NotificationsTabIcon color={color} focused={focused} hasNewNotifications={hasNewNotifications} size={size} />
),
title: t('tabs.notifications'),
})}
/>
<Tab.Screen
component={UserProfileScreen}
initialParams={{ mode: 'default' }}
name='userProfile'
options={{
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image source={require('../../assets/png/icons/tab-user-profile.png')} style={{ tintColor: color }} />
</View>
),
title: t('tabs.userProfile'),
}}
/>
<Tab.Screen
component={SettingsNavigator}
name='settingsNavigator'
options={{
tabBarIcon: ({ color, size }) => (
<View style={[{ width: size, height: size }, styles.tabBarIconContainer]}>
<Image
source={require('../../assets/png/icons/tab-settings.png')}
style={{ height: size, tintColor: color, width: size }}
/>
</View>
),
title: t('tabs.settings'),
}}
/>
</Tab.Navigator>
)
const styles = StyleSheet.create({
tabBarIconContainer: {
alignItems: 'center',
justifyContent: 'center',
},
})
UPDATE
There is a simpler solution to this: Add an initialRouteName
to the navigator in the deep links config.
notificationsNavigator: {
initialRouteName: 'notifications', // <---- This
screens: {
newsArticle: 'notifications/news/article',
bulletinBoard: 'notifications/bulletinboard',
bulletinBoardPost: 'notifications/bulletinboard/post',
enquiriesEnquiryDetails: 'notifications/enquiry',
},
https://reactnavigation.org/docs/configuring-links/#rendering-an-initial-route
I fixed this by adding a screenListeners
prop to my StackNavigator
and check on focus
events, whether the notifications
screen is the root element or not, and then manipulate the routes if necessary.
I first tried doing this in a state
event but my manipulation of the state did not take effect when launching the app from a deep link when it was not in background already. This solution seems to work well in all cases:
<Stack.Navigator
initialRouteName='notifications'
screenListeners={({ navigation }) => ({
focus() {
const { routes } = navigation.getState()
// Check if NotificationsScreen is not loaded
if (routes[0].name !== 'notifications') {
// @ts-expect-error poor typing of react-navigation
navigation.dispatch((state) => {
// Add the notifications route to top of the stack
const routes = [
{
name: 'notifications',
},
...state.routes,
]
return CommonActions.reset({
...state,
routes,
index: routes.length - 1,
})
})
}
},
})}
screenOptions={{
headerShown: true,
headerBackTitle: '',
gestureEnabled: true,
}}
>