├── .gitignore ├── README.md ├── dashboard1.jpg ├── dashboard2.jpg ├── graphqlHooks.js └── header.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | AmplifyApp 2 | amplify-web-app 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building Mobile Applications with React Native & AWS Amplify 2 | 3 | In this workshop we'll learn how to build cloud-enabled mobile applications with React Native & [AWS Amplify](https://aws-amplify.github.io/). 4 | 5 | ![Amplify React Native Workshop](header.jpg) 6 | 7 | ### Topics we'll be covering: 8 | 9 | - [Authentication](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-authentication) 10 | - [GraphQL API with AWS AppSync](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-a-graphql-api-with-aws-appsync) 11 | - [Serverless Functions](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-a-serverless-function) 12 | - [REST API with a Lambda Function](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-a-rest-api) 13 | - [Analytics](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-analytics) 14 | - [Adding Storage with Amazon S3](https://github.com/dabit3/aws-amplify-workshop-react-native#working-with-storage) 15 | - [Multiple Serverless Environments](https://github.com/dabit3/aws-amplify-workshop-react-native#multiple-serverless-environments) 16 | - [Removing / Deleting Services](https://github.com/dabit3/aws-amplify-workshop-react-native#removing-services) 17 | 18 | 25 | 26 | ## Getting Started - Creating the React Native Application 27 | 28 | To get started, we first need to create a new React Native project & change into the new directory using either the [React Native CLI](https://facebook.github.io/react-native/docs/getting-started.html) (See __Building Projects With Native Code__ in the documentation) or [Expo CLI](https://facebook.github.io/react-native/docs/getting-started). 29 | 30 | We can use the React Native CLI or Expo to create a new app: 31 | 32 | ### If you're using the React Native CLI (you're not using Expo) 33 | 34 | Change into the app directory & install the dependencies 35 | 36 | ```bash 37 | $ npx react-native init RNAmplify 38 | 39 | $ cd RNAmplify 40 | 41 | $ npm install --save aws-amplify aws-amplify-react-native uuid amazon-cognito-identity-js @react-native-community/netinfo 42 | 43 | # or 44 | 45 | $ yarn add aws-amplify aws-amplify-react-native uuid amazon-cognito-identity-js @react-native-community/netinfo 46 | ``` 47 | 48 | Next, for iOS you need to install the pods: 49 | 50 | ```sh 51 | $ cd ios 52 | 53 | $ pod install --repo-update 54 | 55 | $ cd .. 56 | ``` 57 | 58 | ### If you are using Expo 59 | 60 | ```bash 61 | $ npx expo init RNAmplify 62 | 63 | > Choose a template: blank 64 | 65 | $ cd RNAmplify 66 | 67 | $ npm install --save aws-amplify aws-amplify-react-native uuid @react-native-community/netinfo 68 | 69 | # or 70 | 71 | $ yarn add aws-amplify aws-amplify-react-native uuid 72 | ``` 73 | 74 | ### Running the app 75 | 76 | Next, run the app: 77 | 78 | ```sh 79 | $ npx react-native run-ios 80 | 81 | # or if running android 82 | 83 | $ npx react-native run-android 84 | 85 | # or, if using expo 86 | 87 | $ expo start 88 | ``` 89 | 90 | ## Installing the CLI & Initializing a new AWS Amplify Project 91 | 92 | ### Installing the CLI 93 | 94 | Next, we'll install the AWS Amplify CLI: 95 | 96 | ```bash 97 | $ npm install -g @aws-amplify/cli 98 | ``` 99 | 100 | Now we need to configure the CLI with our credentials: 101 | 102 | ```js 103 | $ amplify configure 104 | ``` 105 | 106 | > If you'd like to see a video walkthrough of this configuration process, click [here](https://www.youtube.com/watch?v=fWbM5DLh25U). 107 | 108 | Here we'll walk through the `amplify configure` setup. Once you've signed in to the AWS console, continue: 109 | - Specify the AWS Region: __your preferred region__ 110 | - Specify the username of the new IAM user: __amplify-workshop-user__ 111 | > In the AWS Console, click __Next: Permissions__, __Next: Tags__, __Next: Review__, & __Create User__ to create the new IAM user. Then, return to the command line & press Enter. 112 | - Enter the access key of the newly created user: 113 | accessKeyId: __()__ 114 | secretAccessKey: __()__ 115 | - Profile Name: __amplify-workshop-user__ 116 | 117 | ### Initializing A New AWS Amplify Project 118 | 119 | > Make sure to initialize this Amplify project in the root of your new React Native application 120 | 121 | ```bash 122 | $ amplify init 123 | ``` 124 | 125 | - Enter a name for the project: __RNAmplify__ 126 | - Enter a name for the environment: __dev__ 127 | - Choose your default editor: __Visual Studio Code (or your favorite editor)__ 128 | - Please choose the type of app that you're building __javascript__ 129 | - What javascript framework are you using __react-native__ 130 | - Source Directory Path: __/__ 131 | - Distribution Directory Path: __/__ 132 | - Build Command: __npm run-script build__ 133 | - Start Command: __npm run-script start__ 134 | - Select the authentication method you want to use: __AWS profile__ 135 | - Please choose the profile you want to use: __amplify-workshop-user__ 136 | 137 | Now, the AWS Amplify CLI has iniatilized a new project & you will see a couple of new files & folders: __amplify__ & __aws-exports.js__. These files hold your project configuration. 138 | 139 | ### Configuring the React Native application 140 | 141 | The next thing we need to do is to configure our React Native application to be aware of our new AWS Amplify project. We can do this by referencing the auto-generated __aws-exports.js__ file that is now in our root folder. 142 | 143 | ### If you are using the React Native CLI (not using Expo) 144 | 145 | To configure the app, open __index.js__ and add the following code below the last import: 146 | 147 | ```js 148 | // index.js 149 | import Amplify from 'aws-amplify' 150 | import config from './aws-exports' 151 | Amplify.configure(config) 152 | ``` 153 | 154 | Now, our app is ready to start using our AWS services. 155 | 156 | ### If you are using the Expo (not using the React Native CLI) 157 | 158 | To configure the app, open __App.js__ and add the following code below the last import: 159 | 160 | ```js 161 | // App.js 162 | import Amplify from 'aws-amplify' 163 | import config from './aws-exports' 164 | Amplify.configure(config) 165 | ``` 166 | 167 | Now, our app is ready to start using our AWS services. 168 | 169 | ## Adding Authentication 170 | 171 | To add authentication, we can use the following command: 172 | 173 | ```sh 174 | $ amplify add auth 175 | ``` 176 | 177 | - Do you want to use default authentication and security configuration? __Default configuration__ 178 | - How do you want users to be able to sign in when using your Cognito User Pool? __Username__ (keep default) 179 | - Do you want to configure advanced settings? __No__ 180 | 181 | Now, we'll run the push command and the cloud resources will be created in our AWS account. 182 | 183 | ```bash 184 | $ amplify push 185 | ``` 186 | 187 | To view the AWS services any time after their creation, run the following command: 188 | 189 | ```sh 190 | $ amplify console 191 | ``` 192 | 193 | ### Using the withAuthenticator component 194 | 195 | To add authentication, we'll go into __App.js__ and first import the `withAuthenticator` HOC (Higher Order Component) from `aws-amplify-react`: 196 | 197 | ```js 198 | // App.js 199 | import { withAuthenticator } from 'aws-amplify-react-native' 200 | ``` 201 | 202 | Next, we'll wrap our default export (the App component) with the `withAuthenticator` HOC: 203 | 204 | ```js 205 | export default withAuthenticator(App, { 206 | includeGreetings: true 207 | }) 208 | ``` 209 | 210 | Now, we can run the app and see that an Authentication flow has been added in front of our App component. This flow gives users the ability to sign up & sign in. 211 | 212 | To refresh, you can use one of the following commands: 213 | 214 | ```sh 215 | # iOS Simulator 216 | CMD + d # Opens debug menu 217 | CMD + r # Reloads the app 218 | 219 | # Android Emulator 220 | CTRL + m # Opens debug menu 221 | rr # Reloads the app 222 | ``` 223 | 224 | ### Accessing User Data 225 | 226 | We can access the user's info now that they are signed in by calling `Auth.currentAuthenticatedUser()`. 227 | 228 | ```js 229 | // App.js 230 | import React from 'react'; 231 | import { 232 | SafeAreaView, 233 | StyleSheet, 234 | Text, 235 | } from 'react-native'; 236 | 237 | import { withAuthenticator } from 'aws-amplify-react-native' 238 | 239 | import { Auth } from 'aws-amplify' 240 | 241 | class App extends React.Component { 242 | async componentDidMount() { 243 | const user = await Auth.currentAuthenticatedUser() 244 | console.log('user:', user) 245 | } 246 | render() { 247 | return ( 248 | 249 | Hello World 250 | 251 | ) 252 | } 253 | } 254 | 255 | const styles = StyleSheet.create({ 256 | container: { 257 | flex: 1, 258 | justifyContent: 'center', 259 | alignItems: 'center' 260 | }, 261 | title: { 262 | fontSize: 28 263 | } 264 | }) 265 | 266 | export default withAuthenticator(App, { 267 | includeGreetings: true 268 | }) 269 | ``` 270 | 271 | ### Signing out with a custom Sign Out button 272 | 273 | We can also sign the user out using the `Auth` class & calling `Auth.signOut()`. This function returns a promise that is fulfilled after the user session has been ended & AsyncStorage is updated. 274 | 275 | Because `withAuthenticator` holds all of the state within the actual component, we must have a way to rerender the actual `withAuthenticator` component by forcing React to rerender the parent component. 276 | 277 | To do so, let's make a few updates: 278 | 279 | ```js 280 | // App.js 281 | import React from 'react'; 282 | import { 283 | SafeAreaView, 284 | StyleSheet, 285 | Text, 286 | } from 'react-native'; 287 | 288 | import { withAuthenticator } from 'aws-amplify-react-native' 289 | 290 | import { Auth } from 'aws-amplify' 291 | 292 | class App extends React.Component { 293 | async componentDidMount() { 294 | const user = await Auth.currentAuthenticatedUser() 295 | console.log('user:', user) 296 | } 297 | signOut = () => { 298 | Auth.signOut() 299 | .then(() => this.props.onStateChange('signedOut')) 300 | .catch(err => console.log('err: ', err)) 301 | } 302 | render() { 303 | return ( 304 | 305 | Hello World 306 | Sign Out 307 | 308 | ) 309 | } 310 | } 311 | 312 | const styles = StyleSheet.create({ 313 | container: { 314 | flex: 1, 315 | justifyContent: 'center', 316 | alignItems: 'center' 317 | }, 318 | title: { 319 | fontSize: 28 320 | } 321 | }) 322 | 323 | export default withAuthenticator(App); 324 | 325 | ``` 326 | 327 | ### Custom authentication strategies 328 | 329 | To view a final solution for a custom authentication strategy, check out the __AWS Amplify React Native Auth Starter__ [here](https://github.com/aws-samples/aws-amplify-auth-starters/tree/react-native#aws-amplify-react-native-auth-starter). 330 | 331 | > This section is an overview and is considered an advanced part of the workshop. If you are not comfortable writing a custom authentication flow, I would read through this section and use it as a reference in the future. If you'd like to jump to the next section, click [here](https://github.com/dabit3/aws-amplify-workshop-react-native#adding-a-graphql-api-with-aws-appsync). 332 | 333 | The `withAuthenticator` component is a really easy way to get up and running with authentication, but in a real-world application we probably want more control over how our form looks & functions. 334 | 335 | Let's look at how we might create our own authentication flow. 336 | 337 | To get started, we would probably want to create input fields that would hold user input data in the state. For instance when signing up a new user, we would probably need 3 user inputs to capture the user's username, email, & password. 338 | 339 | To do this, we could create some initial state for these values & create an event handler that we could attach to the form inputs: 340 | 341 | ```js 342 | // initial state 343 | state = { 344 | username: '', password: '', email: '' 345 | } 346 | 347 | // event handler 348 | onChangeText = (key, value) => { 349 | this.setState({ [key]: value }) 350 | } 351 | 352 | // example of usage with TextInput 353 | this.onChange('username', v)} 358 | /> 359 | ``` 360 | 361 | We'd also need to have a method that signed up & signed in users. We can us the Auth class to do thi. The Auth class has over 30 methods including things like `signUp`, `signIn`, `confirmSignUp`, `confirmSignIn`, & `forgotPassword`. Thes functions return a promise so they need to be handled asynchronously. 362 | 363 | ```js 364 | // import the Auth component 365 | import { Auth } from 'aws-amplify' 366 | 367 | // Class method to sign up a user 368 | signUp = async() => { 369 | const { username, password, email } = this.state 370 | try { 371 | await Auth.signUp({ username, password, attributes: { email }}) 372 | } catch (err) { 373 | console.log('error signing up user...', err) 374 | } 375 | } 376 | ``` 377 | 378 | ## Adding a GraphQL API with AWS AppSync 379 | 380 | To add a GraphQL API, we can use the following command: 381 | 382 | ```sh 383 | $ amplify add api 384 | ``` 385 | 386 | Answer the following questions 387 | 388 | - Please select from one of the above mentioned services __GraphQL__ 389 | - Provide API name: __RestaurantAPI__ 390 | - Choose the default authorization type for the API __API key__ 391 | - Enter a description for the API key __public__ 392 | - After how many days from now the API key should expire __365__ 393 | - Do you want to configure advanced settings for the GraphQL API __No__ 394 | - Do you have an annotated GraphQL schema? __N__ 395 | - Choose a schema template: __Single object with fields (e.g. “Todo” with ID, name, description)__ 396 | - Do you want to edit the schema now? (Y/n) __Y__ 397 | 398 | > When prompted, update the schema to the following: 399 | 400 | ```graphql 401 | type Restaurant @model { 402 | id: ID! 403 | clientId: String 404 | name: String! 405 | description: String! 406 | city: String! 407 | } 408 | ``` 409 | 410 | Next, deploy the API: 411 | 412 | ```sh 413 | amplify push 414 | 415 | ? Are you sure you want to continue? Yes 416 | ? Do you want to generate code for your newly created GraphQL API: Yes 417 | ? Choose the code generation language target: javascript 418 | ? Enter the file name pattern of graphql queries, mutations and subscriptions: ./graphql/**/*.js 419 | ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions: Yes 420 | ? Enter maximum statement depth [increase from default if your schema is deeply nested] (2) 421 | ``` 422 | 423 | ### Optional - To mock and test the API locally, you can run the mock command: 424 | 425 | ```bash 426 | $ amplify mock api 427 | ``` 428 | 429 | This should start an AppSync Mock endpoint: 430 | 431 | ``` 432 | AppSync Mock endpoint is running at http://10.219.99.136:20002 433 | ``` 434 | 435 | Open the endpoint in the browser to use the GraphiQL Editor. 436 | 437 | From here, we can now test the API. 438 | 439 | ### Adding mutations from within the GraphiQL Editor. 440 | 441 | In the GraphiQL editor, execute the following mutation to create a new restaurant in the API: 442 | 443 | ```graphql 444 | mutation createRestaurant { 445 | createRestaurant(input: { 446 | name: "Nobu" 447 | description: "Great Sushi" 448 | city: "New York" 449 | }) { 450 | id name description city 451 | } 452 | } 453 | ``` 454 | 455 | Now, let's query for the restaurant: 456 | 457 | ```graphql 458 | query listRestaurants { 459 | listRestaurants { 460 | items { 461 | id 462 | name 463 | description 464 | city 465 | } 466 | } 467 | } 468 | ``` 469 | 470 | We can even add search / filter capabilities when querying: 471 | 472 | ```graphql 473 | query searchRestaurants { 474 | listRestaurants(filter: { 475 | city: { 476 | contains: "New York" 477 | } 478 | }) { 479 | items { 480 | id 481 | name 482 | description 483 | city 484 | } 485 | } 486 | } 487 | ``` 488 | 489 | Or, get an individual restaurant by ID: 490 | 491 | ```graphql 492 | query getRestaurant { 493 | getRestaurant(id: "RESTAURANT_ID") { 494 | name 495 | description 496 | city 497 | } 498 | } 499 | ``` 500 | 501 | ### Interacting with the GraphQL API from our client application - Querying for data 502 | 503 | Now that the GraphQL API is created we can begin interacting with it! 504 | 505 | The first thing we'll do is perform a query to fetch data from our API. 506 | 507 | To do so, we need to define the query, execute the query, store the data in our state, then list the items in our UI. 508 | 509 | ```js 510 | import React from 'react'; 511 | import { 512 | SafeAreaView, 513 | View, 514 | StyleSheet, 515 | Text, 516 | } from 'react-native'; 517 | 518 | // imports from Amplify library 519 | import { withAuthenticator } from 'aws-amplify-react-native' 520 | import { API, graphqlOperation } from 'aws-amplify' 521 | 522 | // import the GraphQL query 523 | import { listRestaurants } from './graphql/queries' 524 | 525 | class App extends React.Component { 526 | // define some state to hold the data returned from the API 527 | state = { 528 | restaurants: [] 529 | } 530 | // execute the query in componentDidMount 531 | async componentDidMount() { 532 | try { 533 | const restaurantData = await API.graphql(graphqlOperation(listRestaurants)) 534 | console.log('restaurantData:', restaurantData) 535 | this.setState({ 536 | restaurants: restaurantData.data.listRestaurants.items 537 | }) 538 | } catch (err) { 539 | console.log('error fetching restaurants...', err) 540 | } 541 | } 542 | render() { 543 | return ( 544 | 545 | { 546 | this.state.restaurants.map((restaurant, index) => ( 547 | 548 | {restaurant.name} 549 | {restaurant.description} 550 | {restaurant.city} 551 | 552 | )) 553 | } 554 | 555 | ) 556 | } 557 | } 558 | 559 | const styles = StyleSheet.create({ 560 | container: { 561 | flex: 1, 562 | justifyContent: 'center', 563 | }, 564 | item: { padding: 10 }, 565 | name: { fontSize: 20 }, 566 | description: { fontWeight: '600', marginTop: 4, color: 'rgba(0, 0, 0, .5)' }, 567 | city: { marginTop: 4 } 568 | }) 569 | 570 | export default withAuthenticator(App, { includeGreetings: true }); 571 | ``` 572 | 573 | ## Performing mutations 574 | 575 | Now, let's look at how we can create mutations. The mutation we will be working with is `createRestaurant`. 576 | 577 | ```js 578 | // App.js 579 | import React from 'react'; 580 | import { 581 | SafeAreaView, 582 | View, 583 | StyleSheet, 584 | Text, 585 | TextInput, 586 | Button 587 | } from 'react-native'; 588 | 589 | // imports from Amplify library 590 | import { withAuthenticator } from 'aws-amplify-react-native' 591 | import { API, graphqlOperation } from 'aws-amplify' 592 | 593 | // import the GraphQL query 594 | import { listRestaurants } from './graphql/queries' 595 | // import the GraphQL mutation 596 | import { createRestaurant } from './graphql/mutations' 597 | 598 | // create client ID 599 | import { v4 as uuid } from 'uuid' 600 | const CLIENTID = uuid() 601 | 602 | class App extends React.Component { 603 | // add additional state to hold form state as well as restaurant data returned from the API 604 | state = { 605 | name: '', description: '', city: '', restaurants: [] 606 | } 607 | // execute the query in componentDidMount 608 | async componentDidMount() { 609 | try { 610 | const restaurantData = await API.graphql(graphqlOperation(listRestaurants)) 611 | console.log('restaurantData:', restaurantData) 612 | this.setState({ 613 | restaurants: restaurantData.data.listRestaurants.items 614 | }) 615 | } catch (err) { 616 | console.log('error fetching restaurants...', err) 617 | } 618 | } 619 | // this method calls the API and creates the mutation 620 | createRestaurant = async() => { 621 | const { name, description, city } = this.state 622 | // store the restaurant data in a variable 623 | const restaurant = { 624 | name, description, city, clientId: CLIENTID 625 | } 626 | // perform an optimistic response to update the UI immediately 627 | const restaurants = [...this.state.restaurants, restaurant] 628 | this.setState({ 629 | restaurants, 630 | name: '', description: '', city: '' 631 | }) 632 | try { 633 | // make the API call 634 | await API.graphql(graphqlOperation(createRestaurant, { 635 | input: restaurant 636 | })) 637 | console.log('item created!') 638 | } catch (err) { 639 | console.log('error creating restaurant...', err) 640 | } 641 | } 642 | // change form state then user types into input 643 | onChange = (key, value) => { 644 | this.setState({ [key]: value }) 645 | } 646 | render() { 647 | return ( 648 | 649 | this.onChange('name', v)} 652 | value={this.state.name} placeholder='name' 653 | /> 654 | this.onChange('description', v)} 657 | value={this.state.description} placeholder='description' 658 | /> 659 | this.onChange('city', v)} 662 | value={this.state.city} placeholder='city' 663 | /> 664 |