DEV Community

Cover image for The guide to implement Push Notifications with React Native, Expo and AWS Amplify
rpostulart for AWS Community Builders

Posted on • Updated on

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

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

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

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.

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:
  • 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.

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

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

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:

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

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]

Top comments (3)

Collapse
 
kris profile image
kris

Using React Native AWS Amplify, AWS amplify and AWS CLI to set up the push notification in React Native app based on the expo ecosystem. Configuring GraphQL and aws-apk as well. This article provides a lot and delivers stepwise detailed integration of the overall AWS Amplify feature in the expo ecosystem. Highly recommended for beginners using expo for React Native app development.

Collapse
 
jackatdev profile image
Jackson Santos • Edited

Hello rpostulart, thank you for your guides. Much appreciated.
I would like to share some thoughts.
If we only consider push notifications then Pinpoint isn't really necessary right? Either ExpoServer.
You could go straight ahead and send the payload via fetch in your lambda, like in the Expo Docs:

const message = {
      to: this.state.expoPushToken,
      sound: 'default',
      title: 'Original Title',
      body: 'And here is the body!',
      data: { data: 'goes here' },
      _displayInForeground: true,
    };
    const response = await fetch('https://exp.host/--/api/v2/push/send', {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Accept-encoding': 'gzip, deflate',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(message),
    });
Enter fullscreen mode Exit fullscreen mode

So I assume you have included pinpoint only to demonstrate how these tools can be combined in a user engagement case. But unlikely the email delivery you would still not be able to track whether your push notification has been delivered or not, right? Or am I missing something?

Some observations in case people don't figure it out:

  • This scenario will only work in the Expo Client in Android. For the APK you will need to send push notifications via FCM docs.expo.io/versions/v37.0.0/guid...
  • On IOS push notifications will only show if the app is on the background.
  • There is a missing statement in the PolicyDocument in lambaexecutionpolicy from pinpoint-cloudformation-template.json, this needs to be added in order to be able to create an app in pinpoint:
{
    "Effect": "Allow",
    "Action": [
        "mobiletargeting:*"
    ],
    "Resource": {
        "Fn::Sub": [
            "arn:aws:mobiletargeting:*:${account}:apps/*",
            {
                "account": {
                    "Ref": "AWS::AccountId"
                }
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks rpostulart!

Collapse
 
pjsandwich profile image
Patrick Prioletti • Edited

In addition to the missing policy, there is an error in the way the Expo Push Token is written to the API in ./src/Main.js.

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 Notifications.requestPermissionsAsync();
      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;
Enter fullscreen mode Exit fullscreen mode

Where we define token, the cange is token -> token.data:

      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.data
        };
        await API.graphql(
          graphqlOperation(mutations.updateUser, { input: inputParams })
        )
          .then(result => {
            console.log(result);
          })
          .catch(err => console.log(err));
      }
    }
Enter fullscreen mode Exit fullscreen mode