Muhammad Waleed · Follow
14 min read · Mar 17, 2024
Welcome to my comprehensive guide on seamlessly engaging with your React Native app audience in real-time! Discover the straightforward process of integrating push notifications using Expo. Keep your app dynamic and your users informed every step of the way with these simple steps!
· Expo Dev Account
· Basic knowledge of React Native and python development.
· Node.js installed on your development machine.
· Familiarity with npm or pnpm for managing dependencies.
· A physical device (iOS or Android) or an emulator/simulator set up for testing push notifications during development.
Create an expo app using below command,
npx create-expo-app push-notifications
Install all the required dependencies by running,
npx expo install expo-notifications expo-device expo-constants @react-navigation/native react-native-screens react-native-safe-area-context react-native-gesture-handler @react-navigation/stack @react-native-async-storage/async-storage
Now run the app using below command,
npx expo start
You can use emulator/simulator or download the expo go app and scan the QR code.
If you are already signed in to an Expo account using Expo CLI, you can skip the steps described in this section. If you are not, run the following command to log in:
npx eas login
To configure an Android or an iOS project for EAS Build, run the following command:
npx eas build:configure
It will create an eas.json file in your root directory and modify the app.json file which will later be used for building an apk.
Our app is ready to rock with loader, home, login, and register screens, each powered up with API calls and async storage for data retrieval. The loader screen works smartly to check the user’s login status and smoothly guides them through, ensuring a delightful user experience every step of the way!
Let’s change the app.js file to set up paths and handle push notifications.
const Stack = createStackNavigator();
Firstly we’re setting up a navigation stack for our app. This stack helps us smoothly transition between different screens or pages.
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
Here, we’re configuring how our app should handle notifications when they arrive. We’re saying that when a notification comes in, we want it to show an alert, play a sound, and set a badge on the app icon. This ensures that users are promptly informed and engaged when they receive notifications.
async function sendPushNotification(expoPushToken) {
const message = {
to: expoPushToken,
sound: 'default',
title: 'Original Title',
body: 'And here is the body!',
data: { someData: 'goes here' },
};
In this function, we’re creating a message to send a push notification to a specific device identified by its Expo Push Token. The message includes details like the title, body, and any additional data we want to send along with the notification. Once the message is prepared, it will be sent to the specified device, allowing us to communicate with our app users in real-time.
await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
}
In this code snippet, we’re using the fetch function to send a POST request to the Expo push notification server. We specify the URL where the request should be sent, along with the method (POST) and headers to ensure proper communication. Additionally, we include the message we prepared earlier as the body of the request, which contains the necessary information for the push notification. This allows us to deliver the message to the Expo server, which then handles the task of sending the push notification to the target device.
async function registerForPushNotificationsAsync() {
let token; if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig.extra.eas.projectId,
});
await AysncStorage.setItem('pushToken', token.data);
} else {
alert('Must use physical device for Push Notifications');
}
return token.data;
}
We’re registering the device for push notifications in above function. First, we check if the device is running on Android, and if so, we configure the notification channel settings. Then, we check if the device is indeed a physical device and not a simulator. Next, we request permission from the user to receive push notifications. If permission is granted, we retrieve the Expo Push Token for the device and store it using AsyncStorage. Finally, we return the Expo Push Token data for further use in our app.
const [expoPushToken, setExpoPushToken] = useState('');
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token));
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
setNotification(notification);
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
console.log(response);
});
return () => {
Notifications.removeNotificationSubscription(notificationListener.current);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
In this part of the code, we’re setting up some special hooks to handle push notifications. We create a couple of boxes to keep track of things: one for storing the Expo Push Token (like a special ID for our app), and another to note if a notification comes in.
We also create some special listeners to listen out for notifications. When a notification arrives, we update our boxes accordingly.
The useEffect
hook is like a helper that kicks in when our app starts up. It helps us register our device for push notifications and sets up the listeners for notifications. When our app stops, it cleans up everything nicely so it doesn't cause any problems later on.
<NavigationContainer>
<Stack.Navigator initialRouteName='Loader' screenOptions={{
headerShown: false
}}>
<Stack.Screen name="Loader" component={Loader} />
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="LoginScreen" component={LoginScreen} />
<Stack.Screen name="RegisterScreen" component={RegisterScreen} />
</Stack.Navigator>
</NavigationContainer>
Congratulations 🎉, we’ve successfully set up our app’s screens and made sure push notifications work smoothly for our users!
Let’s create screens for our app with corresponding functionalities.
For LoginScreen, we’re using hooks to manage the user’s email and password. It’s like having special storage where we keep track of what the user types in. Then, we have a function called handleLogin
.
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');const handleLogin = async () => {
try {
const response = await fetch('http://localhost:5000/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
await AsyncStorage.setItem('user', JSON.stringify(data.user)).then(async () => {
await AsyncStorage.getItem("pushToken").then((token) => {
if (token) {
fetch('http://localhost:5000/add-expo-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "token": token, "user_id": data.user.id})
})
.then((response) => response.json()).then((data) => {
console.log(data);
})
.catch((error) => {
console.error('Error:', error);
})
}
}).catch((error) => {
console.error('Error:', error);
});
navigation.navigate('Home');
});
} else {
alert(data.error);
}
} catch (error) {
console.error('Error:', error);
}
};
When the user tries to log in, this function takes charge. It talks to our server and sends the email and password the user entered.
If everything goes smoothly and our server gives us a green light, we save some details about the user using AsyncStorage (for session maintenance), which is like our app’s memory. Then, we check if we have a special token for sending push notifications. If we do, we also send that token to the server.
After all that, we guide the user to the home screen of our app. But if something goes wrong, like if the email or password is incorrect, we make sure to give the user a friendly heads-up.
<View style={styles.container}>
<Text style={styles.heading}>Login</Text>
<Text>Enter your Email and Password to login.</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry={true}
value={password}
onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={handleLogin}
>
<Text style={styles.btnText}>Login</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row', justifyContent: 'center', marginTop: 20 }}>
<Text style={{ textAlign: 'center' }}>Don't have an account?</Text>
<TouchableOpacity onPress={() => navigation.navigate('RegisterScreen')}>
<Text style={{ color: 'blue' }} >Register</Text>
</TouchableOpacity>
</View>
</View>
Now we’re creating a simple login screen ui for our app. Users can enter their email and password, then tap the “Login” button to log in. If they don’t have an account yet, they can tap “Register” to create one. Easy, right?
Just like login screen, we’re setting up the registration screen for our app. Users can enter their desired username, email, password, and confirm their password.
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');const handleRegister = async () => {
if (password !== confirmPassword) {
alert("Passwords don't match");
return;
}
try {
const response = await fetch('http://localhost/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "username":username, "email":email, "password":password })
});
const data = await response.json();
if (response.ok) {
navigation.navigate('LoginScreen');
} else {
alert(data.error);
}
} catch (error) {
console.error('Error:', error);
}
};
Once all the required information is filled in, they can tap the “Register” button to create an account which will transfer the control to the handleRegister function. It checks if there’s a mismatch between the password and confirm password fields, we’ll let the user know with a friendly alert message, otherwise after successful registration, users are directed to the login screen. If any errors occur during registration, we’ll inform the user with an alert message. For UI we can use the same approach as LoginScreen.
<View style={styles.container}>
<Text style={styles.heading}>Register</Text>
<Text>Fill Below Details to sign up.</Text>
<TextInput
style={styles.input}
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry={true}
value={password}
onChangeText={setPassword}
/>
<TextInput
style={styles.input}
placeholder="Confirm Password"
secureTextEntry={true}
value={confirmPassword}
onChangeText={setConfirmPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={handleRegister}
>
<Text style={styles.btnText}>Register</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row', justifyContent: 'center', marginTop: 20 }}>
<Text style={{ textAlign: 'center' }}>Already have an account?</Text>
<TouchableOpacity onPress={() => navigation.navigate('LoginScreen')}>
<Text style={{ color: 'blue' }} >Login</Text>
</TouchableOpacity>
</View>
</View>
It’s a straightforward process to get users signed up and ready to use our app!
In LoderScreen, we can use the useEffect hook to run some code when the component mounts. Inside, we check if there’s any user data stored in our app’s memory using AsyncStorage.
useEffect(() => {
(async () => {
await AsyncStorage.getItem('user').then((data) => {
if (data) {
navigation.navigate('Home');
} else {
navigation.navigate('LoginScreen');
}
}).catch((error) => {
console.error('Error:', error);
})
})();
}, []);
If we find user data, we redirect the user to the home screen. But if there’s no user data, we guide them to the login screen and in the return block we can simply show a loader animation.
<View style={styles.container}>
<ActivityIndicator size="large" color="black" />
</View>
It’s a simple way to ensure that users are directed to the right place based on their login status.
For this screen, we’re using the useState hook to manage the state of our user data and Expo push token. Initially, we set the user’s username to “Guest” until they log in and provide their details. With the help of useEffect hook, we asynchronously fetch the user data stored in AsyncStorage when the component mounts. If user data exists, we update the user’s details accordingly.
const [user, setUser] = useState({ username: 'Guest' });
const [expoPushToken, setExpoPushToken] = useState('');useEffect(() => {
const fetchUser = async () => {
try {
const data = await AsyncStorage.getItem('user');
if (data) {
setUser(JSON.parse(data));
}
} catch (error) {
console.error('Error:', error);
}
};
fetchUser();
}, []);
const handleSendNotification = async () => {
await AsyncStorage.getItem('pushToken').then((data) => {
if (data) {
setExpoPushToken(data);
fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: expoPushToken,
sound: 'default',
title: `Hello ${user.username}!`,
body: 'This is a test notification',
data: { someData: 'goes here' },
}),
})
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson);
})
.catch((error) => {
console.error('Error:', error);
}
)
}
})
}
const handleLogout = async () => {
try {
await AsyncStorage.removeItem('user');
await AsyncStorage.removeItem('pushToken');
navigation.navigate('LoginScreen');
}
catch (error) {
console.error('Error:', error);
}
}
We’ve implemented functionality for sending notifications with the handleSendNotification function. This involves fetching the Expo push token from AsyncStorage and sending a test notification to the user.
For convenience, we’ve also included a handleLogout function. This allows users to log out seamlessly, removing their data from AsyncStorage and navigating them back to the login screen. For the UI we can use the same approach as we used before,
<View style={styles.container}>
<Text style={styles.heading}>Welcome {user.username}</Text>
<Text>You will be able to receive push notifications here.</Text>
<TouchableOpacity style={styles.button} onPress={() => handleSendNotification()}>
<Text>Send Push Notification</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={() => handleLogout()}>
<Text>Logout</Text>
</TouchableOpacity>
</View>
🎉 Congratulations on reaching the Home Screen! With this frontend section complete, we’re now transitioning to backend implementation.
Let’s power up our app with server-side magic to handle notifications, database, and more! 🚀
We’ve opted for Flask to power our backend because of its simplicity and flexibility in building web applications. With Flask, we can quickly set up routes, handle HTTP requests, and integrate with various libraries to extend functionality.
app = Flask(__name__)
CORS(app)DATABASE_URL = "DB_CONNECTION_STRING"
app.config["SQLALCHEMY_DATABASE_URI"] = DATABASE_URL
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
Note: Replace the DATABASE_URL
variable with your own database connection string.
This snippet configures our backend using Flask and CORS, ensuring smooth communication between the frontend and backend. We also set up our database connection using SQLAlchemy, enabling effective data management with Python.
Models
Now let’s dive into creating models using SQLAlchemy for data storage.
Models are like the backbone of our database interactions, simplifying tasks with Python. Each model reflects a database table, defining its fields and connections.
For our app, we have created two models,
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), nullable=False)
email = db.Column(db.String(100), unique=True, nullable=False)
password = db.Column(db.String(100), nullable=False)
expo_tokens = db.relationship("ExpoToken", backref="user", lazy=True)class ExpoToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
token = db.Column(db.String(200), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
User Model: Represents app users, featuring attributes like ID, username, unique email, and password. It establishes a relationship with ExpoToken for managing push tokens.
ExpoToken Model: Handles Expo push tokens associated with users, with fields for ID, token, and a user ID link.
To create these tables, within our app’s context, we execute:
with app.app_context():
db.create_all()
Now, it’s time to set up the endpoints for our APIs, so our app can talk to the server without any hiccups.
For handling user authentication, we’ve got two endpoints:
@app.route("/register", methods=["POST"])
def register():
data = request.get_json()
user = User(
username=data["username"], email=data["email"], password=data["password"]
)
db.session.add(user)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
return jsonify({"error": "User already exists"}), 400
return jsonify({"message": "User created successfully"}), 201
Registration Endpoint: Handles user registration. Parses user data from the request and creates a new user in the database. If the user already exists, it returns an error; otherwise, it confirms successful registration.
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
user = User.query.filter_by(email=data["email"], password=data["password"]).first()
if user is None:
return jsonify({"error": "Invalid credentials"}), 400
user = {"id":user.id,"username":user.username,"email":user.email}
return jsonify({"message": "Login successful","user":user}), 200
Login Endpoint: Manages user login. Retrieves user credentials from the request and checks if they match any existing user. If the credentials are valid, it returns a success message along with user details; otherwise, it indicates invalid credentials.
For saving expo to simply register the device for notification,
@app.route("/add-expo-token", methods=["POST"])
def add_expo_token():
data = request.get_json()
user = User.query.filter_by(id=data["user_id"]).first()
if user is None:
return jsonify({"error": "User not found"}), 400
expo_token = ExpoToken(token=data["token"], user_id=user.id)
db.session.add(expo_token)
db.session.commit()
return jsonify({"message": "Expo token added successfully"}), 201
Above endpoint adds Expo push tokens for users. It extracts JSON data from a POST request, finds the user by ID, and associates the token with the user in the database. If successful, it returns a message confirming the token addition.
To send a push notification to a device via the Expo push notification service, we can utilize a function like this:
def send_notification(token, msg, body):
url = "https://exp.host/--/api/v2/push/send"
headers = {"Content-Type": "application/json"}
data = {"to": token, "title": msg, "body": body}
requests.post(url, headers=headers, data=json.dumps(data))
Above function constructs a POST request to the Expo server endpoint (https://exp.host/--/api/v2/push/send
) with the provided token, message title, and body. The request includes headers specifying the content type as JSON, and the data is serialized to JSON format before being sent.
To send notifications using stored tokens:
@app.route("/send-notifications", methods=["GET","POST"])
def send_notifications():
if request.method == "GET":
return render_template("index.html")
msg = request.form.get("title")
body = request.form.get("body") if not msg or not body:
return jsonify({"success": False, "message": "Missing required fields in the form"}), 400
expo_tokens = ExpoToken.query.all()
if not expo_tokens:
return jsonify(
{
"success": False,
"message": "No Expo tokens found for the provided user ID",
}
), 404
for token in expo_tokens:
send_notification(token.token, msg, body)
time.sleep(2)
return jsonify(
{"success": True, "message": "Notification sending process started"}
), 200
The above endpoint handles requests to send push notifications. It checks for required fields in the request, retrieves Expo tokens from the database, and sends notifications to each token with a 2-second delay between them using the send_notification
function. Finally, it responds with a message confirming the notification sending process. Additionaly, we can use below form to send notification to every user registered in our app.
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-semibold mb-4 text-center">Push Notification Demo</h1>
<form action="/send-notifications" method="POST">
<div class="mb-4">
<label for="title" class="block text-gray-700 font-semibold mb-2">Message Title:</label>
<input type="text" id="title" name="title" class="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring focus:ring-blue-300" placeholder="Enter message title" required>
</div>
<div class="mb-4">
<label for="body" class="block text-gray-700 font-semibold mb-2">Message Body:</label>
<textarea id="body" name="body" rows="4" class="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring focus:ring-blue-300" placeholder="Enter message body" required></textarea>
</div> <div class="text-right">
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300">Send</button>
</div>
</form>
</div>
Great job on finishing the coding part. Our app is now ready for use in the development environment.
We can use our app by running below command in our terminal,
npx expo start
Let’s set up our EAS configurations to prepare for building our APK. For a production-ready app, we’ll integrate Firebase. Note that APIs should be deployed on Heroku/AWS to use it after building the APK and endpoints in expo project should be modified accordingly.
Firebase Configuration
First, create a new project in your Firebase console:
Then, select “Android” in your project dashboard:
Enter the package name matching the one in your app.json file:
Download the google-services.json configuration file and place it in your project directory for integrating Firebase services into your app:
Don’t forget to update app.json to include a reference to this file, this step is necessary for configuring Firebase services in your app correctly.
Next, enable the Cloud Messaging API (Legacy) in your Firebase project settings:
Copy this key and paste it into your Expo project’s credentials section under Service Credentials.
Building App APK
Now, let’s build an Android preview version of our project using EAS CLI:
npx eas build -p android --profile preview
While the APK is being built, you’ll have some time to spare. Why not take a break and enjoy a cup of tea? Keep an eye on the updates in the terminal, and before you know it, your APK will be ready for download!
Congratulations! 🎉 You’ve successfully crafted a production-ready Expo app complete with backend functionalities, Firebase integration for authentication, a robust database system, and seamless push notification capabilities. Your app is now equipped to engage and delight users, bringing your vision to life with ease and efficiency!
If you’re curious to dig into the details of this project and explore further, hop on over to the GitHub repository. Have fun coding!