The guide to implement Push Notifications with React Native, Expo and AWS Amplify
Tags: aws, amplify, expo, reactnative
Published: 2020-01-28 19:58:55

If you build an app you want to engage your customers so that they use your app as much as possible and also in a way that it brings value to them. Therefor you have to set up push notifications. In this Guide I will show you how that can be possible with some great tools. We are going to build a simple app that demonstrates the push notifications. 

There are some powerful tools out there to make app development as easy as possible. 

###React Native
React Native is great because you can just code in javascript and compile to IOS and Android. So less costs and a faster development cycle. 

###Expo
Expo works out well because it is packaged with a lot features which you need and you can develop for IOS and Android without installing any other tools or even without having a Mac.

###AWS Amplify
AWS Amplify is the glue between these tools and the backend. Here you can also use Javascript for setting up your API's, storage, authentication and authorization, database, data store and more.

##Push Notifications
When you will dive deeper in these tool sets and setting up your architecture you will start loving them and when you are coming to a point where you want to use push notifications you have to say good bye to Expo because AWS Amplify doesn't come with a solution out of the box where you can use push notifications without ejecting. We have some luck that AWS has more tools that can support push notifications without ejecting. 

##Architecture
We will use AWS Amplify, AWS Pinpoint, AWS Lambda, AWS DynamoDB and Expo Push Notifications Server to accomplish it.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lrvms4wumbx9exviipix.png)

In this example we will make a message in our app. We will create a scheduled Pinpoint Campaign via the **AWS Lambda Pinpoint** (This can also be an immediate campaign). As soon as the campaign will be triggered, first a hook will kick off. This hook will send campaign data to the **AWS Lambda Push Notifications**. We will prepare the push notifications message and send it via the Expo Server SDK to Expo. This will send the push message to the client, to your app. In the meanwhile the campaign data has been send back to AWS Pinpoint to send it further to others channels (if you want).  You are going to set up an email channel so you can see how it works

## Getting Started

I will use NPM but course you can also you Yarn.


### Set up React Native

First, we'll create the React Native application we'll be working with.


```
$ npx expo init pushApp

> Choose a template: blank

$ cd pushApp

$ npm install aws-amplify aws-amplify-react-native

```

### Set up AWS Amplify
We first need to have the AWS Amplify CLI installed. The Amplify CLI is a command line tool that allows you to create & deploy various AWS services.

To install the CLI, we'll run the following command:

```
$ npm install -g @aws-amplify/cli

```

Next, we'll configure the CLI with a user from our AWS account:

```
$ amplify configure

```


>For a video walkthrough of the process of configuring the CLI, [click](https://www.youtube.com/watch?v=fWbM5DLh25U)


Now we can now initialize a new Amplify project from within the root of our React Native application:

```
$ amplify init
```

Here we'll be guided through a series of steps:
* Enter a name for the project: **amplifypushapp** (or your preferred project name)
* Enter a name for the environment: **dev** (use this name, because we will reference to it)
* Choose your default editor: **Visual Studio Code** (or your text editor)
* Choose the type of app that you're building: **javascript**
* What javascript framework are you using: **react-native**
* Source Directory Path: **/**
* Distribution Directory Path: **build**
* Build Command: **npm run-script build**
* Start Command: **npm run-script start**
* Do you want to use an AWS profile? **Y**
* Please choose the profile you want to use: **YOUR_USER_PROFILE**
* 
Now, our Amplify project has been created & we can move on to the next steps.


### Add Graphql to your project

Your React Native App is up and running and AWS Amplify is configured. Amplify comes with different services which you can use to enrich your app. We are focussing mostly on the API service. So let’s add an API.

```
Amplify add api
```

These steps will take place:
* Select **Graphql**
* Enter a name for the API: **pushAPI** (your preferred API name)
* Select an authorisation type for the API: **Amazon Cognito User Pool** ( Because we are using this app with authenticated users only, but you can choose other options)
* Select at do you want to use the default authentication and security configuration: **Default configuration**
* How do you want users to be able to sign in? **Username** (with this also the AWS Amplify Auth module will be enabled)
* Do you want to configure advanced settings? **No, I am done.**
* Do you have an annotated GraphQL schema? **n**
* Do you want a guided schema creation?: **n**
* Provide a custom type name: **user**

You API and your schema definition have been created now. You can find it in you project directory:

``` Amplify > backend > api > name of your api ```

The @model directive will create a DynamoDB for you. There are more directives possible, for the full set look at the [AWS Amplify docs](https://aws-amplify.github.io/docs/cli-toolchain/graphql#directives). 

### Configure AWS Email Service
Log into the console and go to the SES service. Then follow these instructions to configure an e-mail address and get it activated. You will need to use this email address later as the email address where you send the mails from.

https://docs.aws.amazon.com/ses/latest/DeveloperGuide/setting-up-email.html

###Add analytics to your project

We are going to add analytics to your project because for now this is the easiest way to set up access to pinpoint from your pinpoint function. You can also modify the cloudformation template to do so. 

```
Amplify add analytics
```

Complete the steps with your information.


###Add Functions to your project
By adding functions we are going to create the Lambda's. 

```
Amplify add function
```
Follow these steps:
* Provide a friendly name for your resource to be used as a label for this category in the project: **pushNotification**
* Provide the AWS Lambda function name: **<press enter>**
* Choose the function template that you want to use: **Hello world function**
* Do you want to access other resources created in this project from your Lambda function? **Y**
* Select the category **api**
* Select the operations you want to permit for pushAPI **read**
* Do you want to edit the local lambda function now? **N**

Repeat this step again but call the next function **pinpoint** and anwer these steps with this information:
* Do you want to access other resources created in this project from your Lambda function? **Y**
* Select the category **Analytics**
* Select the operations you want to permit for Analytics **create, read, update, delete**
* Do you want to edit the local lambda function now? **N**

Your Functions are created now and you can find it in your project directory:

``` Amplify > backend > function > name of your function ```

Go to the *src directory* of the Pinpoint function and install this package

```

$ npm install aws-sdk

```
Open the index.js file and paste this code. Please walk through the code and replace the right values.

```javascript
/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var apiPushAPIGraphQLAPIIdOutput = process.env.API_PUSHAPI_GRAPHQLAPIIDOUTPUT
var apiPushAPIGraphQLAPIEndpointOutput = process.env.API_PUSHAPI_GRAPHQLAPIENDPOINTOUTPUT
var analyticsAmplifypushappId = process.env.ANALYTICS_AMPLIFYPUSHAPP_ID
var analyticsAmplifypushappRegion = process.env.ANALYTICS_AMPLIFYPUSHAPP_REGION

Amplify Params - DO NOT EDIT */

const AWS = require("aws-sdk");
AWS.config.region = "<REGION>"; // fill in your right region ******
const pinpoint = new AWS.Pinpoint();

exports.handler = async (event, context) => {
  try {
    event = event.arguments.input;

    // Create a AWS Pinpoint project
    const appID = await createApp();

    // Enable the SES email address for the project
    enableChannels(appID, event.email);

    // Create the endpoints for the Pinpoint project/app
    await createEndPoints(
      appID,
      event.id,
      event.email,
      event.name,
      event.token
    );

    // Create a segment where you want to filter the endpoint you want to send a message to
    const segmentID = await createSegment(appID);

    // create starter segment and campaign.
    const hookLambda = "pushNotification-dev";
    const result = await createCampaign(
      appID,
      event.message,
      hookLambda,
      segmentID
    );

    return result;
  } catch (error) {
    console.log("Oops! An error happened.");
  }
};

async function createApp() {
  let params = {
    CreateApplicationRequest: {
      /* required */
      Name: "Push App" /* Campaign name, required */
    }
  };

  return new Promise((res, rej) => {
    pinpoint.createApp(params, function(err, data) {
      if (err) {
        rej(err);
        console.log(err, err.stack); // an error occurred
      } else {
        res(data.ApplicationResponse.Id); //console.log(data);// successful response
      }
    });
  });
}

/*
When you create a new pinpoint app you need to activate an emailaddress where the emails can be send from
*/
function enableChannels(appID, email) {
  console.log(appID, email);
  var params = {
    ApplicationId: appID /* required */,
    EmailChannelRequest: {
      /* required */
      FromAddress:
        "<FROM EMAIL ADDRESS>" /* use the emailaddress that you activated in AWS SES, required  */,
      Identity:
        "arn:aws:ses:<REGION>:<ACCOUNTID>:identity/" + email /* required */,
      Enabled: true
    }
  };
  pinpoint.updateEmailChannel(params, function(err, data) {
    if (err) console.log(err, err.stack);
    else console.log(data); // successful response
  });
}

/*
An endpoint is an object which contains user data which you can use later in a segment to send messages
*/
async function createEndPoints(appID, id, email, name, token) {
  
  let params = {
    ApplicationId: appID /* required */,
    EndpointId: id /* required */,
    EndpointRequest: {
      /* required */
      Address: email,
      ChannelType: "EMAIL",
      EndpointStatus: "ACTIVE",
      OptOut: "NONE",
      User: {
        UserAttributes: {
          name: [
            name
            /* more items */
          ],
          expoToken: [
            token
            /* more items */
          ]
        }
      }
    }
  };

  await pinpoint.updateEndpoint(params, function(err, data) {
    if (err) {
      console.log(err, err.stack);
      // an error occurred
    } else {
      console.log(data); // successful response
    }
  });
}

function createSegment(appID) {
  let params = {
    ApplicationId: appID /* required */,
    WriteSegmentRequest: {
      /* required */
      Dimensions: {
        Demographic: {
          Channel: {
            Values: [
              /* required */
              "EMAIL"
              /* more items */
            ],
            DimensionType: "INCLUSIVE"
          }
        }
      },
      Name: "Segment"
    }
  };

  return new Promise((res, rej) => {
    pinpoint.createSegment(params, function(err, data) {
      if (err) {
        rej(err);
        console.log(err, err.stack); // an error occurred
      } else {
        res(data.SegmentResponse.Id); //console.log(data);// successful response
      }
    });
  });
}
/*
With the endpoint(s) created you can create a segment. A segment is a filter which selects the right endpionts to send messages to
*/
async function createCampaign(appID, message, env, segmentID) {
  const utcDate = new Date(Date.now());

  const params = {
    ApplicationId: appID /* required */,
    WriteCampaignRequest: {
      /* required */
      HoldoutPercent: 0,
      Hook: {
        LambdaFunctionName: env,
        Mode: "FILTER"
      },
      IsPaused: false,
      Limits: {},
      MessageConfiguration: {
        EmailMessage: {
          Title: "Test Email Message",
          HtmlBody:
            `<!DOCTYPE html>\n    <html lang="en">\n    <head>\n    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n</head>\n<body>\n<H2>Hallo {{User.UserAttributes.name}},</H2>\n\n <br />This is a Text Message from PinPoint. \n You have send this text: \n\n` +
            message +
            `\n</body>\n</html>`,
          FromAddress: "<FROM EMAIL ADDRESS>"
        },
        DefaultMessage: {
          // you push message
          Body: message
        }
      },
      Name: "push campaign",
      Schedule: {
        IsLocalTime: false,
        QuietTime: {},
        StartTime: utcDate.toISOString(),
        Frequency: "ONCE"
      },
      SegmentId: String(segmentID),
      SegmentVersion: 1,
      tags: {}
    }
  };

  return new Promise((res, rej) => {
    pinpoint.createCampaign(params, function(err, data) {
      if (err) {
        console.log(err, err.stack); // an error occurred
        const response = {
          statusCode: 500,
          body: JSON.stringify(err)
        };
        rej(response);
      } else {
        console.log(data);
        const response = {
          statusCode: 200,
          body: JSON.stringify(data)
        };

        res(response); // successful response
      }
    });
  });
}


```

You are almost there. Now let's make the next function. Go to the Push Notifications directory and then to the **src** directory. Install this NPM package 

```

$ npm install expo-server-sdk

```
Open index.js and past this code

```javascript
/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var apiPushAPIGraphQLAPIIdOutput = process.env.API_PUSHAPI_GRAPHQLAPIIDOUTPUT
var apiPushAPIGraphQLAPIEndpointOutput = process.env.API_PUSHAPI_GRAPHQLAPIENDPOINTOUTPUT

Amplify Params - DO NOT EDIT */ const {
  Expo
} = require("expo-server-sdk");

// Create a new Expo SDK client
let expo = new Expo();

exports.handler = function(event, context, callback) {
  try {
    let messages = [];

    // prettier-ignore
    for (var key in event.Endpoints) {
    if (event.Endpoints.hasOwnProperty(key)) {
      var endpoint = event.Endpoints[key];

        messages.push({
          to: String(endpoint.User.UserAttributes.expoToken),
          sound: "default",
          body: event.Message.apnsmessage.body,
           data: { "status": "ok" }
        });

    }
  }

    // The Expo push notification service accepts batches of notifications so
    // that you don't need to send 1000 requests to send 1000 notifications. We
    // recommend you batch your notifications to reduce the number of requests
    // and to compress them (notifications with similar content will get
    // compressed).
    let chunks = expo.chunkPushNotifications(messages);
    let tickets = [];
    (async () => {
      // Send the chunks to the Expo push notification service. There are
      // different strategies you could use. A simple one is to send one chunk at a
      // time, which nicely spreads the load out over time:
      for (let chunk of chunks) {
        try {
          let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
          console.log(ticketChunk);
          tickets.push(...ticketChunk);
          // NOTE: If a ticket contains an error code in ticket.details.error, you
          // must handle it appropriately. The error codes are listed in the Expo
          // documentation:
          // https://docs.expo.io/versions/latest/guides/push-notifications#response-format
        } catch (error) {
          console.error(error);
        }
      }
    })();

    // Later, after the Expo push notification service has delivered the
    // notifications to Apple or Google (usually quickly, but allow the the service
    // up to 30 minutes when under load), a "receipt" for each notification is
    // created. The receipts will be available for at least a day; stale receipts
    // are deleted.
    //
    // The ID of each receipt is sent back in the response "ticket" for each
    // notification. In summary, sending a notification produces a ticket, which
    // contains a receipt ID you later use to get the receipt.
    //
    // The receipts may contain error codes to which you must respond. In
    // particular, Apple or Google may block apps that continue to send
    // notifications to devices that have blocked notifications or have uninstalled
    // your app. Expo does not control this policy and sends back the feedback from
    // Apple and Google so you can handle it appropriately.
    let receiptIds = [];
    for (let ticket of tickets) {
      // NOTE: Not all tickets have IDs; for example, tickets for notifications
      // that could not be enqueued will have error information and no receipt ID.
      if (ticket.id) {
        receiptIds.push(ticket.id);
      }
    }

    let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds);
    async () => {
      // Like sending notifications, there are different strategies you could use
      // to retrieve batches of receipts from the Expo service.
      for (let chunk of receiptIdChunks) {
        try {
          let receipts = await expo.getPushNotificationReceiptsAsync(chunk);
          console.log(receipts);

          // The receipts specify whether Apple or Google successfully received the
          // notification and information about an error, if one occurred.
          for (let receipt of receipts) {
            if (receipt.status === "ok") {
              continue;
            } else if (receipt.status === "error") {
              console.error(
                `There was an error sending a notification: ${receipt.message}`
              );
              if (receipt.details && receipt.details.error) {
                // The error codes are listed in the Expo documentation:
                // https://docs.expo.io/versions/latest/guides/push-notifications#response-format
                // You must handle the errors appropriately.
                console.error(`The error code is ${receipt.details.error}`);
              }
            }
          }
        } catch (error) {
          console.error(error);
        }
      }
    };

    callback(null, event.Endpoints);
  } catch (error) {
    callback(error);
  }
};

```

Now we need to push first all the service to the cloud. Go to the root your project and run this command 

```
amplify push
```

Follow these steps:
* Do you want to generate code for your newly created GraphQL API?**Yes**
* Choose the code generation language target **Javascript**
* Enter the file name pattern of graphql queries, mutations and subscriptions **Enter** (default)
* Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions? **Y**
* Enter maximum statement depth [increase from default if your schema is deeply nested] **Enter** (default)


Go back to the **src** directory of your pushNotification function. Open this file: pushNotification-cloudformation-template.json and go to the "Resources": { section and paste this code:

```
	"LambdaInvokePermission": {
			"Type": "AWS::Lambda::Permission",
			"Properties": {
				"Action": "lambda:InvokeFunction",
				"FunctionName": {
					"Fn::If": [
						"ShouldNotCreateEnvResources",
						"pushNotification",
						{
							"Fn::Join": [
								"",
								[
									"pushNotification",
									"-",
									{
										"Ref": "env"
									}
								]
							]
						}
					]
				},
				"Principal": {
					"Fn::Sub": [
						"pinpoint.${region}.amazonaws.com",
						{
							"region": {
								"Ref": "AWS::Region"
							}
						}
					]
				},
				"SourceArn": {
					"Fn::Sub": [
						"arn:aws:mobiletargeting:${region}:${account}:/apps/*",
						{
							"region": {
								"Ref": "AWS::Region"
							},
							"account": {
								"Ref": "AWS::AccountId"
							}
						}
					]
				}
			}
		},
```

Save the file. This code will set the permissions so that AWS Pinpoint can invoke the Lambda as a hook. You have to do a separate push. If you try to do it with the previous push it can be the case that not all services are pushed when you try to set the permissions.

```
amplify push
```

Go to:

``` Amplify > backend > api > name of your api > open schema.graphql```

Now the functions have been deployed we need to update also our schema. Put this code in schema.graphql. It will create an extra mutation so you can invoke the functions in your app. 


```
type User @model {
  id: ID!
  name: String!
  email: String!
  expoToken: String
}

type Mutation {
  pinpoint(input: pinpointInput): pinpointResult
    @function(name: "pinpoint-${env}")
}

type pinpointResult {
  statusCode: Int
  body: String
}

input pinpointInput {
  token: String!
  name: String!
  email: String!
  message: String!
  id: String!
}

```

Do another push

```
amplify push
```

* Are you sure you want to continue? **Y**


Go to the root of your project, then to src directory > graphql > mutations.js check if this code is there, if not please add and save it:

```javascript
export const pinpoint = /* GraphQL */ `
  mutation pinpoint($input: pinpointInput!) {
    pinpoint(input: $input) {
      statusCode
      body
    }
  }
`;
```


### Add some data via Cognito and AppSync

Go in the console to **AWS Cognito** and click on Manager User Pools >  then on your user pool > user and groups > create user. Fill in the form and leave all the checkboxes as verified. Click on the new user and make a note of the **sub** value (something like b14cc22-c73f-4775-afd7-b54f222q4758) and then go to **App Clients** in the menu and make a note of the **App client ID** from the clientWeb (the bottom one) use these values in the next step.

Let’s add some data which you can use in your app. Go to the AppSync service in the console. 

- Go to **AWS AppSync** via the console.
- Open your project
- Click on **Queries**
- Log in with a Cognito user by clicking on the button 'Login via Cognito User Pools' (You can create a user via Cognito in the console or via your App) (Use the data that your have written down)
- Add the following code and run the code (update with your e-mail address):
```javascript
mutation PutUser {
  createUser( input: {
    id: "b14cc22-c73f-4775-afd7-b54f222q4758",
    name: "Ramon", 
    email: "<EMAILADDRESS>"
  }
  ){
    id
    name
    email
  }
}
```

### Let's build the React Native App

I made a simple (and a little bit ugly, everything in two components) app where a user needs to log in, we will get his user profile, look if there is valid ExpoToken available, if not then we request one and save that in the profile. In the app this user can make a message that will be send through a push notification and email to the user

Go to the root of your project and open App.js and replace it with this code: 

```javascript
import React from "react";
import { withAuthenticator } from "aws-amplify-react-native";
import Amplify, { Analytics } from "aws-amplify";

// Get the aws resources configuration parameters
import awsconfig from "./aws-exports"; // if you are using Amplify CLI
import Main from "./src/Main";

Amplify.configure(awsconfig);
Analytics.disable(); // disabled analytics otherwise you get annoying messages

class App extends React.Component {
  render() {
    return <Main />;
  }
}

export default withAuthenticator(App);

```

This will import everything you need and wraps your app with a HOC withAuthenticator. This creates login and signup functionality for your app. 

Now create a file in the **src folder** with this name: **Main.js** and paste the following code:

```javascript
import React from "react";
import { View, TextInput, Button } from "react-native";
import * as queries from "./graphql/queries.js";
import * as mutations from "./graphql/mutations";
import { API, graphqlOperation, Auth } from "aws-amplify";
import { Notifications } from "expo";
import * as Permissions from "expo-permissions";

class Main extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      profile: {},
      message: "",
      user: ""
    };
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  async componentDidMount() {
    const user = await Auth.currentSession()
      .then(data => {
        this.setState({ user: data.idToken.payload.sub });
        return data.idToken.payload.sub;
      })
      .catch(err => console.log(err));

    const profile = await this.getUserProfile(user);

    // There is no expoToken available yet, so we will request that and save it into the profile
    if (profile.expoToken === null) {
      const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);

      if (status !== "granted") {
        alert("No notification permissions!");
        return;
      }

      let token = await Notifications.getExpoPushTokenAsync();

      // Only update the profile with the expoToken if it not exists yet
      if (token !== "") {
        const inputParams = {
          id: user,
          expoToken: token
        };
        await API.graphql(
          graphqlOperation(mutations.updateUser, { input: inputParams })
        )
          .then(result => {
            console.log(result);
          })
          .catch(err => console.log(err));
      }
    }
  }

  async getUserProfile(sub) {
    const result = await API.graphql(
      graphqlOperation(queries.getUser, { id: sub })
    )
      .then(result => {
        this.setState({
          profile: result.data.getUser
        });
        return result.data.getUser;
      })
      .catch(err => console.log(err));

    return result;
  }

  async handleSubmit() {
    const inputParams = {
      message: this.state.message,
      token: this.state.profile.expoToken,
      name: this.state.profile.name,
      email: this.state.profile.email,
      id: this.state.user
    };

    await API.graphql(
      graphqlOperation(mutations.pinpoint, { input: inputParams })
    )
      .then(result => {
        console.log(result);
        console.log("success");
        this.setState({ message: "" });
      })
      .catch(err => console.log(err));
  }

  render() {
    return (
      <View style={{ marginTop: 80, marginLeft: 10, marginRight: 10 }}>
        <TextInput
          placeholder="Your push message"
          value={this.state.message}
          onChangeText={input => this.setState({ message: input })}
          style={{
            paddingLeft: 5,
            height: 40,
            fontSize: 16,
            marginBottom: 6,
            marginTop: 2
          }}
        ></TextInput>
        <Button title="Submit" onPress={this.handleSubmit} />
      </View>
    );
  }
}

export default Main;

``` 

Your app is ready and you can start it from your root project with:

```
expo start"
```

You need to install Expo client on a physical device and start the app otherwise you can't test the push notifications. Sign in with the user that you have created via AWS Cognito, fill in a push message, wait a few seconds .... et voilà .... you have received a push message in the app and an email message on your account.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/98t530a270u8mtfzv3np.png)

#Conclusion
It is really amazing how you can deliver these features fast to your customers with these great tools (AWS Amplify, React Native and Expo). Your app is ready to get your customers engaged :)

I was struggling with getting push notifications up and running while still keeping all the tools onboard so I can benefit from it. I had to think beyond the current limitations and that is how I came up with this architecture and implementation.

I hope you liked this guide and I am looking forward for your feedback in the comments or please share the projects where you have implemented this set up. Happy coding!

> See github for the actual code: [https://github.com/rpostulart/pushapp]