Reputation: 1
I have a react native app which incorporates expo-notifications library that sends notifications to users on iOS and android devices. I am facing a big issue. I was able to configure the project on expo to enable push notification support and also generated a certificate for the iOS part. Notifications are working perfectly fine on android as they request for permissions for the android part and even display the expo push token for the notification and send notifications normally. However, regarding the iOS part, the notification permissions are being requested, and the expo push token exists, but the notification is not being sent on the iOS simulator whether the app is in foreground or even background. It does not display an error, it just does not send a notification.
Here is the code
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useCallback, useEffect } from "react";
import {
Alert,
Platform,
StyleSheet,
View,
PermissionsAndroid,
} from "react-native";
import { fonts } from "./constants/fonts";
import CustomNavigation from "./routes/CustomNavigation";
import * as Notifications from "expo-notifications";
import Constants from "expo-constants";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
import { setPushToken } from "./store/features/pushtoken.slice";
import { Text } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { usePostUserLocationMutation } from "./store/api/profile";
import * as Location from "expo-location";
import * as TaskManager from "expo-task-manager";
import { useGetDriverShipmentsQuery } from "./store/api/shipments";
import { setUserLocation } from "./store/features/user.slice";
const LOCATION_TRACKING = "location-tracking";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
async function registerForPushNotificationsAsync() {
let token;
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;
}
// Project ID can be found in app.json | app.config.js; extra > eas > projectId
// token = (await Notifications.getExpoPushTokenAsync({ projectId: "YOUR_PROJECT_ID" })).data;
token = (
await Notifications.getExpoPushTokenAsync({
projectId: Constants.manifest?.extra!.eas.projectId,
})
).data;
console.log(token);
// The token should be sent to the server so that it can be used to send push notifications to the device
// console.log(token);
return token;
}
const Index = () => {
const navigation = useNavigation<any>();
SplashScreen.preventAutoHideAsync();
const [fontsLoaded] = useFonts(fonts());
const [notification, setNotification] = React.useState<any>(false);
const dispatch = useDispatch<AppDispatch>();
const userState = useSelector<RootState, RootState["user"]>(
(state) => state.user
);
const expoPushToken = useSelector<RootState, RootState["pushToken"]>(
(state) => state.pushToken
);
const [postUserLocation, { data, isLoading, isSuccess, isError, error }] =
usePostUserLocationMutation();
const {
data: dataShipments,
isLoading: isLoadingShipments,
isSuccess: isSuccessShipments,
isFetching: isFetchingShipments,
} = useGetDriverShipmentsQuery(
{ page: 0, expired: 0 },
{ pollingInterval: 10000 }
);
const notificationListener = React.useRef<any>();
const responseListener = React.useRef<any>();
React.useEffect(() => {
registerForPushNotificationsAsync().then((token) => {
dispatch(setPushToken({ pushToken: token }));
});
// This listener is fired whenever a notification is received while the app is foregrounded
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification);
});
// This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const {
notification: {
request: {
content: {
data: { screen, props },
},
},
},
} = response;
// When the user taps on the notification, this line checks if they //are suppose to be taken to a particular screen
if (screen) {
if (props) {
navigation.navigate(screen, { ...props });
} else {
navigation.navigate(screen);
}
}
});
return () => {
Notifications.removeNotificationSubscription(
notificationListener.current
);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
if (dataShipments && dataShipments.shipments.length > 0) {
TaskManager.defineTask(LOCATION_TRACKING, async ({ data, error }) => {
try {
if (error) {
console.log("LOCATION_TRACKING task ERROR:", error);
return;
}
if (data) {
//@ts-ignore
const { locations } = data;
let lat = locations[0].coords.latitude;
let long = locations[0].coords.longitude;
if (Boolean(userState.email)) {
const res = await postUserLocation({
lat: lat,
lng: long,
}).unwrap();
console.log("res ", lat, long);
dispatch(setUserLocation({ lat: lat, lng: long }));
}
}
} catch (err) {
console.warn(err);
}
});
}
React.useEffect(() => {
if (expoPushToken) {
async function requestLocationPermission() {
let hasPermissions = true;
try {
let resf = await Location.requestForegroundPermissionsAsync();
let resb = await Location.requestBackgroundPermissionsAsync();
if (resf.status != "granted" && resb.status != "granted") {
return;
}
hasPermissions = resf.status == "granted" && resb.status == "granted";
} catch (err: any) {
console.warn(err.message);
hasPermissions = false;
} finally {
return hasPermissions;
}
}
async function getLocation() {
const permissionsStatus = await requestLocationPermission();
if (
permissionsStatus &&
dataShipments &&
dataShipments.shipments.length > 0
) {
await Location.startLocationUpdatesAsync(LOCATION_TRACKING, {
accuracy: Location.Accuracy.Highest,
timeInterval: 5000,
distanceInterval: 0,
showsBackgroundLocationIndicator: true,
foregroundService: {
notificationTitle: "Location Tracking",
notificationBody: `From the moment you are assigned the delivery of a shipment, your location will be tracked to ensure the logistics of the shipment for the shipper's knowledge. When you finish all your deliveries, your location will no longer be tracked.`,
},
});
} else {
if (dataShipments && dataShipments.shipments.length === 0) {
await Location.stopLocationUpdatesAsync(LOCATION_TRACKING);
}
}
}
getLocation();
}
return () => {};
}, [expoPushToken, dataShipments]);
const cb = useCallback(async () => {
if (fontsLoaded) {
await SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
} else {
cb();
}
return <CustomNavigation />;
};
export default Index;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
This is a portion of the server side code on how we send notifications
export const updateStatus = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { id, action } = req.body;
console.log("updated");
const expo = req.expo;
const messages = [] as ExpoPushMessage[];
const currentUser = req.user;
const error = new CustomError("Something went wrong!", 500);
try {
const foundShipment = await shipment
.findById(id)
.populate(["driver", "host"]);
if ((action as actionType) === "select_driver") {
error.message = "forbidden action";
error.status = 403;
throw error;
}
console.log(action);
console.log(currentUser._id.toString());
if (
(foundShipment.host as IUser)._id.toString() ===
currentUser._id.toString()
) {
const allowedActionsCustomer = getShipmentStatus(foundShipment.status)[
"hostActions"
];
if (!allowedActionsCustomer.includes(action)) {
console.log("error");
error.message = "forbidden action";
error.status = 403;
throw error;
}
switch (action as actionType) {
case "acknowledge_completion":
for (let token of (foundShipment.driver as IUser).push_token) {
messages.push({
to: token,
data: { screen: "driver" },
sound: "default",
title: "Shipment acknowledgement",
body: "Your shipment has been acknowledged",
channelId: "default",
});
}
break;
case "cancel_shipment":
if (foundShipment.driver)
for (let token of (foundShipment.driver as IUser).push_token) {
messages.push({
to: token,
sound: "default",
data: { screen: "driver" },
title: "Shipment canceled",
body: "Your shipment has been cancelled",
channelId: "default",
});
}
break;
case "mark_late":
for (let token of (foundShipment.driver as IUser).push_token) {
messages.push({
to: token,
sound: "default",
data: { screen: "driver" },
title: "Shipment delayed",
body: "Your shipment is marked late",
channelId: "default",
});
}
break;
}
} else if (
(foundShipment.driver as IUser)._id.toString() ===
currentUser._id.toString()
) {
const allowedActionsDriver = getShipmentStatus(foundShipment.status)[
"driverActions"
];
if (!allowedActionsDriver.includes(action)) {
error.message = "forbidden action";
error.status = 403;
throw error;
}
switch (action as actionType) {
case "cancel_shipment":
for (let token of (foundShipment.host as IUser).push_token) {
messages.push({
to: token,
sound: "default",
data: { screen: "shipper" },
title: "Shipment canceled",
body: "Your shipment has been cancelled",
channelId: "default",
});
}
break;
case "deliver_shipment":
for (let token of (foundShipment.host as IUser).push_token) {
messages.push({
to: token,
sound: "default",
data: { screen: "shipper" },
title: "Shipment delivered",
body: "Your shipment has been delivered",
channelId: "default",
});
}
break;
case "pick_up":
for (let token of (foundShipment.host as IUser).push_token) {
messages.push({
to: token,
sound: "default",
data: { screen: "shipper" },
title: "Shipment picked up",
body: "Your shipment has been picked up",
channelId: "default",
});
}
break;
}
} else {
error.message = "forbidden action";
error.status = 403;
throw error;
}
foundShipment.status = getNewStatus(action as actionType);
if (foundShipment.status === "fulfilled") {
const foundDriver = await usersModel.findById(
(foundShipment.driver as IUser)._id.toString()
);
foundDriver.nb_successful_deliveries =
foundDriver.nb_successful_deliveries + 1;
if (foundShipment.shipment_payment_type !== "cash") {
foundDriver.wallet_balance += foundShipment.shipment_price * 0.6;
}
await foundDriver.save();
} else if (foundShipment.status === "unfulfilled") {
let foundDriver;
if (foundShipment.driver) {
foundDriver = await usersModel.findById(
(foundShipment.driver as IUser)._id.toString()
);
}
const foundShipper = await usersModel.findById(
(foundShipment.host as IUser)._id.toString()
);
if (foundShipment.shipment_payment_type !== "cash") {
foundShipper.wallet_balance += foundShipment.shipment_price;
}
if (foundDriver) {
foundDriver.nb_failed_deliveries = foundDriver.nb_failed_deliveries + 1;
await foundDriver.save();
}
}
console.log(messages);
const chunks = expo.chunkPushNotifications(messages as any);
for (let chunk of chunks) {
if (chunk) {
await expo.sendPushNotificationsAsync(chunk);
}
}
await foundShipment.save();
res.send({ status: true });
} catch (err) {
return next(err);
}
};
Can someone please tell me what to do? I appreciate your help yall. If anyone needs any more code content please let me know what to send.
I tried to console log the expo push token and it gave the push token on iOS simulator but it didnt send the notification.
Edit: This is an ejected project from expo to react-native. I believe that the issue might be related to the expo code not working in react native ejected code but still I need help.
Upvotes: 0
Views: 2815
Reputation: 602
There are some limitations to test push notifications in an iOS simulator.
To test iOS notifications you can check this article.
And this.
To test push notifications the same way you´re testing on Android emulator you must have an iOS physic device.
Upvotes: 1