Back to home.

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:00
by Angelo Reale
tags: english, javascript, realm, mongodb, atlas

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.