-
Hello, I would like to know how to ensure that that navigation is ready when my notification listeners are called inside my app. Here is my index.js /**
* @format
*/
import 'react-native-gesture-handler';
import { AppRegistry } from 'react-native';
import App from './src/App';
import { name as appName } from './app.json';
import messaging from '@react-native-firebase/messaging';
import React from 'react';
// Callback called when notification received by device in background state
messaging().setBackgroundMessageHandler(async remoteMessage => {
if (__DEV__){
console.log('Message handled in the background!', remoteMessage);
}
});
// Required for Firebase, as per the doc https://rnfirebase.io/messaging/usage#background-application-state
function HeadlessCheck({isHeadless}) {
if (isHeadless) {
// App has been launched in the background by iOS, ignore
return null;
}
return <App />; // <-- App component in the next code snippet
}
AppRegistry.registerComponent(appName, () => HeadlessCheck); Here is my App component (root component) import...
export interface Props {}
interface State {
switchTheme?: boolean;
navigationState?: InitialState;
showAppUpdateDialog?: boolean;
}
export default class App extends React.Component<Props, State> {
public state: State;
public props: Props;
public centralServerProvider: CentralServerProvider;
public notificationManager: NotificationManager;
public deepLinkingManager: DeepLinkingManager;
private appVersion: CheckVersionResponse;
private location: LocationManager;
private themeSubscription: NativeEventSubscription;
private navigationRef: React.Ref<NavigationContainerRef<ReactNavigation.RootParamList>>;
private appStateSubscription;
public constructor(props: Props) {
super(props);
this.navigationRef = React.createRef();
this.state = {
switchTheme: false,
navigationState: null,
showAppUpdateDialog: false
};
}
public async componentDidMount() {
this.centralServerProvider = await ProviderFactory.getProvider();
this.deepLinkingManager = DeepLinkingManager.getInstance();
this.deepLinkingManager?.initialize(this.navigationRef?.current, this.centralServerProvider);
this.deepLinkingManager.startListening();
this.location = await LocationManager.getInstance();
this.location.startListening();
// Init Notification --------------------------------------
await this.notificationManager?.initialize(this.navigationRef?.current);
this.notificationManager.setCentralServerProvider(this.centralServerProvider);
await this.notificationManager.start(); // <-- This registers the listeners, but how to make sure they are called when navigation is ready ?
}
public componentWillUnmount() {
this.deepLinkingManager.stopListening();
this.notificationManager.stop(); // <-- Unregister notification listeners
this.location.stopListening()
}
public render() {
const { switchTheme, showAppUpdateDialog, navigationState } = this.state;
return switchTheme ? (
<NativeBaseProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<RootSiblingParent>
<StatusBar barStyle={ThemeManager.getInstance()?.isThemeTypeIsDark() ? 'light-content' : 'dark-content'} translucent backgroundColor="transparent" />
{this.createRootNavigator()}
</RootSiblingParent>
</GestureHandlerRootView>
</NativeBaseProvider>
) : (
<NativeBaseProvider>
<View />
</NativeBaseProvider>
);
}
private createRootNavigator() {
return (
<SafeAreaProvider>
<NavigationContainer
onReady={() => this.onReady()}
ref={this.navigationRef}
onStateChange={persistNavigationState}
initialState={this.state.navigationState}>
<rootStack.Navigator initialRouteName="AuthNavigator" screenOptions={{ headerShown: false }}>
<rootStack.Screen name="AuthNavigator" children={createAuthNavigator} />
<rootStack.Screen name="AppDrawerNavigator" children={createAppDrawerNavigator} />
</rootStack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
private onReady(): void {
void RNBootSplash.hide({ fade: true });
}
} Here is my NotificationManager class import { NavigationContainerRef } from '@react-navigation/native';
import { Alert, Platform } from 'react-native';
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import CentralServerProvider from '../provider/CentralServerProvider';
import { NotificationData, UserNotificationType } from '../types/UserNotifications';
import { getVersion, getApplicationName, getBundleId } from 'react-native-device-info';
import {PLATFORM} from '../theme/variables/commonColor';
export default class NotificationManager {
private static instance: NotificationManager;
private token: string;
private navigator: NavigationContainerRef<any>;
private removeForegroundNotificationListener: () => void;
private removeBackgroundNotificationListener: () => void;
private removeTokenRefreshEventListener: () => void;
private centralServerProvider: CentralServerProvider;
// eslint-disable-next-line no-useless-constructor
private constructor() {}
public static getInstance(): NotificationManager {
if (!NotificationManager.instance) {
NotificationManager.instance = new NotificationManager();
}
return NotificationManager.instance;
}
public setCentralServerProvider(centralServerProvider: CentralServerProvider): void {
this.centralServerProvider = centralServerProvider;
}
public async initialize(navigator: NavigationContainerRef<ReactNavigation.RootParamList>): Promise<void> {
this.navigator = navigator;
// Check if app has permission
let authorizationStatus = await messaging().hasPermission();
if (Platform.OS === PLATFORM.IOS &&
authorizationStatus !== messaging.AuthorizationStatus.PROVISIONAL &&
authorizationStatus !== messaging.AuthorizationStatus.AUTHORIZED
) {
try {
// requesting permission from user is required for iOS
authorizationStatus = await messaging().requestPermission();
} catch ( error ) {
console.error(error);
}
}
if (authorizationStatus === messaging.AuthorizationStatus.AUTHORIZED || messaging.AuthorizationStatus.PROVISIONAL) {
try {
// Retrieve mobile token
const fcmToken = await messaging().getToken();
if ( fcmToken ) {
this.token = fcmToken;
}
} catch ( error ) {
console.error(error);
}
}
}
public async start(): Promise<void> {
// Get initial notification received when the app was in quit state
const initialNotification = await messaging().getInitialNotification(); <--- If I call start inside the onReady method of NavigationContainer, getInitialNotification returns something from background state.
if (initialNotification) {
console.log(initialNotification);
this.onBackgroundNotificationOpened(initialNotification);
}
// Listen for notifications when the app is in background state
if (!this.removeBackgroundNotificationListener) {
console.log('registering background listener');
this.removeBackgroundNotificationListener = messaging().onNotificationOpenedApp((remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
console.log('opened notification');
this.onBackgroundNotificationOpened(remoteMessage);
});
}
// Listen for notifications when app is in foreground state
this.removeForegroundNotificationListener = this.removeForegroundNotificationListener ??
messaging().onMessage((remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
//Alert.alert(remoteMessage.notification.title, remoteMessage.notification.body);
});
// Listen for mobile token refresh event
this.removeTokenRefreshEventListener = messaging().onTokenRefresh((token) => {
if (this.centralServerProvider.isUserConnected()) {
void this.updateToken(token);
}
});
}
private async updateToken(token: string) {
this.token = token;
try {
await this.centralServerProvider.saveUserMobileData(this.centralServerProvider.getUserInfo().id, {
mobileToken: this.getToken(),
mobileOS: Platform.OS,
mobileAppName: getApplicationName(),
mobileBundleID: getBundleId(),
mobileVersion: getVersion()
});
} catch (error) {
console.log(error);
}
}
private onBackgroundNotificationOpened(remoteMessage: FirebaseMessagingTypes.RemoteMessage) {
// Check remote message is of notification type
if (remoteMessage?.notification) {
const notificationData = remoteMessage.data as unknown as NotificationData;
if (notificationData) {
const notificationType = notificationData?.notificationType;
switch ( notificationType ) {
case UserNotificationType.END_USER_ERROR:
const errorMessage = remoteMessage.notification.body;
const errorTitle = remoteMessage.notification.title;
Alert.alert(errorTitle, errorMessage);
}
}
}
}
public stop() {
this.removeForegroundNotificationListener?.();
console.log('Removing background listener');
console.log(this.removeBackgroundNotificationListener);
this.removeBackgroundNotificationListener?.();
this.removeTokenRefreshEventListener?.();
}
public getToken(): string {
return this.token;
}
public getOs(): string {
return Platform.OS;
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 4 replies
-
I don't believe you can ensure navigation is ready when the notification / link / message listeners fire. I solve it from the other direction. In your app you should maintain a list of pending navigation actions you wish you could take if the navigator was ready, if the navigator is not ready. When the navigator calls onReady, check for pending navigations and perform the one that is most important. This works well for me and eliminates any complicated timing setups |
Beta Was this translation helpful? Give feedback.
-
That's interesting ! |
Beta Was this translation helpful? Give feedback.
-
I'm done implementing the mechanism you suggested. Still now I have an issue with the onReady prop callback only being called when putting the app to background on iOS 😢 |
Beta Was this translation helpful? Give feedback.
I don't believe you can ensure navigation is ready when the notification / link / message listeners fire.
I solve it from the other direction. In your app you should maintain a list of pending navigation actions you wish you could take if the navigator was ready, if the navigator is not ready.
When the navigator calls onReady, check for pending navigations and perform the one that is most important.
This works well for me and eliminates any complicated timing setups