Skip to content

React Native Bars App: AWS Amplify, AWS AppSync, AWS Cognito, Google Places, Mapbox

Notifications You must be signed in to change notification settings

pjay79/BarsAppAmplify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BarsAppAmplify

React Native, AWS Amplify, AWS AppSync, AWS Cognito, Google Places, Mapbox. Please note: this is a work still in progress, and many features are not fully developed yet.

Update 1st Dec 2018, MapBox has been removed from this app as Google Places API terms require data to be place on Google Maps only

This app is being prepared for deployment

ToDo

  • enable offline support
  • add pagination

Folder structure:

folder1-small folder2-small

Screenshots

iOS

img_3560 img_3561 img_3562 img_3563 img_3644 img_3616 img_3617 img_3596 img_3598 img_3599 img_3600 img_3601 img_3602 img_3603 img_3604 img_3606 img_3607 img_3608

Technology stack:

  • @mapbox/react-native-mapbox-gl
  • aws-amplify
  • aws-amplify-react-native
  • aws-appsync
  • aws-appsync-react
  • aws-sdk
  • axios
  • babel-plugin-transform-remove-console
  • geolib
  • graphql-tag
  • lodash
  • moment
  • react-apollo
  • react-native-app-intro-slider
  • react-native-collapsible
  • react-native-config
  • react-native-elements
  • react-native-geolocation-service
  • react-native-keyboard-aware-scroll-view"
  • react-native-map-link
  • react-native-modal
  • react-native-splash-screen
  • react-native-swipeout
  • react-native-vector-icons
  • react-navigation
  • uuid

Installation

React Native setup:

Install Node.js:
https://nodejs.org/en/download/

brew install watchman
npm install -g react-native-cli

And also install Xcode for iOS simulator + Android Studio / Genymotion for Android simulator. Preferably connect up a hardware device for this particular app to access geolocation, maps + directions, and phone connection.

Project setup:

Clone the repo: git clone https://github.com/pjay79/BarsAppAmplify.git
Change to the project folder: cd BarsAppAmplify
Add dependencies: npm install or yarn

Amazon

Sign up to AWS Free Tier:
https://aws.amazon.com/free/

AWS Amplify CLI setup

npm install -g @aws-amplify/cli

amplify configure

This command will direct you to create a new IAM user, when prompted enter the accessKeyId and secretAccessKey, store these in a safe place, you can also assign this user an AWS Profile Name:

amplify-cropped

amplify init (in the project folder)

amplify-init-cropped

amplify add auth (update: I have now set MFA to optional to get the password reset functionality working)

amplify-auth

amplify add api

amplify-api-setup

The base schema.graphql file looks like this:

type Bar @model {
	id: ID!
	title: String!
	content: String!
	price: Int
	rating: Float
}

This app will have a many-to-many connection between type Bar and type User. Currently AWS Amplify does not yet support many-to-many connections, hence the @connection directive which is used for specifying relationships between @model object types cannot be used. Update the schema.graphql file to look as follows.

type Bar @model {
  id: ID!
  createdAt: String
  updatedAt: String
  name: String!
  phone: String
  location: String
  lat: String
  lng: String
  url: AWSURL
  addedBy: ID!
  users(first: Int, after: String): [Bar]
}

type BarMember @model {
  id: ID!
  createdAt: String
  updatedAt: String
  userId: ID!
  barId: ID!
}

type User @model {
  id: ID!
  createdAt: String
  updatedAt: String
  username: String!
  bars(first: Int, after: String): [Bar]
}

Important Step

amplify push

This command will update your cloud resources and add an aws-exports.js file to your project root directory. In your App.js file make sure this file is imported from the correct location.

Other directives

Note: AWS Amplify has has the following directives that can be used with AppSync:

@model: Used for storing types in Amazon DynamoDB.

@connection: Used to define different authorization strategies.

@auth: Used for specifying relationships between @model object types.

@searchable: Used for streaming the data of an @model object type to Amazon ElasticSearch Service.

AWS AppSync Codegen

AWS Amplify can generate types, as well as query, mutation, and subscription files based on your schema. In this project you will not need to do this as the relevant files have already been created in this repository. See the video below for an example:

https://www.youtube.com/watch?v=r0PbwDoNMcY

AWS AppSync Schema

Go to the AWS Console and AWS AppSync under Services. Select the API that has been generated API for this app and go to the schema.

schema

The schema that has been created needs some modification to allow for the many-to-many relationship between Bars and Users to work. Modify the schema as follows:

type Bar {
	id: ID!
	createdAt: String
	updatedAt: String
	name: String!
	phone: String
	location: String
	lat: String
	lng: String
	url: AWSURL
	website: AWSURL
	addedBy: ID!
	users(first: Int, after: String): BarUsersConnection
}

type BarMember {
	id: ID
	createdAt: String
	updatedAt: String
	userId: ID!
	barId: ID!
}

type BarUsersConnection {
	items: [User]
	nextToken: String
}

input CreateBarInput {
	id: ID!
	name: String!
	phone: String
	location: String
	lat: String
	lng: String
	url: AWSURL
	website: AWSURL
	addedBy: ID!
}

input CreateBarMemberInput {
	userId: ID!
	barId: ID!
}

input CreateUserInput {
	id: ID!
	username: String!
}

input DeleteBarInput {
	id: ID
}

input DeleteBarMemberInput {
	id: ID
}

input DeleteUserInput {
	id: ID
}

type ModelBarConnection {
	items: [Bar]
	nextToken: String
}

input ModelBarFilterInput {
	id: ModelIDFilterInput
	createdAt: ModelStringFilterInput
	name: ModelStringFilterInput
	phone: ModelStringFilterInput
	location: ModelStringFilterInput
	lat: ModelStringFilterInput
	lng: ModelStringFilterInput
	url: ModelStringFilterInput
	website: ModelStringFilterInput
	addedBy: ModelIDFilterInput
	and: [ModelBarFilterInput]
	or: [ModelBarFilterInput]
	not: ModelBarFilterInput
}

type ModelBarMemberConnection {
	items: [BarMember]
	nextToken: String
}

input ModelBarMemberFilterInput {
	id: ModelIDFilterInput
	createdAt: ModelStringFilterInput
	userId: ModelIDFilterInput
	barId: ModelIDFilterInput
	and: [ModelBarMemberFilterInput]
	or: [ModelBarMemberFilterInput]
	not: ModelBarMemberFilterInput
}

input ModelBooleanFilterInput {
	ne: Boolean
	eq: Boolean
}

input ModelFloatFilterInput {
	ne: Float
	eq: Float
	le: Float
	lt: Float
	ge: Float
	gt: Float
	contains: Float
	notContains: Float
	between: [Float]
}

input ModelIDFilterInput {
	ne: ID
	eq: ID
	le: ID
	lt: ID
	ge: ID
	gt: ID
	contains: ID
	notContains: ID
	between: [ID]
	beginsWith: ID
}

input ModelIntFilterInput {
	ne: Int
	eq: Int
	le: Int
	lt: Int
	ge: Int
	gt: Int
	contains: Int
	notContains: Int
	between: [Int]
}

enum ModelSortDirection {
	ASC
	DESC
}

input ModelStringFilterInput {
	ne: String
	eq: String
	le: String
	lt: String
	ge: String
	gt: String
	contains: String
	notContains: String
	between: [String]
	beginsWith: String
}

type ModelUserConnection {
	items: [User]
	nextToken: String
}

input ModelUserFilterInput {
	id: ModelIDFilterInput
	createdAt: ModelStringFilterInput
	username: ModelStringFilterInput
	and: [ModelUserFilterInput]
	or: [ModelUserFilterInput]
	not: ModelUserFilterInput
}

type Mutation {
	createBar(input: CreateBarInput!): Bar
	updateBar(input: UpdateBarInput!): Bar
	deleteBar(input: DeleteBarInput!): Bar
	createBarMember(input: CreateBarMemberInput!): BarMember
	updateBarMember(input: UpdateBarMemberInput!): BarMember
	deleteBarMember(input: DeleteBarMemberInput!): BarMember
	createUser(input: CreateUserInput!): User
	updateUser(input: UpdateUserInput!): User
	deleteUser(input: DeleteUserInput!): User
}

type Query {
	getBar(id: ID!): Bar
	listBars(filter: ModelBarFilterInput, limit: Int, nextToken: String): ModelBarConnection
	getBarMember(userId: ID!, barId: ID!): BarMember
	listBarMembers(filter: ModelBarMemberFilterInput, limit: Int, nextToken: String): ModelBarMemberConnection
	getUser(id: ID!): User
	listUsers(filter: ModelUserFilterInput, limit: Int, nextToken: String): ModelUserConnection
}

type Subscription {
	onCreateBar: Bar
		@aws_subscribe(mutations: ["createBar"])
	onUpdateBar: Bar
		@aws_subscribe(mutations: ["updateBar"])
	onDeleteBar: Bar
		@aws_subscribe(mutations: ["deleteBar"])
	onCreateBarMember: BarMember
		@aws_subscribe(mutations: ["createBarMember"])
	onUpdateBarMember: BarMember
		@aws_subscribe(mutations: ["updateBarMember"])
	onDeleteBarMember: BarMember
		@aws_subscribe(mutations: ["deleteBarMember"])
	onCreateUser: User
		@aws_subscribe(mutations: ["createUser"])
	onUpdateUser: User
		@aws_subscribe(mutations: ["updateUser"])
	onDeleteUser: User
		@aws_subscribe(mutations: ["deleteUser"])
}

input UpdateBarInput {
	id: ID!
	name: String
	phone: String
	location: String
	lat: String
	lng: String
	url: AWSURL
	website: AWSURL
}

input UpdateBarMemberInput {
	id: ID!
	userId: ID
	barId: ID
}

input UpdateUserInput {
	id: ID!
	username: String
}

type User {
	id: ID!
	createdAt: String
	updatedAt: String
	username: String!
	bars(first: Int, after: String): UserBarsConnection
}

type UserBarsConnection {
	items: [Bar]
	nextToken: String
}

AWS AppSync Resolvers

Resolver for Bar.users: BarMemberTable

## Request

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "query" : {
        "expression": "barId = :id",
        "expressionValues" : {
            ":id" : {
                "S" : "${ctx.source.id}"
            }
        }
    },
    "index": "barId-index",
    "limit": $util.defaultIfNull(${ctx.args.first}, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.after, null))
}
## Response

{
    "items": $util.toJson($ctx.result.items),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

Resolver for BarUsersConnection.items: UserTable

## Request
## Please remember to replace the hyphenated table name below with the one that was created for your app

#set($ids = [])
#foreach($user in ${ctx.source.items})
    #set($map = {})
    $util.qr($map.put("id", $util.dynamodb.toString($user.get("userId"))))
    $util.qr($ids.add($map))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchGetItem",
    "tables" : {
        "User-rndmxxybyjfv5lvzou3767zbte": {
               "keys": $util.toJson($ids),
               "consistentRead": true
       }
    }
}
## Response
## Please remember to replace the hyphenated table name below with the one that was created for your app

#if( ! ${ctx.result.data} )
  $util.toJson([])
#else
  $util.toJson($ctx.result.data.User-uq7n63nywrc4tku2tzgx4mx75u)
#end

Resolver for User.bars: BarMemberTable

## Request

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "query" : {
        "expression": "userId = :id",
        "expressionValues" : {
            ":id" : {
                "S" : "${ctx.source.id}"
            }
        }
    },
    "index": "userId-index",
    "limit": $util.defaultIfNull(${ctx.args.first}, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.after, null))
}
## Response

{
    "items": $util.toJson($ctx.result.items),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

Resolver for UserBarsConnection.items: BarTable

## Request
## Please remember to replace the hyphenated table name below with the one that was created for your app


#set($ids = [])
#foreach($bar in ${ctx.source.items})
    #set($map = {})
    $util.qr($map.put("id", $util.dynamodb.toString($bar.get("barId"))))
    $util.qr($ids.add($map))
#end

{
    "version" : "2018-05-29",
    "operation" : "BatchGetItem",
    "tables" : {
        "Bar-rndmxxybyjfv5lvzou3767zbte": {
               "keys": $util.toJson($ids),
               "consistentRead": true
       }
    }
}
## Response
## Please remember to replace the hyphenated table name below with the one that was created for your app

#if( ! ${ctx.result.data} )
  $util.toJson([])
#else
  $util.toJson($ctx.result.data.Bar-uq7n63nywrc4tku2tzgx4mx75u)
#end

Resolver for Query.getBarMember: BarMember

## Request

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "userId-index",
    "query" : {
        ## Provide a query expression. **
        "expression": "userId = :userId",
        "expressionValues" : {
            ":userId" : $util.dynamodb.toDynamoDBJson($ctx.args.userId)
        }
    },
    "filter" : {
    	"expression" : "barId = :barId",
        "expressionValues" : {
            ":barId" : $util.dynamodb.toDynamoDBJson($ctx.args.barId)
        }
    },
}
## Response

#if($ctx.result.items.size() > 0)
  $util.toJson($ctx.result.items[0])
#else
  null
#end

Resolver for Mutation.createBar: BarTable

Update the key only, leave the rest as it is.

"key": {
  "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},

Resolver for Mutation.createUser: UserTable

Update the key only, leave the rest as it is.

"key": {
  "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},

DynamoDB

From the AWS AppSync console select Data Sources and find the BarMember table. Create 2 indexes for this table, barId-index, and userId-index, with no sort keys and default settings. See example below:

create-index

Google Places API

Sign up to Google Places and get an API key.

google-places

Mapbox API

Sign up to Mapbox and get an API key.

mapbox

Add API keys

This project uses react-native-config to store API keys in an environment file. Creata a .env file in the project root directory, then add your Google Places API and Mapbox API keys here.

GOOGLE_PLACES_API_KEY=YOUR_KEY_GOES_HERE
MAPBOX_ACCESS_TOKEN=YOUR_KEY_GOES_HERE

Launch

Run on ios device:
react-native run-ios --device "iPhone X"

Run on android device:
adb devices
react-native run-android --deviceId "myDeviceId"

Run on ios: react-native run-ios
Run on android: react-native run-android

If you are getting build errors try the following:

  • rebuild
  • delete app from simulator or device and rebuild
  • erase all content and settings from simulator and rebuild
  • clean build folder in xcode and rebuild
  • rm -rf ~/.rncache
  • rm -rf node_modules && rm -rf ~/.rncache && yarn

If you are getting any yellow box warnings when entering text into the SearchBar, disable remote debugging.

Flow

I am in the process of migrating from PropTypes to using Flow. To check for Flow errors:

yarn run flow start
yarn run flow status

Testing with Jest and Enzyme

I have started adding tests using Jest and Enzyme. To check the current tests are working:

yarn run test

Additional information

React Apollo

In this app I have chosen to primarily use React Apollo's graphql higher order component to connect queries, mutations, and subscriptions to the app. With React Apollo 2.1 you can use the new Query, Mutation, and Subscription components instead.

AWS Amplify API

In the Auth section of this app I have used AWS Amplify's API and graphqlOperation helper. This API is effectively an alternative GraphQL client for working with queries, mutations, and subscriptions. It is great to use when you do not need offline support and the more advanced features of React Apollo.

AWS Appsync

With AWS AppSync you can combine React Apollo's graphql higher order component with the graphqlMutation (offline support) and buildSubscription helpers. These take away some of the boilerplate code normally required to implement mutations and subscriptions. I have used the buildSubscription helper in this app.

About

React Native Bars App: AWS Amplify, AWS AppSync, AWS Cognito, Google Places, Mapbox

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published