A guide to implement Push Notifications with React Native, Expo, and AWS Amplify

By Ramon Postulart

ABN AMRO
ABN AMRO Developer Blog
16 min readSep 7, 2020

--

When building an app, you want to engage your customers. You want them to use your app as much as possible and bring value to them. Therefore, you have to set up push notifications. In this guide, Ramon Postulart, IT engineering lead at ABN AMRO, will show you how that’s possible with some great tools. He wrote a guide on how 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 it to IOS and Android. So fewer costs and a faster development cycle.

Expo

Expo works out well because it is packaged with a lot of features that 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 APIs, storage, authentication and authorization, database, data store, and more.

Push Notifications

When you will dive deeper into these toolsets and setting up your architecture you will start loving them. When you are at the point where you want to use push notifications, you have to say goodbye to Expo. This is because AWS Amplify doesn’t come with a solution out of the box where you can use push notifications without ejecting. Luckily, 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.

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. Meanwhile, the campaign data has been send back to AWS Pinpoint to send it further to other 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 of 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 here.

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 refer 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 that you can use to enrich your app. We are focussing mostly on the API service. So let’s add an API.

Amplify add api

Follow these steps:

  • Select Graphql
  • Enter a name for the API: pushAPI (your preferred API name)
  • Select an authorization 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

Your API and your schema definition have been created now. You can find it in your 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.

Configure AWS Email Service

Log in to 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 emails 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 cloud formation 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 Lambdas.

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:
  • 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 answer 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 now created, 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.

/* 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
}
});
});
}

We’re 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 paste this code:

/* 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:

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):
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 sent 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:

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 with Authenticator. 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:

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.

Well done!

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 to 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]
Click here to go to the original text.

--

--

ABN AMRO
ABN AMRO Developer Blog

Build the future of banking! Use our APIs to automate, innovate, and connect to millions of customers. Go to: https://developer.abnamro.com/