Store Push Notification keys on a React Native app at no cost using MongoDB App Services (Custom HTTP Endpoints from Data API) and Cloudflare Workers
Sassari on 2022-08-21T00:00:00.000+02:00by Angelo Reale
You might be on your journey to create your first mobile app, or might just be trying to improve an app you currently have. Maybe that’s not a commercial app, and you’re wondering: how can I store unique push notification keys that users generate on their own devices for sending push notifications to them at a low cost? Or better yet, at no cost?
I also asked myself the same, since the App I’m creating is just meant to be a hobbyist radio player for me and my friends, I didn’t want to spin up a fully fledged server just to store an array of a couple of strings like
1ExponentPushToken[XXXXXXXXX]
.This is when I had the idea of using MongoDB App Services (disclaimer: I work at MongoDB but I don’t speak on behalf of the company) to create a simple HTTPS endpoint I could fetch from and create/read those push notification tokens.
If your app is not commercial and doesn’t have a lot of traffic (like mine) MongoDB App Services’ free tier can be rather generous (1M requests per month!), and I just needed to hit the create endpoint once in a user’s lifetime (the generated key is immutable over apps versions and re-installations), as well as hit the read endpoint every so often, when I’d need to actually send push notifications.
I was thinking, I didn’t want to expose the secrets to authenticate/connect to my MongoDB cluster out in the wild, so I’ve used Cloudflare’s Workers to intercept traffic on a specific wildcard endpoint of mine
1example.com/api/save_push_tokens?token=*
and do some Serverless request to the actual MongoDB App Services HTTPS Endpoint and add the token to my document.MongoDB App Services need authentication, so I’ve generated an 1api-token
for my Cloudflare layer, and used this method.Also, the MongoDB’s Data API functions to create/read from the document are pretty darn simple, and I’ll share here:
1Create:
2exports = function({ query }, response) {
3 const {token} = query;
4
5 const doc = context.services.get("mongodb-atlas").db("YOUR_DB").collection("YOUR_COLLECTION").updateOne({}, {
6 "$addToSet": {
7 "tokens": token,
8 }
9 });
10
11 return doc;
12};
1Read:
2exports = async function({ query, headers, body}, response) {
3 const doc = await context.services.get("mongodb-atlas").db("YOUR_DB").collection("YOUR_COLLECTION").findOne({});
4 return doc.tokens;
5};
Simple as that.
Then, on the Cloudflare side of things:
1addEventListener("fetch", (event) => {
2 event.respondWith(
3 handleRequest(event.request).catch(
4 (err) => new Response(err.stack, {
5 status: 500
6 })
7 )
8 );
9});
10/**
11* Many more examples available at:
12* https://developers.cloudflare.com/workers/examples
13* @param {Request} request
14* @returns {Promise<Response>}
15*/
16async function handleRequest(request) {
17 const apiKey = 'YOUR_API_KEY'
18 let query
19 try {
20 query = request.url.split("?data=")[1].split("&")[0]
21 } catch (e) {
22 return respond({
23 error: e
24 })
25 }
26 if (!query.includes("ExponentPushToken")) return respond({
27 error: 'WRONG!'
28 })
29 const settings = {
30 method: "GET",
31 headers: {
32 'api-key': apiKey
33 }
34 }
35
36 const endpoint = `https://YOUR_MONGODB_DATA_API_ENDPOINT/?TOKEN=${encodeURIComponent(query)}`;
37 const res = await fetch(endpoint, settings)
38 return respond({
39 query, ok: res.ok
40 })
41}
42async function respond(payload) {
43 let response = new Response(JSON.stringify(payload), {
44 status: 200,
45 statusText: 'OK'
46 })
47
48 return response
49}
If you’re curious about the React Native side of things, I don’t mind also sharing some code (please highlight to the usage of
1@react-native-async-storage/async-storage
on the user’s mobile so we don’t blow up Cloudflare’s and MongoDB’s free tier limits!):1import { StyleSheet, Text, View, Platform, Linking } from 'react-native';
2import Player from './components/Player';
3import { Provider } from 'react-redux';
4import { legacy_createStore as createStore } from 'redux'
5import playerReducer from './redux/PlayerReducer';
6import { useEffect, useRef, useState } from 'react';
7import * as Device from 'expo-device';
8import * as Notifications from 'expo-notifications';
9import axios from 'axios';
10import AsyncStorage from '@react-native-async-storage/async-storage';
11
12const store = createStore(playerReducer);
13
14export default function App() {
15 const [expoPushToken, setExpoPushToken] = useState(undefined)
16 const responseListener = useRef();
17
18 // store key
19 const storeLocalPush = async (value) => {
20 try {
21 await AsyncStorage.setItem('@push', value)
22 } catch (e) {
23 console.error(e)
24 }
25 }
26
27 const getLocalPush = async () => {
28 try {
29 const value = await AsyncStorage.getItem('@push')
30 if(value !== null) {
31 return value
32 }
33 } catch(e) {
34 console.error(e)
35 }
36 }
37
38 // PUSH
39 const registerForPushNotificationsAsync = async () => {
40 if (Device.isDevice) {
41 const { status: existingStatus } = await Notifications.getPermissionsAsync();
42 let finalStatus = existingStatus;
43 if (existingStatus !== 'granted') {
44 const { status } = await Notifications.requestPermissionsAsync();
45 finalStatus = status;
46 }
47 if (finalStatus !== 'granted') {
48 alert('Failed to get push token for push notification!');
49 return;
50 }
51 const token = (await Notifications.getExpoPushTokenAsync({experienceId:'YOUREXPERIENCEID'})).data;
52 setExpoPushToken(token);
53 } else {
54 alert('Must use physical device for Push Notifications');
55 }
56
57 if (Platform.OS === 'android') {
58 Notifications.setNotificationChannelAsync('default', {
59 name: 'default',
60 importance: Notifications.AndroidImportance.MAX,
61 vibrationPattern: [0, 250, 250, 250],
62 lightColor: '#FF231F7C',
63 });
64 }
65 };
66
67 // store push
68 const storePush = async () => {
69 const local = await getLocalPush()
70 if(!local || local !== expoPushToken) {
71 const res = await axios.get(`https://YOUR_CLOUDFARE_READY_SERVICE_WORKER?data=${expoPushToken}`)
72 const ok = Boolean(res.data.ok)
73 if (ok) await storeLocalPush(expoPushToken)
74 }
75 }
76
77 //open site
78 const openSite = async (url) => {
79 try {
80 await Linking.openURL(url);
81 } catch (e) {
82 console.error(e)
83 }
84 }
85
86 useEffect(() => {
87 registerForPushNotificationsAsync()
88
89 responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
90 const url = response.notification.request.content.data.url
91 if (!url) return
92 try {
93 openSite(url)
94 } catch(e) {
95 console.error(e)
96 }
97 });
98 }, [])
99
100 useEffect(() => {
101 if(expoPushToken) {
102 storePush()
103 }
104 }, [expoPushToken])
105
106 return (
107 <Provider store={store}>
108 <View style={styles.container}>
109 <Player />
110 </View>
111 </Provider>
112 );
113}
114
115const styles = StyleSheet.create({
116 container: {
117 flex: 1,
118 backgroundColor: '#fff',
119 alignItems: 'center',
120 justifyContent: 'center',
121 },
122});
As for the actual sending of push notifications, I’d do it every so often, so all I need is a simple node script:
1const fetch = require('node-fetch')
2const apiKey = 'YOUR_API_KEY'
3const endpoint = `YOUR_DATA_API_ENDPOINT`
4
5const title = `We're live!`
6const body = 'Get into the groove from the app.'
7const data = { url: '' }
8
9const sendPushNotification = async (expoPushToken) => {
10 const message = {
11 to: expoPushToken,
12 sound: 'default',
13 title,
14 body,
15 data
16 };
17
18 await fetch('https://exp.host/--/api/v2/push/send', {
19 method: 'POST',
20 headers: {
21 Accept: 'application/json',
22 'Accept-encoding': 'gzip, deflate',
23 'Content-Type': 'application/json',
24 },
25 body: JSON.stringify(message),
26 });
27}
28
29const getKeys = async () => {
30 const settings = {
31 method: "GET",
32 headers: {
33 'api-key': apiKey
34 }
35 }
36 const res = await fetch(endpoint, settings)
37
38 if (res.ok) {
39 const data = await res.json()
40 console.log({ data })
41 return data
42 }
43 return []
44}
45
46const sendNotifications = async () => {
47 const keys = await getKeys()
48 keys.forEach((token) => {
49 sendPushNotification(token)
50 })
51}
52
53sendNotifications()
And, voilà!
Now you can start pestering your friends who have your app installed with push notifications for free! :)
My app’s first push notification! If you’re curious, visit purizu.com.