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
- enable offline support
- add pagination
- @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
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.
Clone the repo:
git clone https://github.com/pjay79/BarsAppAmplify.git
Change to the project folder:
cd BarsAppAmplify
Add dependencies:
npm install
or yarn
Sign up to AWS Free Tier:
https://aws.amazon.com/free/
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 init
(in the project folder)
amplify add auth
(update: I have now set MFA to optional to get the password reset functionality working)
amplify add api
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]
}
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.
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 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
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.
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
}
## 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))
}
## 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
## 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))
}
## 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
## 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
Update the key only, leave the rest as it is.
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},
Update the key only, leave the rest as it is.
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},
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:
Sign up to Google Places and get an API key.
Sign up to Mapbox and get an API key.
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
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.
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
I have started adding tests using Jest and Enzyme. To check the current tests are working:
yarn run test
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.
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.
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.