Push Notifications: Why You Want Them & How to Build Them Into Your App (Part 2 of 2)

In part 1 of this series, we discussed what push notifications are and how they can be a useful addition to your marketing and communication strategy. In part 2 of our push notification series, we're going to share how you can implement push notification functionality in your app.

(Note: Depending on your skill level and experience, it may be beneficial to brush up on how to scaffold a new custom mobile app with Crowdbotics prior to jumping into this tutorial.)

How to Add a Push Notification Module to Your Mobile App

If you're looking to add push notification functionality to your app, the Crowdbotics App Builder offers a push notification integration module for mobile apps. This module uses OneSignal to provide push notifications capabilities for your Android or iOS app, and it includes the OneSignal SDK for React Native.

Before we get started, let's create a demo app with the App Builder. When you log into your account at app.crowdbotics.com, you'll be welcomed by the dashboard screen shown below.

A screenshot of your user dashboard in the Crowdbotics platform.

To create a demo app for our purposes, click on the Create App button. After clicking the button, you'll be redirected to the create app page. Once here, enter the name of your app in the "Application name" field, select the Mobile App option in the middle, and then press the Create App button at the bottom of the page.

Once you're on the Storyboard screen, select Modules from the list on the right panel, and then add the Push Notifications-1 module by dragging and dropping it onto your Storyboard. Click the Save button on the top right.

At this point, the module has been added to your RAD stack app! To access the application source code, Crowdbotics has created a GitHub repository, and a screenshot of this repository can be seen below.

You will have to download or clone the repository because there is a manual step required to integrate the OneSignal push notification service in React Native.

After cloning the repository, open it inside your preferred IDE or text editor. The first thing you'll want to do is to install the dependencies listed in the package.json file. Open up a terminal window inside the IDE or your preferred terminal and execute the command yarn install.

Next, rename the file .env.template to .env. This file is used to store environment variables across the whole application. Following this, add an environment variable to it:

ONE_SIGNAL_APP_ID=

We will add its value later after we've configured a OneSignal app.

Getting Firebase Server Key

For this tutorial, we are going to integrate and test push notifications on an Android device. To set up the OneSignal app for an Android device, you need a server key and Sender ID from Firebase Cloud Messaging. To get them, make sure you have a Firebase account and that you are logged in.

After logging in, the Firebase project displays a dashboard screen. From the left menu bar, click on the Settings button, and open Project Settings. Next, click on the tab that says Cloud Messaging. Under this tab, you will find the section called "Project credentials" that contains two fields: Server key and Sender ID. Make sure you save these values as they will be required in the next section!

Configuring a OneSignal App

In this section, we are going to set up a OneSignal app to get its ID. The value of this ID will then be used in the RAD Stack app we set up to use this service. Before starting though, please make sure you have an account set up, that you're logged into OneSignal, and have access to your OneSignal dashboard.

OneSignal is a push notification service that allows your application to send out rich text notifications with images and/or buttons. It provides you with an interface to manage and compose notifications as well as analytics reports regarding the notifications users receive.

After logging in, you should be taken to the OneSignal Dashboard screen. On this screen, a list of all the applications you've created for OneSignal is displayed.

  • Click on the button New App/Website.
  • Enter the name of your app.
  • Then select Google Android (FCM) platform and click Next: Configure Your Platform.

Enter the Firebase server key and sender ID that you saved from the previous section and click Save & Continue.

In the following screen, select React Native/Expo as the SDK and click Save & Continue.

On the next screen, copy and paste the App ID provided by OneSignal into the .env file of your RAD stack app and click the button Done.

Here is how the .env file should look:

PROJECT_NAME="your-app-project-name"
ONE_SIGNAL_APP_ID=a1677df0-a9cb-41a7-8686-6d843b4d23f2

In your Crowdbotics app, click on the Settings tab from the left menu bar. Next, open the Environment Variable tab, and create a new variable with the name ONE_SIGNAL_APP_ID and input its ID as the value in both Mobile and API sections.

Building the App to Test Your Notification

For testing purposes, let's build an Android app from the RAD Stack app source code. For testing purposes, make sure you have an Android emulator set up or a device connected to your local development environment.

Before we build the app, we need to configure the Android app to include OneSignal SDK. Open the RAD Stack app's source code, go to the file android/app/build.gradle and add the following build script at the top of the file:

buildscript {
    repositories {
        maven { url 'https://plugins.gradle.org/m2/' } // Gradle Plugin Portal
    }
    dependencies {
        classpath 'gradle.plugin.com.onesignal:onesignal-gradle-plugin:[0.12.9, 0.99.99]'
    }
}

apply plugin: 'com.onesignal.androidsdk.onesignal-gradle-plugin'

Next, open a terminal window and execute the following command:

npx react-native run-android

On completing the build, an instance of the app on the Android device will be triggered. The example app below may be pretty bare-bones, but it will serve our purposes for this tutorial.

Using the Crowdbotics Push notifications module, you get the advantage of functions that comes with OneSignal SDK for React Native apps configured within the app.

import OneSignal from 'react-native-onesignal';
import { Platform, Alert } from 'react-native';
import { useState, useEffect } from 'react';
import { ONE_SIGNAL_APP_ID } from '@env';

const useOneSignal = () => {
  const [isSubscribed, setIsSubscribed] = useState(false);

  useEffect(() => {
    async function getDeviceState() {
      const deviceState = await OneSignal.getDeviceState();
      setIsSubscribed(deviceState.isSubscribed);
    }
    /* O N E S I G N A L   S E T U P */
    OneSignal.setAppId(ONE_SIGNAL_APP_ID);
    OneSignal.setLogLevel(6, 0);
    OneSignal.setRequiresUserPrivacyConsent(false);
    if (Platform.OS === 'ios') {
      OneSignal.promptForPushNotificationsWithUserResponse(response => {
        console.log('Prompt response:', response);
      });
    }
    /* O N E S I G N A L  H A N D L E R S */
    OneSignal.setNotificationWillShowInForegroundHandler(notifReceivedEvent => {
      console.log(
        'OneSignal: notification will show in foreground:',
        notifReceivedEvent
      );
      let notif = notifReceivedEvent.getNotification();
      const button1 = {
        text: 'Cancel',
        onPress: () => {
          notifReceivedEvent.complete();
        },
        style: 'cancel'
      };
      const button2 = {
        text: 'Complete',
        onPress: () => {
          notifReceivedEvent.complete(notif);
        }
      };
      Alert.alert('Complete notification?', 'Test', [button1, button2], {
        cancelable: true
      });
    });
    OneSignal.setNotificationOpenedHandler(notification => {
      console.log('OneSignal: notification opened:', notification);
    });
    OneSignal.setInAppMessageClickHandler(event => {
      console.log('OneSignal IAM clicked:', event);
    });
    OneSignal.addEmailSubscriptionObserver(event => {
      console.log('OneSignal: email subscription changed: ', event);
    });
    OneSignal.addSubscriptionObserver(event => {
      console.log('OneSignal: subscription changed:', event);
      setIsSubscribed(event.to.isSubscribed);
    });
    OneSignal.addPermissionObserver(event => {
      console.log('OneSignal: permission changed:', event);
    });
    getDeviceState();
  });

  return isSubscribed;
};

export default {
  title: 'Push Notifications',
  hook: useOneSignal
};

From the above code snippet, you can use the React hook useOneSignal to extend and create further functionalities.

try our app estimate calculator CTA image

Sending a Push Notification from OneSignal to Your RAD Stack App

From the OneSignal dashboard screen shown below, click the button New Push to compose a new push notification.

It will open a New Push Notification screen as shown below.

From this screen, you can configure a new one-time push notification or a campaign to send to your Android device. Let's compose one and test it out!

In the Audience section, select the option "Send to Subscribed Users". This will send a notification to any user who has your app installed.

In the Message section, you can compose a push notification message using fields like title, message, image, and other optional settings like the visibility of push notifications on the lock screen, altering the LED or accent color, setting up a priority, add a call-to-action button, etc.

Adding Call to Action buttons:

In the Delivery schedule section, you can select when to deliver the notification. There are many different options available to you—you can send your notification immediately, schedule it for a later time, or even send your notification at a more optimal time based on your subscribers' timezone. Using these options is incredibly helpful when it comes to creating a campaign for a targeted audience.

For the demo app, let's send a notification immediately and see how it is received on the Android device.

OneSignal also creates a sample view when you're composing a message so that you can see how a notification will appear on the devices you're sending it to (like this one for Android, below):

Once your message and settings are in place, click the button Review & Send to send out your notification.

Adding a Push Notification Module to a Django Backend

The backend part of the RAD Stack offered by the Crowdbotics builder is built in using Django framework.

To add the backend module, open your Crowdbotics app in the builder. On the Storyboard screen, select Modules from the right-side panel, and then add the "Push Notifications-2" module by dragging and dropping it on the Storyboard. Then, click the Save button.

This module uses OneSignal services to provide push notification services from the Django backend. OneSignal supports creating and sending notifications through a hosted server.

To make it work from the backend, you need the APP ID that is created when you set up a new OneSignal app.

Let's break down what are the requirements are to use this module on the Django backend. OneSignal REST API requires you to setup:

  • HTTP header
  • API_ROOT
  • APP_ID
  • And custom endpoints (depending on your app)

Crowdbotics' app builder generates all the code you need in the Django backend to make it work, and you will find these constants defined under the backend/modules/push-notifications/constants.py file.

# Requests config
JSON_HEADER = {"Content-Type": "application/json; charset=utf-8"}


def get_header(auth_key: str = None) -> Dict:
    header = JSON_HEADER
    if auth_key:
        header["Authorization"] = f"Basic {auth_key}"
    return header


# Endpoints
API_ROOT = "https://onesignal.com/api/v1"
NOTIFICATIONS_PATH = "/notifications"
NOTIFICATION_PATH = "/notifications/{id}"
NOTIFICATION_HISTORY_PATH = "/notifications/{id}/history"
DEVICES_PATH = "/players"
DEVICE_PATH = "/players/{id}"
EDIT_TAGS_PATH = "/apps/{app_id}/users/{user_id}"
NEW_SESSION_PATH = "/players/{id}/on_session"
NEW_PURCHASE_PATH = "/players/{id}/on_purchase"
CSV_EXPORT_PATH = "/players/csv_export"
SEGMENTS_PATH = "/apps/{app_id}/segments"
SEGMENT_PATH = "/apps/{app_id}/segments/{segment_id}"
VIEW_OUTCOMES_PATH = "/apps/{app_id}/outcomes"
APPS_PATH = "/apps"
APP_PATH = "/apps/{app_id}"

These constants are then used in a file called client.py in the same directory. This file houses everything you need to create, view or send notifications, and perform other related actions available with OneSignal REST API.

import requests
from requests import Response
from os.path import join

from .constants import (
    API_ROOT,
    NOTIFICATIONS_PATH,
    NOTIFICATION_PATH,
    NOTIFICATION_HISTORY_PATH,
    DEVICES_PATH,
    DEVICE_PATH,
    EDIT_TAGS_PATH,
    NEW_SESSION_PATH,
    NEW_PURCHASE_PATH,
    CSV_EXPORT_PATH,
    SEGMENTS_PATH,
    SEGMENT_PATH,
    VIEW_OUTCOMES_PATH,
    APPS_PATH,
    APP_PATH,
    get_header,
)


class Client:
    def __init__(
        self,
        app_id: str,
        rest_api_key: str,
        user_auth_key: str = "",
        api_root: str = API_ROOT,
    ):
        self.app_id = app_id
        self.rest_api_key = rest_api_key
        self.user_auth_key = user_auth_key
        self.api_root = api_root

    def _path(self, path: str, **kwargs) -> str:
        return join(self.api_root, path.format(**kwargs))

    def create_notification(self, body: Dict) -> Response:
        """
        Sends notifications to your users
        https://documentation.onesignal.com/reference/create-notification
        :param body: Notification parameters (Segments, Filters, User ID).
        :return: Response
        """
        header = get_header(self.rest_api_key)
        path = self._path(NOTIFICATIONS_PATH)
        payload = {"app_id": self.app_id, **body}
        return requests.post(path, headers=header, data=payload)

    def cancel_notification(self, id: int) -> Response:
        """
        Stop a scheduled or currently outgoing notification
        https://documentation.onesignal.com/reference/cancel-notification
        :param id: Notification id
        :return: Response
        """
        header = get_header(self.rest_api_key)
        path = self._path(NOTIFICATION_PATH, id=id)
        payload = {"app_id": self.app_id}
        return requests.delete(path, headers=header, params=payload)

    def view_apps(self) -> Response:
        """
        View the details of all of your current OneSignal apps
        https://documentation.onesignal.com/reference/view-apps-apps
        :return: Response
        """
        header = get_header(self.user_auth_key)
        path = self._path(APPS_PATH)
        return requests.get(path, headers=header)

    def view_app(self, app_id: int) -> Response:
        """
        View the details of a single OneSignal app
        https://documentation.onesignal.com/reference/view-an-app
        :param app_id: App id
        :return: Response
        """
        header = get_header(self.user_auth_key)
        path = self._path(APP_PATH, app_id=app_id)
        return requests.get(path, headers=header)

    def create_app(self, body: Dict) -> Response:
        """
        Creates a new OneSignal app
        https://documentation.onesignal.com/reference/create-an-app
        :param body: App parameters
        :return: Response
        """
        header = get_header(self.user_auth_key)
        path = self._path(APPS_PATH)
        payload = body
        return requests.post(path, headers=header, data=payload)

    def update_app(self, app_id: int, body: Dict) -> Response:
        """
        Updates the name or configuration settings of an existing OneSignal app
        https://documentation.onesignal.com/reference/update-an-app
        :param app_id: App id
        :param body: App parameters
        :return: Response
        """
        header = get_header(self.user_auth_key)
        path = self._path(APP_PATH, app_id=app_id)
        payload = body
        return requests.post(path, headers=header, data=payload)

    def view_devices(self, limit: int, offset: int) -> Response:
        """
        View the details of multiple devices in one of your OneSignal apps
        https://documentation.onesignal.com/reference/view-devices
        :param limit: How many devices to return. Max is 300. Default is 300
        :param offset: Result offset. Default is 0. Results are sorted by id;
        :return: Response
        """
        header = get_header(self.rest_api_key)
        path = self._path(DEVICES_PATH)
        payload = {"app_id": self.app_id, "limit": limit, "offset": offset}
        return requests.get(path, headers=header, params=payload)

    def view_device(self, id: int) -> Response:
        """
        View the details of an existing device in one of your OneSignal apps
        https://documentation.onesignal.com/reference/view-device
        :param id: Player's OneSignal ID
        :return: Response
        """
        path = self._path(DEVICE_PATH, id=id)
        payload = {"app_id": self.app_id}
        return requests.get(path, headers=get_header(), params=payload)

    def add_device(self, body: Dict) -> Response:
        """
        Register a new device to one of your OneSignal apps
        Warning: Don't use this.
        This API endpoint is designed to be used from our open source Mobile and
        Web Push SDKs. It is not designed for developers to use it directly,
        unless instructed to do so by OneSignal support.
        https://documentation.onesignal.com/reference/add-a-device
        :param body: Device parameters
        :return: Response
        """
        path = self._path(DEVICES_PATH)
        payload = body
        payload["app_id"] = self.app_id
        return requests.post(path, headers=get_header(), data=payload)

    def edit_device(self, id: int, body: Dict) -> Response:
        """
        Update an existing device in one of your OneSignal apps
        https://documentation.onesignal.com/reference/edit-device
        :param id: Required - The device's OneSignal ID
        :param body: Device parameters
        :return: Response
        """
        path = self._path(DEVICE_PATH, id=id)
        payload = body
        payload["app_id"] = self.app_id
        return requests.put(path, headers=get_header(), data=payload)

    def edit_tags(self, user_id: int, body: Dict) -> Response:
        """
        Update an existing device's tags in one of your OneSignal apps using the
        External User ID.
        https://documentation.onesignal.com/reference/edit-tags-with-external-user-id
        :param user_id: Required: The External User ID mapped to the device
        record in OneSignal. Must be actively set on the device to be updated.
        :param body: Tags
        :return: Response
        """
        path = self._path(EDIT_TAGS_PATH, app_id=self.app_id, user_id=user_id)
        return requests.put(path, headers=get_header(), data=body)

    def new_session(self, id: int, body: Dict) -> Response:
        """
        Update a device's session information
        https://documentation.onesignal.com/reference/new-session
        :param id: Player's OneSignal ID
        :param body: Body parameters
        """
        path = self._path(NEW_SESSION_PATH, id=id)
        return requests.post(path, headers=get_header(), data=body)

    def new_purchase(self, id: int, body: Dict) -> Response:
        """
        Track a new purchase in your app
        https://documentation.onesignal.com/reference/new-purchase
        :param id: Player's OneSignal ID
        :param body: Body parameters
        """
        path = self._path(NEW_PURCHASE_PATH, id=id)
        return requests.post(path, headers=get_header(), data=body)

    def csv_export(self, body: Dict) -> Response:
        """
        Generate a compressed CSV export of all of your current user data
        https://documentation.onesignal.com/reference/csv-export
        :param body: CSV Export parameters
        """
        header = get_header(self.rest_api_key)
        path = self._path(CSV_EXPORT_PATH)
        params = {"app_id": self.app_id}
        return requests.post(path, headers=header, params=params, data=body)

    def view_notification(self, id: int) -> Response:
        """
        View the details of a single notification and outcomes associated with it
        https://documentation.onesignal.com/reference/view-notification
        :param id: Required - Notification ID
        """
        header = get_header(self.rest_api_key)
        path = self._path(NOTIFICATION_PATH, id=id)
        params = {"app_id": self.app_id}
        return requests.get(path, headers=header, params=params)

    def view_notifications(
        self, limit: int = 50, offset: int = 0, kind: int = None
    ) -> Response:
        """
        View the details of multiple notifications
        https://documentation.onesignal.com/reference/view-notifications
        :param limit: Optional. How many notifications to return. Max is 50.
        Default is 50
        :param offset: Optional. Page offset. Default is 0. Results are sorted by
        queued_at in descending order. queued_at is a representation of the time
        that the notification was queued at.
        :param kind: Optional. Kind of notifications returned. Default (not set)
        is all notification types.
        0 - Dashboard only
        1 - API only
        3 - Automated only
        """
        header = get_header(self.rest_api_key)
        path = self._path(NOTIFICATIONS_PATH)
        params = {"app_id": self.app_id, "limit": limit, "offset": offset}
        if kind:
            params["kind"] = kind
        return requests.get(path, headers=header, params=params)

    def view_notification_history(self, notification_id: int, body: Dict) -> Response:
        """
        View the devices sent a message - OneSignal Paid Plan Required
        https://documentation.onesignal.com/reference/notification-history
        :param notification_id: The "id" of the message found in the creation
        notification POST response, View Notifications GET response, or URL
        within the Message Report.
        :param body: Body params
        """
        header = get_header(self.rest_api_key)
        path = self._path(NOTIFICATION_HISTORY_PATH, id=notification_id)
        payload = body
        payload["app_id"] = self.app_id
        return requests.post(path, headers=header, data=payload)

    def create_segments(self, body: Dict) -> Response:
        """
        Create segments visible and usable in the dashboard and API - Required:
        OneSignal Paid Plan
        https://documentation.onesignal.com/reference/create-segments
        :param body: Body params
        """
        header = get_header(self.rest_api_key)
        path = self._path(SEGMENTS_PATH, app_id=self.app_id)
        return requests.post(path, headers=header, data=body)

    def delete_segments(self, segment_id: int) -> Response:
        """
        Delete segments (not user devices) - Required: OneSignal Paid Plan
        https://documentation.onesignal.com/reference/delete-segments
        :param segment_id: The segment_id can be found in the URL of the segment
        when viewing it in the dashboard.
        """
        header = get_header(self.rest_api_key)
        path = self._path(SEGMENT_PATH, app_id=self.app_id, segment_id=segment_id)
        return requests.delete(path, headers=header)

    def view_outcomes(
        self,
        outcome_names: str,
        outcome_names_array: str = None,
        outcome_time_range: str = None,
        outcome_platforms: str = None,
        outcome_attribution: str = None,
    ) -> Response:
        """
        View the details of all the outcomes associated with your app
        https://documentation.onesignal.com/reference/view-outcomes
        :param outcome_names: Required
        Comma-separated list of names and the value (sum/count) for the returned outcome data.
        Note: Clicks only support count aggregation.

        For out-of-the-box OneSignal outcomes such as click and session duration, please use the “os” prefix with two underscores. For other outcomes, please use the name specified by the user.

        Example:os__session_duration.count,os__click.count,CustomOutcomeName.sum
        :param outcome_names_array: Optional
        If outcome names contain any commas, then please specify only one value at a time.

        Example: outcome_names[]=os__click.count&outcome_names[]=Sales, Purchase.count
        where “Sales, Purchase” is the custom outcomes with a comma in the name.
        :param outcome_time_range: Optional
        Time range for the returned data. The values can be 1h (for the last 1 hour data), 1d (for the last 1 day data), or 1mo (for the last 1 month data).

        Default is 1h if the parameter is omitted.
        :param outcome_platforms: Optional
        Platform id. Refer device's platform ids for values.

        Example:
        outcome_platform=0 for iOS
        outcome_platform=7,8 for Safari and Firefox

        Default is data from all platforms if the parameter is omitted.
        :param outcome_attribution: Optional
        Attribution type for the outcomes. The values can be direct or influenced or unattributed.

        Example: outcome_attribution=direct

        Default is total (returns direct+influenced+unattributed) if the parameter is omitted.
        """
        header = get_header(self.rest_api_key)
        params = {"outcome_names": outcome_names}
        if outcome_names_array:
            params["outcome_names_array"] = outcome_names_array
        if outcome_time_range:
            params["outcome_time_range"] = outcome_time_range
        if outcome_platforms:
            params["outcome_platforms"] = outcome_platforms
        if outcome_attribution:
            params["outcome_attribution"] = outcome_attribution
        path = self._path(VIEW_OUTCOMES_PATH, app_id=self.app_id)
        return requests.get(path, headers=header, params=params)

Conclusion

With this article, we demonstrated how you can set up and integrate OneSignal using a module in the Crowdbotics Marketplace, and we successfully tested this method on an Android device. Setting up the same module on iOS requires an iOS Push Certificate and a real iOS device. Due to restrictions, it doesn't currently work on the iOS simulator. For more information on how to set up on iOS check out OneSignal's documentation here.

Now you have a better understanding of how to leverage the Crowdbotics App Builder by using pre-defined modules! At Crowdbotics, we offer both a low-code Crowdbotics App Builder and managed app development by expert PMs and engineers. The Crowdbotics App Builder includes automated and drag-n-drop visual scaffolding tools, pre-built modules and templates, and complete flexibility to customize and modify source code using GitHub.

If you'd like for us to build this functionality into your app for you, we can do that too—get in touch with us today for an estimated quote and timeline!

Originally published:

July 2, 2021