Accessing push notification data while app is killed - react-native

I am trying to access push notification data with killed app.
I have been reading the docs and implemented the steps that are provided but still i don't get any data, it's like the push notification is not detected.
I am using:
"expo": "^42.0.0",
"expo-notifications": "~0.12.3",
The code is the following:
import { enableScreens } from 'react-native-screens'
import { NavigationContainer } from '#react-navigation/native';
import CarNavigator from './src/navigation/CarNavigator'
import { Alert } from 'react-native';
import React, { useState, useEffect, useRef } from 'react'
import * as Notifications from 'expo-notifications'
import Constants from 'expo-constants';
enableScreens();
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export default function App() {
const [expoPushToken, setExpoPushToken] = useState('');
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();
const registerForPushNotificationsAsync = async () => {
let token;
if (Constants.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
} else {
console.log('Must use physical device for Push Notifications');
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token;
}
useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token));
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
setNotification(notification);
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
Alert.alert('IN');
});
return () => {
Notifications.removeNotificationSubscription(notificationListener.current);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
/* END */
return (
<NavigationContainer>
<CarNavigator />
</NavigationContainer>
)
}
As per the documentation this part handles push notification data while app is killed as stated here:
https://docs.expo.dev/versions/latest/sdk/notifications/#listening-to-notification-events
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
Alert.alert('IN');
});
When i send a push notification and start the app using that notification the Alert is not triggered, it's like the push notification is not detected.
I am testing this on android with a standalone apk build.
Anyone had this issue and managed to solve it?
Many thanks,
Trix

Related

Audio and Video not working offline when using useNetInfo from netinfo

I've been battling a bug in my code for the last 4 days and would appreciate some pointers to get me going in the right directions. Component is working fine as long as there is internet connection, but if there is no internet connection, audios and videos are not playing, only thumbnail present.
I'm using netInfo's NetInfo.fetch() to check for connection. If there is connection, I'm refetching data to check for any updates to student assignments.
I'm using expo-av for playing audio/video files (v10.2.1). I'm also using useQuery hook from react-query to fetch data about audio and video files (like url etc.) My video player component is something like this:
Video Player:
import React, {
forwardRef,
ForwardRefRenderFunction,
useCallback,
useImperativeHandle,
useRef
} from 'react';
import { Platform } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { Audio, Video, VideoFullscreenUpdateEvent, VideoProps } from 'expo-av';
const Player: ForwardRefRenderFunction<
Video | undefined,
VideoProps
> = (props, ref) => {
const innerRef = useRef<Video>(null);
const orientation = useCallback<
(event: VideoFullscreenUpdateEvent) => void
>(
(event) => {
if (Platform.OS === 'android') {
if (
event.fullscreenUpdate === Video.FULLSCREEN_UPDATE_PLAYER_DID_PRESENT
) {
Orientation.unlockAllOrientations();
} else if (
event.fullscreenUpdate === Video.FULLSCREEN_UPDATE_PLAYER_DID_DISMISS
) {
Orientation.lockToPortrait();
}
}
props.onFullscreenUpdate?.(event);
},
[props]
);
useImperativeHandle(ref, () => {
if (innerRef.current) {
return innerRef.current;
}
return undefined;
});
return (
<Video
resizeMode="contain"
useNativeControls
ref={innerRef}
onLoad={loading}
{...props}
onFullscreenUpdate={orientation}
/>
);
};
export const VideoPlayer = forwardRef(Player);
Custom Hook:
For async state management, I'm using a custom react-query hook, that looks something like this (non-relevant imports and code removed):
import { useFocusEffect } from '#react-navigation/core';
import { useCallback } from 'react';
import NetInfo from '#react-native-community/netinfo';
export const useStudentAssignment = (
assignmentId: Assignment['id']
): UseQueryResult<Assignment, Error> => {
const listKey = studentAssignmentKeys.list({ assignedToIdEq: studentData?.id });
const queryClient = useQueryClient();
const data = useQuery<Assignment, Error>(
studentAssignmentKeys.detail(assignmentId),
async () => {
const { data: assignment } = await SystemAPI.fetchAssignment(assignmentId);
return Assignment.deserialize({
...assignment,
});
},
{
staleTime: 1000 * 60 * 30,
initialData: () => {
const cache= queryClient.getQueryData<Assignment[]>(listKey);
return cache?.find((assignment) => assignment.id === assignmentId);
},
initialDataUpdatedAt: queryClient.getQueryState(listKey)?.dataUpdatedAt,
}
);
useFocusEffect(
useCallback(() => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
data.refetch();
}
});
}, [data])
);
return data;
};
Component:
import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StackScreenProps } from '#react-navigation/stack';
import { ROUTES } from 'enums/SMSRoutes';
import { StoreType } from 'enums/SMSStoreType';
import { useStudentAssignment } from 'hooks/Assignments/useStudentAssignment';
import { RootStackParamList } from 'navigators';
import { AssignmentViewer } from 'screens/AssignmentViewer';
type NavProps = StackScreenProps<
RootStackParamList,
ROUTES.ASSIGNMENT_VIEW
>;
export const AssignmentView: FC<NavProps> = ({
navigation,
route: {
params: { assignmentId }
}
}) => {
const assignmentQuery = useStudentAssignment(assignmentId);
const assignmentTracker = useStore(StoreType.AssignmentTracker);
const isDoneRef = useRef<boolean>(false);
const questions = assignmentQuery.data?.questions || [];
const activeQuestion = useMemo(() => {
return questions.filter((question) => question.active);
}, [questions]);
const onDone = useCallback(() => {
isDoneRef.current = true;
navigation.push(ROUTES.ASSIGNMENT_COMPLETED);
}, [navigation]);
useEffect(() => {
assignmentTracker.start(assignmentId);
return () => {
assignmentTracker.finish(isDoneRef.current);
};
}, []);
return (
<SafeAreaView>
<AssignmentViewer
questions={activeQuestion}
onDone={onDone}
isLoading={assignmentQuery.isLoading}
/>
</SafeAreaView>
);
};
What I'm trying to do here is that if internet connection is connected and the user navigates to the current view (which is used to view assignments), I'd like to refetch the data. Per the requirements, I can't use the staleTime property or any other interval based refetching.
Component is working fine if I don't refetch, or if internet connection is present. If connection isn't there, it doesn't play the cache'd audio/video.
If I take out the check for internet connection (remove netInfo), component display videos both offline and online. However, refetching fails due to no connectivity.
What should I change to make sure that data is refetched when connected and videos are played even if not connected to Internet?

Problem while using Expo-av Library, App crashes while changing the songs

I am running my application on an android device. So I am using Context API, the song is in context, whenever a user clicks on a song it is updated in context and every component gets it and then useEffect of PlayerWidget gets triggered and that time my app is crashing. At the Start of the app, the song is null.
import React, { useEffect, useState, useContext } from "react";
import { View, Text, Image, TouchableOpacity } from "react-native";
import tailwind from "tailwind-rn";
import { AntDesign, FontAwesome, Foundation } from "#expo/vector-icons";
import { Audio } from "expo-av";
import { Sound } from "expo-av/build/Audio/Sound";
import { AppContext } from "../AppContext";
function PlayerWidget() {
const { song } = useContext(AppContext);
useEffect(() => {
if (song) {
console.log("player", song);
console.log("a");
playCurrentSong();
}
}, [song]);
//const navigation = useNavigation();
const [sound, setSound] = (useState < Sound) | (null > null);
const [isPlaying, setIsPlaying] = useState < boolean > false;
const [duration, setDuration] = (useState < number) | (null > null);
const [position, setPosition] = (useState < number) | (null > null);
/* useEffect(() => {
playCurrentSong();
}, []);
*/
const playBackStatusUpdate = (status: any) => {
setPosition(status.positionMillis);
setDuration(status.durationMillis);
};
const playCurrentSong = async () => {
if (!song) return;
if (sound) {
console.log(sound);
await sound.unloadAsync();
}
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: song.songUri },
{ shouldPlay: isPlaying },
playBackStatusUpdate
);
setSound(newSound);
setIsPlaying(true);
};
const onPlayPausePress = async () => {
if (!sound) {
return;
}
console.log(sound);
if (isPlaying) {
await sound.pauseAsync();
setIsPlaying(false);
} else {
await sound.playAsync();
setIsPlaying(true);
}
};
if (song === null) {
return null;
}
}
export default PlayerWidget;
Can you tell me what I am doing wrong?

Deep linking - getInitialURL and subscribe not being called

Current Behavior
Currently trying to get getInitialURL and subscribe to work, but they are just no tbeing called
import * as React from 'react';
import {DefaultTheme, NavigationContainer} from '#react-navigation/native';
import AuthStack from '../features/auth/AuthStack';
import {navigationRef} from './RootNavigation';
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
import messaging from '#react-native-firebase/messaging';
import {Linking} from 'react-native';
const Navigation = () => {
const routeNameRef = React.useRef();
const MyTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: 'rgba(0,0,0,0)',
},
};
const deepLinksConf = {
initialRouteName: 'Dashboard',
screens: {
Dashboard: {
initialRouteName: 'Tasks',
screens: {
Tasks: {
initialRouteName: 'TasksIndex',
screens: {
TasksIndex: 'TasksIndex',
TaskView: 'TaskView/:id',
},
},
},
},
queue: 'queue',
},
};
const linking = {
prefixes: ['verbleif://'],
config: deepLinksConf,
async getInitialURL() {
console.log('getInitialURL');
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
console.log('just initial');
if (url != null) {
return url;
}
// Check if there is an initial firebase notification
const message = await messaging().getInitialNotification();
console.log('using firebase message link');
// Get deep link from data
// if this is undefined, the app will open the default/home page
return message?.data?.llink;
},
subscribe(listener) {
console.log('subscirbe');
const onReceiveURL = ({url}: { url: string }) => listener(url);
// Listen to incoming links from deep linking
Linking.addEventListener('url', onReceiveURL);
// Listen to firebase push notifications
const unsubscribeNotification = messaging().onNotificationOpenedApp(
(message) => {
console.log('onNotificationOpenedApp', message);
const url = message?.data?.link;
if (url) {
// 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
Linking.removeEventListener('url', onReceiveURL);
unsubscribeNotification();
};
},
};
return (
<NavigationContainer linking={linking} theme={MyTheme} ref={navigationRef}
// onReady={() => routeNameRef.current = navigationRef.current.getCurrentRoute().name}
// onStateChange={async () => {
// const previousRouteName = routeNameRef.current;
// const currentRouteName = navigationRef.current.getCurrentRoute().name;
//
// if (previousRouteName !== currentRouteName) {
// // The line below uses the expo-firebase-analytics tracker
// // https://docs.expo.io/versions/latest/sdk/firebase-analytics/
// // Change this line to use another Mobile analytics SDK
// await analytics().logScreenView({
// screen_name: currentRouteName,
// screen_class: currentRouteName,
// });
// }
//
// // Save the current route name for later comparision
// routeNameRef.current = currentRouteName;
// }}
>
<AuthStack/>
</NavigationContainer>
);
};
export default gestureHandlerRootHOC(Navigation);
Expected Behavior
The console logs to aleast appear
How to reproduce
Use the code above with the newest packages.
Your Environment
I am using react native CLI and just updated everything to newest

React Native redux using connect and store in the same component

I'm working on a react native app, i use redux to manage global state.
I want to store a state in the app component where i call the store :
/* eslint-disable global-require */
import React, { Component } from 'react'
import { Provider, connect } from 'react-redux'
import { BackHandler } from 'react-native'
// ***************************************************
import AppContainer from './eXpand/Components/Navigation';
// ***************************************************
import { AppLoading } from 'expo'
import * as Font from 'expo-font'
import { Ionicons } from '#expo/vector-icons'
import {
cacheAssets,
cacheFonts
} from './eXpand/Helpers/Defaults/AssetsCaching'
import store from '~/Store/store'
import registerForPushNotificationsAsync from './eXpand/Components/Services/notifications';
// Local Import
import { setUserGsm } from '~/Store/actions';
class App extends Component {
_isMounted = false;
constructor(props) {
super(props)
this.state = {
isReady: false,
GSM: null
}
}
/**
* Demande d'autorisation pour accéder au token du GSM et l'envoyer vers l'api
*/
async registerForPush() {
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
finalStatus = status;
}
if (finalStatus !== 'granted') {
return;
}
let gsm = await Notifications.getExpoPushTokenAsync();
if (this._isMounted) {
this.setState({
GSM: gsm
});
}
console.log("### GSM TOKEN GSM ###")
console.log(this.state.GSM)
console.log("#####################")
}
/**
* Add Font with Asynchronous Method
*/
async componentDidMount() {
console.log("########## COMPONENT DID MOUNT ############")
this._isMounted = true;
if (this._isMounted) {
this.registerForPush()
this.props.setUserGsm(this.state.GSM)
}
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
await Font.loadAsync({
// ROBOTO
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
// SFUIDisplay
SFUIDisplayBlack: require('^/Fonts/SFUIDisplay-Black.otf'),
SFUIDisplayBold: require('^/Fonts/SFUIDisplay-Bold.otf'),
SFUIDisplayHeavy: require('^/Fonts/SFUIDisplay-Heavy.otf'),
SFUIDisplayLight: require('^/Fonts/SFUIDisplay-Light.otf'),
SFUIDisplayMedium: require('^/Fonts/SFUIDisplay-Medium.otf'),
SFUIDisplaySemibold: require('^/Fonts/SFUIDisplay-Semibold.otf'),
SFUIDisplayThin: require('^/Fonts/SFUIDisplay-Thin.otf'),
SFUIDisplayUltralight: require('^/Fonts/SFUIDisplay-Ultralight.otf'),
// MyriadPro
MYRIADPROBOLD: require('^/Fonts/MyriadPro-Bold.otf'),
MyriadProBlackSemiCn: require('^/Fonts/MyriadPro-BlackSemiCn.otf'),
MyriadProBoldSemiExtended: require('^/Fonts/MyriadPro-BoldSemiExtended.ttf'),
...Ionicons.font,
// PTSans
PTSansRegular: require('^/Fonts/PTSans-Regular.ttf'),
PTSansBold: require('^/Fonts/PTSans-Bold.ttf'),
PTSansItalic: require('^/Fonts/PTSans-Italic.ttf'),
})
this.setState({ isReady: true })
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton);
console.log("########## COMPONENT WILL MOUNT ############")
this._isMounted = false;
}
handleBackButton() {
return true;
}
async _loadAssetsAsync() {
const imageAssets = cacheAssets([require('^/Logos/expanded.png')])
const fontAssets = cacheFonts({
// ROBOTO
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
// SFUIDisplay
SFUIDisplayBlack: require('^/Fonts/SFUIDisplay-Black.otf'),
SFUIDisplayBold: require('^/Fonts/SFUIDisplay-Bold.otf'),
SFUIDisplayHeavy: require('^/Fonts/SFUIDisplay-Heavy.otf'),
SFUIDisplayLight: require('^/Fonts/SFUIDisplay-Light.otf'),
SFUIDisplayMedium: require('^/Fonts/SFUIDisplay-Medium.otf'),
SFUIDisplaySemibold: require('^/Fonts/SFUIDisplay-Semibold.otf'),
SFUIDisplayThin: require('^/Fonts/SFUIDisplay-Thin.otf'),
SFUIDisplayUltralight: require('^/Fonts/SFUIDisplay-Ultralight.otf'),
// MyriadPro
MYRIADPROBOLD: require('^/Fonts/MyriadPro-Bold.otf'),
MyriadProBlackSemiCn: require('^/Fonts/MyriadPro-BlackSemiCn.otf'),
MyriadProBoldSemiExtended: require('^/Fonts/MyriadPro-BoldSemiExtended.ttf'),
// PTSans
PTSansRegular: require('^/Fonts/PTSans-Regular.ttf'),
PTSansBold: require('^/Fonts/PTSans-Bold.ttf'),
PTSansItalic: require('^/Fonts/PTSans-Italic.ttf'),
})
await Promise.all([imageAssets, fontAssets])
}
render() {
const Root = () => {
if (!this.state.isReady) {
return (
<AppLoading
startAsync={this._loadAssetsAsync}
onFinish={() => this.setState({ isReady: true })}
/>
)
}
return <AppContainer />
}
return (
<Provider store={store}>
<Root />
</Provider>
)
}
}
const mapStateToProps = (state) => ({
GSM: state.GSM
});
const mapDispatchToProps = (dispatch) => ({
setUserGsm: (GSM) => {
dispatch(setUserGsm(GSM));
}
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(App);
I get this error :
Invariant Violation: Could not find "store" in the context of "Connect(App)". Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(App) in connect options.
You cannot do it this way.
The react-redux Provider is passing store to the react-redux connect. And you are using connect in component which is not wrapped (somewhere in React component tree) in Provider (the your component itself is rendering Provider, which is too late).

Handling Errors from Redux API Call as a Toast

So I'm trying to figure out the best way to display a Toast error and success function when the API call fires from redux.
My line of thinking: Create action for the API call. If successful, then I want the screen to change to the home screen. If it fails, then display the message in a Toast.
Here's what some of my actions look like:
export function getTokenAPI(username, password) {
return async function action(dispatch) {
try {
dispatch({ type: t.AUTH_GET_TOKEN });
dispatch(setLoading(true));
const { data } = await API.authGetToken(username, password);
const { success } = data;
if (success) {
const { access_token, refresh_token } = data;
dispatch(setAccessToken(access_token));
dispatch(setRefreshToken(refresh_token));
await dispatch(setLoading(false));
} else if (!success) {
const { errorMessage } = data;
throw Error(errorMessage);
}
} catch (e) {
dispatch(setError(e.message));
dispatch(setLoading(false));
}
};
}
The setError action sets the error key to true and sets the errorMessage. Here's what my screen looks like:
import React from 'react';
import { Container, View, Toast } from 'native-base';
import styles from './styles';
import { connect } from 'react-redux';
import { authActions } from '_ducks/auth';
const LoginScreen = props => {
const { getToken, navigation } = props;
const { navigate } = navigation;
const navigateToHome = () => navigate('Home');
const handleLogin = async () => {
const { error, errorMessage } = props;
await getToken('sample', 'pass123');
if (error) {
Toast.show({
text: errorMessage,
buttonText: 'kay',
});
} else {
navigateToHome();
}
};
return (
<Container>
<View style={styles.container}>
<LoginButton onPress={handleLogin} />
</View>
</Container>
);
};
const mapDispatchToProps = dispatch => ({
getToken: () => dispatch(authActions.getTokenAPI()),
});
const mapStateToProps = state => ({
isLoading: state.authReducer.isLoading,
error: state.authReducer.error,
errorMessage: state.authReducer.errorMessage,
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LoginScreen);
So if there's an error, then display the toast. If it's successful, navigate to the home screen. Essentially, error will not be true quick enough to make the check within handleLogin work appropriately.
Any recommendations on the pattern or process? Should I be using a useEffect hook here?