/aws-mobile-react-native-starter

AWS Mobile React Native Starter App https://aws.amazon.com/mobile

Primary LanguageJavaScriptApache License 2.0Apache-2.0

AWS Mobile React Native Starter App - Serverless Pet Tracker

Bootstrap a React Native application on AWS. This starter automatically provisions a Serverless infrastructure with authentication, authorization, image storage, API access and database operations. It also includes user registration and MFA support. The sample use case is a "Pet Tracker" where after a user registers and logs in they can upload pictures of their pet to the system along with information like the birthday or breed.

A companion blog post for this repository can be found in the AWS Mobile Blog: Announcing: React Native Starter Project with One-Click AWS Deployment and Serverless Infrastructure.

This starter uses the AWS Amplify JavaScript library to add cloud support to the application.

Quicklinks

Architecture Overview

Architecture

AWS Services used:

  • Amazon Cognito User Pools
  • Amazon Cognito Federated Identities
  • Amazon API Gateway
  • AWS Lambda
  • Amazon DynamoDB
  • Amazon S3
  • AWS Mobile Hub

Prerequisites

Getting Started

First clone this repo: git clone https://github.com/awslabs/aws-mobile-react-native-starter

Backend Setup

  1. Set up your AWS resources using AWS Mobile Hub by clicking the button below:

  1. Press Import project

Client Setup

Alt Text

  1. Before proceeding further, in the Mobile Hub console click the Cloud Logic tile and ensure that the API deployment status at the bottom shows CREATE_COMPLETE (this can take a few moments).

  2. Click Configure on the left hand bar of the console and select the Hosting and Streaming tile.

  3. At the bottom of the page click Download aws-exports.js file. Copy this file into the ./aws-mobile-react-native-starter/client folder of the repo you cloned.

    • Alternatively using the AWS CLI:

      $ cd ../aws-mobile-react-native-starter/client
      $ aws s3api list-buckets --query 'Buckets[?starts_with(Name,`reactnativestarter-hosting`)].Name' |grep reactnativestarter |tr -d '"'
      $ aws s3api get-object --bucket <YOUR_BUCKET_NAME> --key aws-exports.js ./aws-exports.js
      
  4. Navigate into ./aws-mobile-react-native-starter/client and run:

    $ npm install
    $ npm run ios #npm run android
    

Done!

Application walkthrough

  1. On a phone or emulator/simulator, open the application

  2. Select the SIGN UP tab in the lower right to register a new user. You will be prompted to enter a valid email and phone number to confirm your registration.

  3. Click Sign Up and you will recieve a code via SMS. Enter this into the prompt and press OK.

  4. From the Sign In tab of the application enter the Username and Password of the user you just registered and select SIGN IN.

  5. A code will be sent via SMS. Enter that code in the prompt and press OK.

  6. Press the plus (+) button to upload a photo. After selecting a photo select the Check mark.

  7. Fill out a few details like the name, birthday, breed and gender of your pet. Press Add Pet to upload the photo. This will first transfer the photo to an S3 bucket which only the logged-in user has access to, it will then write the record to a DynamoDB table (via API Gateway and Lambda) that is also restricted on a per-user basis.

Add Pet

  1. You will see a record of your pet on the homescreen.

My Pets

Use features in your app.

This starter app includes a set of libraries (under client/lib) to help you integrate features into your own React Native app. These libraries include helpers, React Higher Order Components that you can use to easily add capabilities for Sign-Up, Sign-In or API Access with basic reusable React Components through Auth, API and Storage HOCs.

You will need Create React Native App for the next sections.

  • Create a new React Native App (CRNA) using create-react-native-app
  • cd into your new app dir.
  • Eject your react native app (in our examples call it "myapp")
create-react-native-app <project-directory>
cd <project-directory>
npm run eject # Eject as "React Native"
  • Download the aws-exports.js file from your AWS MobileHub project as outlined earlier in the Getting started section. Place it in the root of your new CRNA directory.

  • Copy lib folder from this starter app
    cp -rf ../<some-directory>/aws-react-native-native-starter/client/lib .

Sign-up and Sign-In

  1. Install dependencies with npm install

  2. Install additional dependencies:

npm install aws-sdk react-native-aws-cognito-js react-native-prompt --save

  1. Link the native components by running: react-native link

  2. Open the App.js file.

  3. Import the WithAuth HOC from the library

import { WithAuth } from './lib/Categories/Auth/Components';

This HOC will add a prop called session to your component as well as a method called doSignOut(). There is also a wrapper class called Auth as part of this which is a helper for common Sign-Up and Sign-In activities. We'll show you how to use the session and doSignOut() capabilities next.

  1. Edit your App component to transform it into one that suports Auth
export default WithAuth(class App extends React.Component {
  // ...
});
  1. Import the React Native Button component
import { StyleSheet, Text, View, Button } from 'react-native';
  1. Import the SignIn/SignUp example component
import { SignIn, SignUp } from './lib/Categories/Auth/Components/Examples';
  1. Change your render() method to check if a user is signed in or out, and show SignIn/SignUp components or a SignOut button accordingly.
render() {
  const { session } = this.props;

  return (
    session ?
      (<View style={styles.container}>
        <Button title="Sign Out" onPress={() => this.props.doSignOut()} />
      </View>)
      :
      (<View style={styles.container}>
        <SignIn {...this.props} />
        <SignUp {...this.props} />
      </View>)
  );
}
  1. Test it!
    npm run ios # or android

  2. You now have SignIn/SignUp/SignOut capabilities (With MFA support too!)

Cloud APIs and Backend Access Control

In order to access resources in your AWS account that are protected via AWS Identity and Access Management you will need to sign your requests using the AWS Signature Version 4 signing process. The starter application supports signing requests both for uploading your images to Amazon S3, as well as communicating with the backend (AWS Lambda and DynamoDB) via Amazon API Gateway. There are many different ways that IAM Permissions can be configured to Control Access to API Gateway using credentials which we encourage you to read more about.

The starter application retrieves AWS credentials using the WithAuth HOC from the previous section via Amazon Cognito. This section outlines using an API feature which automatically uses these credentials to sign requests to Amazon API Gateway which are secured using IAM.

The following steps require the WithAuth section to be completed first. Please follow steps 1-9 from the earlier Sign-Up and Sign-In section.

  1. Install additional dependencies
    npm install aws4-react-native axios --save

  2. Import the aws-exports.js file

import awsmobile from './aws-exports';
  1. Import the WithAuth HOC from the library
import { WithAPI } from './lib/Categories/API/Components';
  1. Edit your App component to transform it into one that suports API
export default WithAPI(WithAuth(class App extends React.Component {
  // ...
}));
  1. Add apiResponse to the component's initial state
export default WithAPI(WithAuth(class App extends React.Component {

  state = {
    apiResponse: null,
  }

  // ...
}));
  1. Add a handler method to your component to call your API
export default WithAPI(WithAuth(class App extends React.Component {
  // ...

  async handleCallAPI() {
    const { api } = this.props;

    // Get endpoint
    const cloudLogicArray = JSON.parse(awsmobile.aws_cloud_logic_custom);
    const endPoint = cloudLogicArray[0].endpoint;

    const requestParams = {
      method: 'GET',
      url: endPoint + '/items/pets',
    };

    let apiResponse = null;

    try {
      apiResponse = await api.restRequest(requestParams);
    } catch (err) {
      console.warn(err);
    }

    this.setState({ apiResponse });
  }

  // ...
}));
  1. Change your render() method to show a button to invoke your API
render() {
  const { session } = this.props;

  return (
    session ?
      (<View style={styles.container}>
        <Button title="Call API" onPress={this.handleCallAPI.bind(this)} />
        <Text>Response: {this.state.apiResponse && JSON.stringify(this.state.apiResponse)}</Text>
        <Button title="Sign Out" onPress={() => this.props.doSignOut()} />
      </View>)
      :
      (<View style={styles.container}>
        <SignIn {...this.props} />
        <SignUp {...this.props} />
      </View>)
  );
}
  1. Test it!
    npm run ios # or android

  2. You can now invoke API Gateway APIs from your React Native that are protected via AWS IAM. After you login to the application press the Call API button to see the JSON response returned from the network request.

Storing content in the cloud

Many applications today provide rich media such as images or videos. Sometimes these are also private to users. This starter project provides a Storage component that allows a user to upload data, such as an image, to an Amazon S3 bucket in a folder which is protected so that only that user can access the data. This is done by setting S3 bucket policies on unique user Identities provided by Amazon Cognito. You can read more about this here.

The Storage feature depends on the user to have valid credentials. The following steps require the WithAuth section to be completed first. Please follow steps 1-9 from the earlier Sign-Up and Sign-In section.

  1. First, install additional dependencies
    npm install react-native-fetch-blob buffer --save

  2. Next link the native bridge components

    • react-native-fetch-blob is a library to help you with data transfer on React Native. Run the following command in your terminal:
RNFB_ANDROID_PERMISSIONS=true react-native link
  1. Import the aws-exports.js file if you haven't already
import awsmobile from './aws-exports';
  1. Import dependencies (use App.js from the CRNA process):
import AWS from 'aws-sdk';
import RNFetchBlob from 'react-native-fetch-blob';
import { Buffer } from 'buffer';
  1. Import the WithStorage HOC from the library
import { WithStorage } from './lib/Categories/Storage/Components';
  1. Import the React Native Image component
import { StyleSheet, Text, View, Button, Image } from 'react-native';
  1. Edit your App component to transform it into one that suports Storage
export default WithStorage(WithAuth(class App extends React.Component {
  // ...
}));
  1. Add objectUrl to the component's initial state
export default WithStorage(WithAuth(class App extends React.Component {

  state = {
    objectUrl: null,
  }

  // ...
}));
  1. Add a handler method to your component to upload a file to a private area for the signed in user. The sample method below shows how to download a sample PNG file of an AWS logo and upload it to the S3 bucket. Your application might get images from the camera roll on the phone (see the starter app code for examples of this).
export default WithStorage(WithAuth(class App extends React.Component {
  // ...

  async handleUploadFile() {
    const url = 'https://awsmedia.s3.amazonaws.com/AWS_Logo_PoweredBy_127px.png';
    const [, fileName, extension] = /.*\/(.+)\.(\w+)$/.exec(url);

    // Get cognito identity for the signed in user
    const { IdentityId } = AWS.config.credentials.data;

    // File will be uploaded to the user's private space in the S3 bucket
    const key = `private/${IdentityId}/${fileName}`;

    let objectUrl = null;

    try {
      // Download file from the internet.
      const download = await RNFetchBlob.fetch('GET', url);
      const { data } = download;
      const { respInfo: { headers: { 'Content-Type': contentType } } } = download;

      // Upload the file
      const upload = await this.props.storage.putObject(key, new Buffer(data, 'base64'), contentType);

      // Get url for stored object. This is an S3 presigned url. See: http://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html
      objectUrl = this.props.storage.getObjectUrl(upload.key);

      console.log(objectUrl);
    } catch (err) {
      console.warn(err);
    }

    this.setState({ objectUrl });
  }

  // ...
}));

Note: The AWS Mobile Hub import process you ran at the begining created an S3 bucket with folders such as public and private. The code above creates a key variable for uploading to the private folder for this specific user Identitity. If you wish to make the data public you could use const key = public/filename as the upload location.

  1. Change your render() method to show a button to upload the file and the uploaded image
  render() {
    const { session } = this.props;

    return (
      session ?
        (<View style={styles.container}>
          {this.state.objectUrl && <Image source={{ uri: this.state.objectUrl }} style={{width: 200, height: 200, resizeMode: 'contain'}} />}
          <Button title="Upload file" onPress={this.handleUploadFile.bind(this)} />
          <Button title="Sign Out" onPress={() => this.props.doSignOut()} />
        </View>)
        :
        (<View style={styles.container}>
          <SignIn {...this.props} />
          <SignUp {...this.props} />
        </View>)
    );
  }
  1. Test it!
    npm run ios # or android

  2. You can now store objects in the Cloud on S3 from React Native using AWS IAM credentials. After you press the Upload file button go back into your Mobile Hub project and click the Resources button on the left of the console. Under the section that says Amazon S3 Buckets there should be one that has userfiles in the name. Click that and you'll see it has a folder labeled private which is organized by the user Identities. This will contain the images you've uploaded.

Modifying Express routes in Lambda

The sample application invokes a Lambda function running Express which will make CRUD operations to DynamoDB depending on the route which is passed from the client application. You may wish to modify this backend behavior for your own needs. The steps outline how you could add functionality to "delete a Pet" by showing what modifications would be needed in the Lambda function and the corresponding client modifications to make the request.

  1. After you have cloned this repo, locate ./aws-mobile-react-native-starter/backend/lambdas/crud/app.js and find the following section of code:
app.listen(3000, function () {
  console.log('App started');
});
  1. Immediately Before this code (line_72) add in the following code:
app.delete('/items/pets/:petId', (req, res) => {
  if (!req.params.petId) {
    res.status(400).json({
      message: 'You must specify a pet id',
    }).end();
    return;
  }

  const userId = req.apiGateway.event.requestContext.identity.cognitoIdentityId;

  dynamoDb.delete({
    TableName: PETS_TABLE_NAME,
    Key: {
      userId: userId,
      petId: req.params.petId,
    }
  }, (err, data) => {
    if (err) {
      console.log(err)
      res.status(500).json({
        message: 'Could not delete pet'
      }).end();
    } else {
      res.json(null);
    }
  });
});
  1. Save the file and in the Mobile Hub console for your project click the Cloud Logic card. Expand the View resource details section and note the name of the Lambda function in the list for the next step. It should be something similar to Pets-itemsHandler-mobilehub-XXXXXXXXX.

  2. In a terminal navigate to ./aws-mobile-react-native-starter/backend and run:

npm run build-lambdas
aws lambda update-function-code --function-name FUNCTION_NAME --zip-file fileb://lambdas/crud-lambda.zip

REPLACE the FUNCTION_NAME with your Lambda function name from the previous step.

This might take a moment to complete based on your network connection. Please be patient.

Alternatively you could click the Lambda function resource in the Mobile Hub console which opens the Lambda console and press the Upload button on that page to upload the lambdas/crud-lambda.zip file.

  1. Now that you've updated the Cloud Logic backend modify the client to call the new API route. In the ./aws-mobile-react-native-starter/client/src/Screens directory edit ViewPet.jsx
  • Add the following imports
import { Button } from 'react-native-elements';
import awsmobile from '../../aws-exports';
import API from '../../lib/Categories/API';
  • Add the following function BEFORE the render() method:
  async handleDeletePet(petId) {
    const cloudLogicArray = JSON.parse(awsmobile.aws_cloud_logic_custom);
    const endPoint = cloudLogicArray[0].endpoint;
    const requestParams = {
      method: 'DELETE',
      url: `${endPoint}/items/pets/${petId}`,
    }

    try {
      await API.restRequest(requestParams);

      this.props.navigation.navigate('Home');
    } catch (err) {
      console.log(err);
    }
  }
  • In the return statement of the render method add in a new button after the <View style={styles.breaker} />:
// ...
      <View style={styles.breaker} />
      <Button
          fontFamily='lato'
          backgroundColor={colors.red}
          large
          title="DELETE PET"
          onPress={this.handleDeletePet.bind(this, pet.petId)} />
// ...
  1. Save the file and run the application again:
cd ./aws-mobile-react-native-starter/client
npm run ios # or android

If you have previously uploaded any pets click on their thumbnail from the main page (if not upload one now). You should see a new button DELETE PET. Click on it and the pet should be removed from the screen. The record should also have been removed from the DynamoDB table. You can validate this by going to the Resources section of your Mobile Hub project and opening up the DynamoDB table.

Security Information

Remote Storage

This starter app uploads content to an S3 bucket. The S3 bucket is configured by Mobile Hub to use fine-grained access control to support public, protected and private access, you can find more information here. To learn more about restricting this access further, see Amazon S3 Security Considerations.

Local Storage

This starter app uses React Native's AsyncStorage to persist user tokens locally (accessKeyId, secretAccessKey and sessionToken). You can take further actions to secure these tokens by encrypting them.

API Handler Table Permissions

The Lambda function in this starter will read and write to DynamoDB and it's role will be granted the appropriate permissions to perform such actions. If you wish to modify the sample to perform a more restricted set of actions see Authentication and Access Control for Amazon DynamoDB.