Expo notification doesnt fire method when app is killed/closed - react-native

im using react native expo and push notification works fine when app is running or in background but with the app is close. it doesnt call the method to handle the notification. I need to redirect to detail page. I tried to use function compoents and class components, tried to migrade to legacy notification and the new one.
import React, {useState, useEffect, useRef} from 'react';
import {
Image, ScrollView,
StyleSheet, Text, TouchableOpacity, Platform,
View, Linking,
} from 'react-native';
import * as Notifications from "expo-notifications";
const HomeScreen = (props) => {
useEffect(() => {
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey,type} = data ||{}
if(annKey) {
// navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const {notification} = response ||{}
console.log(notification);
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey, type} = data ||{}
if(annKey){
navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
}
export default HomeScreen;

The problem is that the useEffect() get called too late after the app has finished initializing. Therefore the listener is not added before the system has abandoned the notification, and the handler not called.
Fortunately, since you are using the new expo-notifications library, it introduced the useLastNotificationResponse() React hook. It can replace the addNotificationResponseReceivedListener() listener and returns the last notification the user interacted with (i.e. tap). It can be safely used in a useEffect() hook.
You can find the documentation here : https://docs.expo.io/versions/latest/sdk/notifications/#uselastnotificationresponse-undefined--notificationresponse--null
Here is how to use it (it's better to implement it on your root component):
import * as Notifications from 'expo-notifications';
export default function App() {
const lastNotificationResponse = Notifications.useLastNotificationResponse();
React.useEffect(() => {
if (
lastNotificationResponse &&
lastNotificationResponse.notification.request.content.data['someDataToCheck'] &&
lastNotificationResponse.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
) {
// navigate to your desired screen
}
}, [lastNotificationResponse]);
return (
/*
* your app
*/
);
}

You have to add this to your app.json file:
"android": {
"useNextNotificationsApi": true,
},

Related

Are there any parts where 'react-native-mmkv' and 'redux' should be viewed differently? for react-native

I use both 'react-native-mmkv' and 'redux' in my project, but there is no big difference in the way data is stored and used.
Aside from the performance, what's better for storing and using data securely? Or is it better to use both methods?
I am sorry that I am not good at English.
is my code
import * as React from 'react';
import {
SafeAreaView,
Text,
Pressable,
} from 'react-native';
// UI components
import { Button } from 'atoms';
import { Card, SelectList } from 'molecules';
// Hooks
import { useIsFocused } from '#react-navigation/native';
// Redux
import { useAppSelector, useAppDispatch } from 'hooks';
// utils
import { initializeAccountData } from 'utils';
import { useMMKVString } from "react-native-mmkv";
const App = ({ navigation, route }: any) =\> {
// Navigation
const isFocused = useIsFocused();
// Redux
// const { accounts } = useAppSelector(state =\> state.accounts);
// const dispatch = useAppDispatch();
// MMKV Hooks
const \[accounts, setAccount\] = useMMKVString('accounts')
// Update to didMount, update
React.useEffect(() =\> {
const dataInitialize = async () =\> {
await initializeAccountData();
}
dataInitialize();
}, \[\]);
// Update to page focusing
React.useEffect(() =\> {
}, \[isFocused\]);
// Update to account change
React.useEffect(() =\> {
}, \[accounts\]);
return (
\<SafeAreaView style={{ flex: 1 }}\>
...
\</SafeAreaView\>
)
}
export default App;
Redux is in memory storage, if you kill your app you will lose app state.
MMKV is persistent storage and you need to use it if you want to save data for the long term.
You can connect MMKV with redux if you want to store redux state. An example is here https://github.com/mrousavy/react-native-mmkv/blob/master/docs/WRAPPER_REDUX.md

How to make custom useTabDoublePressEffect() in React Navigation?

How can I call a callback function when I press on a tab in Tab.Navigator which is already selected?
The custom useTabDoublePressEffect hook:
import {EventArg, useNavigation, useRoute} from '#react-navigation/native';
import React from 'react';
export default function useTabDoublePressEffect(callback: Function) {
const navigation = useNavigation();
const route = useRoute();
React.useEffect(() => {
let current = navigation; // The screen might be inside another navigator such as stack nested in tabs
// We need to find the closest tab navigator and add the listener there
while (current && current.getState().type !== 'tab') {
current = current.getParent();
}
if (!current) {
return;
}
const unsubscribe = current.addListener(
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
// #ts-expect-error
'tabPress',
(e: EventArg<'tabPress', true>) => {
// We should scroll to top only when the screen is focused
const isFocused = navigation.isFocused(); // In a nested stack navigator, tab press resets the stack to first screen
// So we should scroll to top only when we are on first screen
const isFirst =
navigation === current ||
navigation.getState().routes[0].key === route.key;
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (isFocused && isFirst && !e.defaultPrevented) {
callback();
}
});
},
);
return unsubscribe;
}, [navigation, route.key]);
}
Basic example for how to use it:
import React, {useState} from 'react';
import {Text} from 'react-native';
import useTabDoublePressEffect from '../hooks/useTabDoublePressEffect';
export default function SomeComponent() {
const [text, setText] = useState('Hello');
useTabDoublePressEffect(() => setText('Bye'));
return <Text>{text}</Text>;
}
Code borrowed from:
https://github.com/react-navigation/react-navigation/blob/ac24e617af10c48b161d1aaa7dfc8c1c1218a3cd/packages/native/src/useScrollToTop.tsx

Expo: How to detect when a WebBrowser instance is closed by the user?

I have an Expo app that will open some web page with a redirect to expo itself. On that case, this is to perform 3DS callbacks. Here is a very simplified version:
import React, {
FC, useEffect, useState,
} from 'react';
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';
import {
Button,
} from '#private/apps-components';
import {
ButtonProps,
View,
} from 'react-native';
export const MyComponent: FC = () => {
const [loading, setLoading] = useState<boolean>(false);
const urlEventHandler = async (event): Promise<void> => {
console.log('url-event', event);
setLoading(false);
// Stuff...
};
useEffect(() => {
Linking.addEventListener('url', urlEventHandler);
return () => Linking.removeEventListener('url', urlEventHandler);
}, []);
const handlePress: ButtonProps['onPress'] = () => {
setLoading(false);
WebBrowser.openBrowserAsync(aRandomUrlThatWillRedirectToTheApp, {
showInRecents: true,
})
}
return (
<View>
<Button
title="Test"
onPress={handlePress}
loading={loading}
/>
</View>
);
};
export default null;
This is working. However, if the customer close the navigator before the web redirect is being processed, the app is stuck on the loading state.
The question is: How to detect if a user has closed the opened WebBrowser?
Solved this using AppState:
https://reactnative.dev/docs/appstate
if (appState === "active") { // do things after closing the browser }
I haven't actually tested this - could follow up - but you could probably use react-navigation to detect whether the component is in focus or not. IE when you open the web browser, the component is not in focus, but when you close the web browser, the component is back in focus.
For react navigation version 4 you would wrap the component in withNavigationFocus in order to achieve this: https://reactnavigation.org/docs/4.x/function-after-focusing-screen#triggering-an-action-with-the-withnavigationfocus-higher-order-component. For 5, and 5+, you can use the useIsFocused hook: https://reactnavigation.org/docs/5.x/function-after-focusing-screen/#re-rendering-screen-with-the-useisfocused-hook

React Native status bar event for dimensions in 0.62.x

StatusBarIOS has a method addListener which allows us to listen for changes to the status bar height, like so:
StatusBarIOS.addListener('statusBarFrameWillChange', (statusBarData) => {
this.setState({statusBarHeight: statusBarData.frame.height});
});
StatusBarIOS is deprecated, with a message that the code has been merged into StatusBar
How can we listen for the statusBarFrameWillChange event?
You can use the NativeEventEmitter module, here's an example of a react hook using the module to get the status bar height.
import React, { useState, useEffect } from 'react';
import { NativeEventEmitter, NativeModules } from 'react-native';
const { StatusBarManager } = NativeModules;
export default function useStatusBarHeight() {
const [value, setValue] = useState();
useEffect(() => {
const emitter = new NativeEventEmitter(StatusBarManager);
StatusBarManager.getHeight((statusBarFrameData) => setValue(statusBarFrameData.height));
const listener = emitter.addListener('statusBarFrameWillChange', (data) => setValue(data.frame.height));
return () => listener.remove();
}, []);
return value;
}
This snippet also uses the StatusBarManager to grab the initial height.

How to listen to route changes in react router v4?

I have a couple of buttons that acts as routes. Everytime the route is changed, I want to make sure the button that is active changes.
Is there a way to listen to route changes in react router v4?
I use withRouter to get the location prop. When the component is updated because of a new route, I check if the value changed:
#withRouter
class App extends React.Component {
static propTypes = {
location: React.PropTypes.object.isRequired
}
// ...
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.onRouteChanged();
}
}
onRouteChanged() {
console.log("ROUTE CHANGED");
}
// ...
render(){
return <Switch>
<Route path="/" exact component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
<Route path="/success" component={SuccessPage} />
// ...
<Route component={NotFound} />
</Switch>
}
}
To expand on the above, you will need to get at the history object. If you are using BrowserRouter, you can import withRouter and wrap your component with a higher-order component (HoC) in order to have access via props to the history object's properties and functions.
import { withRouter } from 'react-router-dom';
const myComponent = ({ history }) => {
history.listen((location, action) => {
// location is an object like window.location
console.log(action, location.pathname, location.state)
});
return <div>...</div>;
};
export default withRouter(myComponent);
The only thing to be aware of is that withRouter and most other ways to access the history seem to pollute the props as they de-structure the object into it.
As others have said, this has been superseded by the hooks exposed by react router and it has a memory leak. If you are registering listeners in a functional component you should be doing so via useEffect and unregistering them in the return of that function.
v5.1 introduces the useful hook useLocation
https://reacttraining.com/blog/react-router-v5-1/#uselocation
import { Switch, useLocation } from 'react-router-dom'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
You should to use history v4 lib.
Example from there
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
withRouter, history.listen, and useEffect (React Hooks) works quite nicely together:
import React, { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
const Component = ({ history }) => {
useEffect(() => history.listen(() => {
// do something on route change
// for my example, close a drawer
}), [])
//...
}
export default withRouter(Component)
The listener callback will fire any time a route is changed, and the return for history.listen is a shutdown handler that plays nicely with useEffect.
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
function MyApp() {
const location = useLocation();
useEffect(() => {
console.log('route has been changed');
...your code
},[location.pathname]);
}
with hooks
With hooks:
import { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { history as historyShape } from 'react-router-prop-types'
const DebugHistory = ({ history }) => {
useEffect(() => {
console.log('> Router', history.action, history.location)
}, [history.location.key])
return null
}
DebugHistory.propTypes = { history: historyShape }
export default withRouter(DebugHistory)
Import and render as <DebugHistory> component
import { useHistory } from 'react-router-dom';
const Scroll = () => {
const history = useHistory();
useEffect(() => {
window.scrollTo(0, 0);
}, [history.location.pathname]);
return null;
}
With react Hooks, I am using useEffect
import React from 'react'
const history = useHistory()
const queryString = require('query-string')
const parsed = queryString.parse(location.search)
const [search, setSearch] = useState(parsed.search ? parsed.search : '')
useEffect(() => {
const parsedSearch = parsed.search ? parsed.search : ''
if (parsedSearch !== search) {
// do some action! The route Changed!
}
}, [location.search])
in this example, Im scrolling up when the route change:
import React from 'react'
import { useLocation } from 'react-router-dom'
const ScrollToTop = () => {
const location = useLocation()
React.useEffect(() => {
window.scrollTo(0, 0)
}, [location.key])
return null
}
export default ScrollToTop
In some cases you might use render attribute instead of component, in this way:
class App extends React.Component {
constructor (props) {
super(props);
}
onRouteChange (pageId) {
console.log(pageId);
}
render () {
return <Switch>
<Route path="/" exact render={(props) => {
this.onRouteChange('home');
return <HomePage {...props} />;
}} />
<Route path="/checkout" exact render={(props) => {
this.onRouteChange('checkout');
return <CheckoutPage {...props} />;
}} />
</Switch>
}
}
Notice that if you change state in onRouteChange method, this could cause 'Maximum update depth exceeded' error.
For functional components try useEffect with props.location.
import React, {useEffect} from 'react';
const SampleComponent = (props) => {
useEffect(() => {
console.log(props.location);
}, [props.location]);
}
export default SampleComponent;
For React Router v6 & React Hooks,
You need to use useLocation instead of useHistory as it is deprecated
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
export default function Component() {
const history = useLocation();
useEffect(() => {
console.log('> Router', history.pathname)
}, [history.pathname]);
}
With the useEffect hook it's possible to detect route changes without adding a listener.
import React, { useEffect } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom';
import Main from './Main';
import Blog from './Blog';
const App = ({history}) => {
useEffect( () => {
// When route changes, history.location.pathname changes as well
// And the code will execute after this line
}, [history.location.pathname]);
return (<Switch>
<Route exact path = '/' component = {Main}/>
<Route exact path = '/blog' component = {Blog}/>
</Switch>);
}
export default withRouter(App);
I just dealt with this problem, so I'll add my solution as a supplement on other answers given.
The problem here is that useEffect doesn't really work as you would want it to, since the call only gets triggered after the first render so there is an unwanted delay.
If you use some state manager like redux, chances are that you will get a flicker on the screen because of lingering state in the store.
What you really want is to use useLayoutEffect since this gets triggered immediately.
So I wrote a small utility function that I put in the same directory as my router:
export const callApis = (fn, path) => {
useLayoutEffect(() => {
fn();
}, [path]);
};
Which I call from within the component HOC like this:
callApis(() => getTopicById({topicId}), path);
path is the prop that gets passed in the match object when using withRouter.
I'm not really in favour of listening / unlistening manually on history.
That's just imo.