├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appsync ├── resolver-mappings │ ├── Mutation.addInventory.request │ ├── Mutation.addInventory.response │ ├── Mutation.createOrder.request │ ├── Mutation.createOrder.response │ ├── Mutation.registerUser.request │ ├── Mutation.registerUser.response │ ├── Mutation.updateUserBalance.request │ ├── Mutation.updateUserBalance.response │ ├── Query.getMe.request │ ├── Query.getMe.response │ ├── Query.getMyOrders.request │ ├── Query.getMyOrders.response │ ├── Query.getOrder.request │ ├── Query.getOrder.response │ ├── Query.listInventory.request │ ├── Query.listInventory.response │ ├── User.orders.request │ └── User.orders.response ├── resolvers.json └── schema.graphql ├── img ├── 00.png ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png ├── 08.png ├── 09.png ├── 10.png ├── 11.png ├── 12.png └── 13.png ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── sam ├── .gitignore ├── UnicornLoyalty │ ├── index.js │ └── lambda-payloads.json ├── cfn-deploy.yml └── template.yaml └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Order.js ├── Points.js └── Unicorns.js ├── images ├── shadowfax.png ├── unicorn.png └── unicorn_small.png ├── index.css ├── index.js └── registerServiceWorker.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /build 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | #awsmobilejs 46 | /awsmobilejs/* 47 | /src/aws-exports.js 48 | /awsmobilejs -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-serverless-appsync-loyalty/issues), or [recently closed](https://github.com/aws-samples/aws-serverless-appsync-loyalty/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-serverless-appsync-loyalty/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-serverless-appsync-loyalty/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Unicorn Loyalty: E-Commerce Serverless GraphQL Loyalty Sample App 2 | 3 | Unicorn Loyalty is a new startup that provides fantastic unicorns for customers. The business just started and it's giving away 1000 Unicoin Points for new customers to purchase the unicorns available on inventory. 4 | 5 | (Live Coding walk-through video covering Lambda and AppSync manual setup available on https://www.youtube.com/watch?v=WOQIqRVzkas or https://www.twitch.tv/videos/288401222) 6 | 7 | ### Behind the Scenes 8 | 9 | ![Architecture](img/00.png) 10 | 11 | * AWS AppSync 12 | * AWS Lambda 13 | * Amazon DynamoDB 14 | * Amazon Cognito User Pools 15 | * Amazon Pinpoint 16 | * No Servers! 17 | 18 | #### Prerequisites 19 | 20 | * [AWS Account](https://aws.amazon.com/mobile/details) with appropriate permissions to create the related resources 21 | * [NodeJS v8.10+](https://nodejs.org/en/download/) with [NPM](https://docs.npmjs.com/getting-started/installing-node) 22 | * [AWS Mobile CLI](https://github.com/aws/awsmobile-cli) `(npm install -g awsmobile-cli)` 23 | * [AWS Amplify](https://aws.github.io/aws-amplify/media/react_guide) `(npm install -g aws-amplify-react)` 24 | * [create-react-app](https://github.com/facebook/create-react-app) `(npm install -g create-react-app)` 25 | 26 | #### Optional 27 | 28 | * [AWS Cloud9](https://aws.amazon.com/cloud9/) : 29 | We assume you are using Cloud9 to build this application. You can optionally choose to use any IDE/Text Editor such as Atom or VS Code, in that case you should use the [AWS SAM CLI](https://github.com/awslabs/aws-sam-cli) to deploy Lambda and the DynamoDB tables. 30 | 31 | ### Initial Setup - Mobile CLI and Amplify 32 | 33 | Create a Cloud 9 environment and execute: 34 | 35 | $ create-react-app unicorn-loyalty 36 | $ cd unicorn-loyalty 37 | 38 | 39 | Install and use the latest LTS Node version: 40 | 41 | 42 | $ nvm i v8 43 | 44 | 45 | Set up your AWS resources with the AWS Mobile CLI: 46 | 47 | 48 | $ awsmobile init 49 | 50 | 51 | Chose the default options, make sure you are logged in with a user that has administrator access and click the resulting link to create an IAM user for the Mobile CLI by selecting OPEN: 52 | 53 | ![Mobile CLI Setup](img/01.png) 54 | 55 | Follow the steps in the IAM Console with default options then use the generated credentials to configure the access in the Mobile CLI: 56 | 57 | ![Mobile CLI Permissions](img/02.png) 58 | 59 | Now let's add the features we need for our application (User Sign In, Analytics, Hosting and AppSync): 60 | 61 | ![Mobile CLI Features](img/03.png) 62 | 63 | Execute the following command to commit the changes: 64 | 65 | $ awsmobile push 66 | 67 | To test if everything is working, open App.js and let's add 4 extra lines of code to add AuthN/Z with MFA (withAuthenticator HOC). Replace the existing code with: 68 | 69 | ```javascript 70 | import React, { Component } from 'react'; 71 | import logo from './logo.svg'; 72 | import './App.css'; 73 | import Amplify from 'aws-amplify'; 74 | import { withAuthenticator } from 'aws-amplify-react'; 75 | import aws_exports from './aws-exports'; // specify the location of aws-exports.js file on your project 76 | Amplify.configure(aws_exports); 77 | 78 | class App extends Component { 79 | render() { 80 | return ( 81 |
82 |
83 | logo 84 |

Welcome to React

85 |
86 |

87 | To get started, edit src/App.js and save to reload. 88 |

89 |
90 | ); 91 | } 92 | } 93 | 94 | export default withAuthenticator(App, { includeGreetings: true }); 95 | ``` 96 | Now execute: 97 | 98 | 99 | $ awsmobile run 100 | 101 | 102 | Then click on PREVIEW -> PREVIEW RUNNING APPLICATION on Cloud9 and sign up a user: 103 | 104 | ![Mobile CLI Features](img/04.png) 105 | 106 | Download all files from the Github repo. Upload them to your Cloud9 workspace (FILE -> UPLOAD LOCAL FILES), overwriting the files in the local React app folder: 107 | 108 | ![Uploading Files](img/05.png) 109 | 110 | ### Lambda Setup 111 | 112 | From Cloud9 select the AWS Resources tab on the right, you'll find a local Lambda funcion under the ```sam``` folder called ```UnicornLoyalty```. Right click and select EDIT CONFIG to check the related SAM template and EDIT FUNCTION to check the Lambda code: 113 | 114 | ![Lambda SAM Config](img/06.png) 115 | 116 | By default the 1000 Unicoins give away special is valid until the last day of 2018. Edit the expiry date accordingly if you want to modify the deadline: 117 | 118 | ```javascript 119 | let expiry = new Date('2018.12.31').getTime() / 1000; 120 | ``` 121 | 122 | On the same menu click DEPLOY (or execute ```sam package/deploy``` with the SAM CLI it you're not on Cloud9). The SAM Template will deploy a Lambda function and 3 DynamoDB tables. Lambda will interact directly with the Users table by detecting newly registered users to make sure they will only get the 1000 Unicoin Points special before the expiry date as well as manage and update the user unicoins/points balance when a order is placed. The other tables will be used directly by AppSync. 123 | 124 | ### AppSync Setup 125 | 126 | The Mobile CLI creates a sample Event API on AppSync by default. We wont use that. Go to the AppSync console and paste the contents of the file ```appsync/schema.graphql``` in the SCHEMA setion: 127 | 128 | ![GraphQL Schema](img/07.png) 129 | 130 | Go to DATA SOURCES, delete the 2 tables from the sample. Now create 3 data sources as follows, pointing to the Items and Orders tables and the Lambda function created earlier : 131 | 132 | ![AppSync Data Sources](img/08.png) 133 | 134 | Back to Cloud9, execute the following command to retrieve the AppSync changes: 135 | 136 | $ awsmobile pull 137 | 138 | Go to the folder ```awsmobilejs/backend/appsync``` and delete the file ```resolvers.json``` and the folder ```resolver-mappings```. 139 | 140 | Now go to the folder ```appsync``` in the root of the application directory and copy the file ```resolvers.json``` and the folder ```resolver-mappings``` to the previous folder ```awsmobilejs/backend/appsync```, replacing the deleted files. 141 | 142 | Next step is to configure AppSync authentication. Execute the following command and select the options: 143 | 144 | 145 | $ awsmobile appsync configure 146 | 147 | ? Please specify the auth type: AMAZON_COGNITO_USER_POOLS 148 | ? user pool id: 149 | ? region: 150 | ? default action: ALLOW 151 | 152 | 153 | Execute the following command to commit the changes: 154 | 155 | 156 | $ awsmobile push 157 | 158 | 159 | ### Creating Some Unicorns 160 | 161 | Open the file ```src/aws-exports.js``` generated by the Mobile CLI and copy the value of the key "aws_user_pools_web_client_id"to retrieve the App Client ID the Cognito User Pools is using to authenticate AppSync calls. 162 | 163 | Go to the AppSync console and select the QUERY section. Click LOGIN WITH USER POOLS, use the client ID you just retrieved from ```aws-exports.js``` and the credentials from the user you signed up earlier. You'll also need to provide a MFA code. 164 | 165 | Execute the following GraphQL operation (mutation) to create your first Unicorn: 166 | 167 | ```javascript 168 | mutation { 169 | addInventory(itemDescription: "Amazing Unicorn", price: 50){ 170 | itemId 171 | itemDescription 172 | price 173 | } 174 | } 175 | ``` 176 | 177 | Create as many Unicorns as you'd like by changing the values. Going to the DynamoDB console, you can confirm the Unicorns were created successfully: 178 | 179 | ![Unicorns on DynamoDB](img/09.png) 180 | 181 | ### Welcome to the Unicorn Loyalty Shop 182 | 183 | Back to Cloud9 execute: 184 | 185 | 186 | $ awsmobile run 187 | 188 | (In case of errors or missing packages, you might need to run ```npm install``` and try again) 189 | 190 | Then click on PREVIEW -> PREVIEW RUNNING APPLICATION to access the Unicorn Loyalty App: 191 | 192 | ![Unicorn Loyalty Shop - Preview](img/10.png) 193 | 194 | Finally you can publish to CloudFront and S3 with a single command: 195 | 196 | $ awsmobile publish 197 | 198 | It will automatically make the Unicorn Loyalty app available in a powerful and reliable global content delivery network backed by a S3 website: 199 | 200 | ![Unicorn Loyalty Shop - Prod](img/13.png) 201 | 202 | You can get Analitycs about usage and revenue from Amazon Pinpoint, all thanks to a couple of lines of code required by the AWS Amplify Analytics component and the to the Pinpoint Project AWS Mobile CLI creates by default: 203 | 204 | ```javascript 205 | Analytics.record({ 206 | name: 'unicornsPurchase', 207 | attributes: {}, 208 | metrics: { totalOrder: order.totalOrder } 209 | }); 210 | Analytics.record('_monetization.purchase', { 211 | _currency: 'USD', 212 | _product_id: order.itemId, 213 | }, { 214 | _item_price: order.unitPrice, 215 | _quantity: order.count, 216 | }) 217 | ``` 218 | 219 | * Usage Data: 220 | ![Unicorn Analytics](img/11.png) 221 | 222 | * Revenue Data: 223 | ![Unicorn Analytics](img/12.png) 224 | 225 | ## Feel like a challenge? What's next? 226 | 227 | We added the backend logic to get all orders from the current user: 228 | 229 | ```graphql 230 | query { 231 | getMyOrders{ 232 | orderId 233 | itemId 234 | count 235 | date 236 | totalOrder 237 | } 238 | } 239 | ``` 240 | 241 | Alternatively, you can get the same result using the relation between the User type (Users Table) and the Order type (Orders Table) by querying the User ID: 242 | 243 | ```graphql 244 | query { 245 | getMe(userId:""){ 246 | orders{ 247 | orderId 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | We also added the backend logic to get the items in a specific order by querying the Order ID: 254 | 255 | ```graphql 256 | query { 257 | getOrder(orderId:""){ 258 | orderId 259 | itemId 260 | count 261 | date 262 | totalOrder 263 | } 264 | } 265 | ``` 266 | 267 | Using AWS Amplify implement a new feature to the Unicorn Loyalty app so users can get information on all the orders they placed as well as Unicorns that were purchased in a previous order. Bonus points if implemented with the built-in pagination support. 268 | 269 | Go Build with Serverless GraphQL! 270 | 271 | ## License Summary 272 | 273 | This sample code is made available under a modified MIT license. See the LICENSE file. 274 | -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.addInventory.request: -------------------------------------------------------------------------------- 1 | ## Below example shows how to create an object from all provided GraphQL arguments 2 | ## The primary key of the obejct is a randomly generated UUD using the $util.autoId() utility 3 | ## Other utilities include $util.matches() for regular expressions, $util.time.nowISO8601() or 4 | ## $util.time.nowEpochMilliSeconds() for timestamps, and even List or Map helpers like 5 | ## $util.list.copyAndRetainAll() $util.map.copyAndRemoveAllKeys() for shallow copies 6 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#utility-helpers-in-util 7 | 8 | { 9 | "version" : "2017-02-28", 10 | "operation" : "PutItem", 11 | "key" : { 12 | ## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id) 13 | "itemId": $util.dynamodb.toDynamoDBJson($util.autoId()), 14 | }, 15 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args) 16 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.addInventory.response: -------------------------------------------------------------------------------- 1 | ## Pass back the result from DynamoDB. ** 2 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.createOrder.request: -------------------------------------------------------------------------------- 1 | ## Below example shows how to create an object from all provided GraphQL arguments 2 | ## The primary key of the obejct is a randomly generated UUD using the $util.autoId() utility 3 | ## Other utilities include $util.matches() for regular expressions, $util.time.nowISO8601() or 4 | ## $util.time.nowEpochMilliSeconds() for timestamps, and even List or Map helpers like 5 | ## $util.list.copyAndRetainAll() $util.map.copyAndRemoveAllKeys() for shallow copies 6 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#utility-helpers-in-util 7 | 8 | { 9 | "version" : "2017-02-28", 10 | "operation" : "PutItem", 11 | "key" : { 12 | "orderId" : { "S" : "${context.arguments.orderId}" } 13 | }, 14 | "attributeValues" : { 15 | "itemId": { "S": "${context.arguments.itemId}" }, 16 | "userId": { "S": "${context.identity.sub}" }, 17 | "totalOrder": { "N": "${context.arguments.totalOrder}" }, 18 | "date": { "S": "$util.time.nowFormatted("dd-MM-yyyy")" }, 19 | "count": { "S": "${context.arguments.count}" } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.createOrder.response: -------------------------------------------------------------------------------- 1 | ## Pass back the result from DynamoDB. ** 2 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.registerUser.request: -------------------------------------------------------------------------------- 1 | #** 2 | The value of 'payload' after the template has been evaluated 3 | will be passed as the event to AWS Lambda. 4 | *# 5 | { 6 | "version" : "2017-02-28", 7 | "operation": "Invoke", 8 | "payload": { 9 | "field": "registerUser", 10 | "arguments": $utils.toJson($context.args) 11 | } 12 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.registerUser.response: -------------------------------------------------------------------------------- 1 | $util.toJson($context.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.updateUserBalance.request: -------------------------------------------------------------------------------- 1 | #** 2 | The value of 'payload' after the template has been evaluated 3 | will be passed as the event to AWS Lambda. 4 | *# 5 | { 6 | "version" : "2017-02-28", 7 | "operation": "Invoke", 8 | "payload": { 9 | "field": "updateBalance", 10 | "arguments": $utils.toJson($context.args) 11 | } 12 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Mutation.updateUserBalance.response: -------------------------------------------------------------------------------- 1 | $util.toJson($context.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getMe.request: -------------------------------------------------------------------------------- 1 | #** 2 | The value of 'payload' after the template has been evaluated 3 | will be passed as the event to AWS Lambda. 4 | *# 5 | { 6 | "version" : "2017-02-28", 7 | "operation": "Invoke", 8 | "payload": { 9 | "field": "getUser", 10 | "arguments": $utils.toJson($context.args) 11 | } 12 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getMe.response: -------------------------------------------------------------------------------- 1 | $util.toJson($context.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getMyOrders.request: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "2017-02-28", 3 | "operation" : "Scan", 4 | "filter" : { 5 | "expression": "#userId = :userId", 6 | "expressionNames": { 7 | "#userId": "userId" 8 | }, 9 | "expressionValues" : { 10 | ":userId" : {"S": "$ctx.identity.sub"} 11 | } 12 | }, 13 | "limit": $util.defaultIfNull($ctx.args.first, 20), 14 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null)) 15 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getMyOrders.response: -------------------------------------------------------------------------------- 1 | #** 2 | Return a flat list of results from a Query or Scan operation. 3 | *# 4 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getOrder.request: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "2017-02-28", 3 | "operation" : "Scan", 4 | "filter" : { 5 | "expression": "#userId = :userId AND #orderId = :orderId", 6 | "expressionNames": { 7 | "#userId": "userId", 8 | "#orderId": "orderId" 9 | }, 10 | "expressionValues" : { 11 | ":userId" : {"S": "$ctx.identity.sub"}, 12 | ":orderId" : $util.dynamodb.toDynamoDBJson($ctx.args.orderId) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.getOrder.response: -------------------------------------------------------------------------------- 1 | ## Pass back the result from DynamoDB. ** 2 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.listInventory.request: -------------------------------------------------------------------------------- 1 | ## Below example will return all items in a table using a Scan 2 | ## Filtering conditions can be optionally added to scans with a "filter" and an "expression", however 3 | ## if possible it is best practice to use a Query and/or Index for frequent conditionals 4 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#aws-appsync-resolver-mapping-template-reference-dynamodb-scan 5 | ## You can also paginate through records in a table by using "nextToken" and "limit", which can be 6 | ## arguments passed from your GraphQL query in a client application (you can uncomment out below) 7 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/configuring-resolvers.html#advanced-resolvers 8 | 9 | { 10 | "version" : "2017-02-28", 11 | "operation" : "Scan", 12 | "limit": $util.defaultIfNull($ctx.args.first, 20), 13 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null)) 14 | } -------------------------------------------------------------------------------- /appsync/resolver-mappings/Query.listInventory.response: -------------------------------------------------------------------------------- 1 | ## Pass back the result from DynamoDB. ** 2 | $util.toJson($ctx.result) -------------------------------------------------------------------------------- /appsync/resolver-mappings/User.orders.request: -------------------------------------------------------------------------------- 1 | ## Below example shows how to look up an item with a Primary Key of "id" from GraphQL arguments 2 | ## The helper $util.dynamodb.toDynamoDBJson automatically converts to a DynamoDB formatted request 3 | ## There is a "context" object with arguments, identity, headers, and parent field information you can access. 4 | ## It also has a shorthand notation avaialable: 5 | ## - $context or $ctx is the root object 6 | ## - $ctx.arguments or $ctx.args contains arguments 7 | ## - $ctx.identity has caller information, such as $ctx.identity.username 8 | ## - $ctx.request.headers contains headers, such as $context.request.headers.xyz 9 | ## - $ctx.source is a map of the parent field, for instance $ctx.source.xyz 10 | ## Read more: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html 11 | 12 | { 13 | "version" : "2017-02-28", 14 | "operation" : "Scan", 15 | "filter" : { 16 | "expression": "#userId = :userId", 17 | "expressionNames": { 18 | "#userId": "userId" 19 | }, 20 | "expressionValues" : { 21 | ":userId" : { "S":"${context.source.userId}"} 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /appsync/resolver-mappings/User.orders.response: -------------------------------------------------------------------------------- 1 | #** 2 | Return a flat list of results from a Query or Scan operation. 3 | *# 4 | $util.toJson($ctx.result.items) -------------------------------------------------------------------------------- /appsync/resolvers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "typeName": "Mutation", 4 | "fieldName": "addInventory", 5 | "dataSourceName": "Items", 6 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.addInventory.request", 7 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.addInventory.response" 8 | }, 9 | { 10 | "typeName": "Mutation", 11 | "fieldName": "createOrder", 12 | "dataSourceName": "Orders", 13 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.createOrder.request", 14 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.createOrder.response" 15 | }, 16 | { 17 | "typeName": "Mutation", 18 | "fieldName": "registerUser", 19 | "dataSourceName": "Users", 20 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.registerUser.request", 21 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.registerUser.response" 22 | }, 23 | { 24 | "typeName": "Mutation", 25 | "fieldName": "updateUserBalance", 26 | "dataSourceName": "Users", 27 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.updateUserBalance.request", 28 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Mutation.updateUserBalance.response" 29 | }, 30 | { 31 | "typeName": "Query", 32 | "fieldName": "getMe", 33 | "dataSourceName": "Users", 34 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMe.request", 35 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMe.response" 36 | }, 37 | { 38 | "typeName": "Query", 39 | "fieldName": "getMyOrders", 40 | "dataSourceName": "Orders", 41 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMyOrders.request", 42 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getMyOrders.response" 43 | }, 44 | { 45 | "typeName": "Query", 46 | "fieldName": "getOrder", 47 | "dataSourceName": "Orders", 48 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.getOrder.request", 49 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.getOrder.response" 50 | }, 51 | { 52 | "typeName": "Query", 53 | "fieldName": "listInventory", 54 | "dataSourceName": "Items", 55 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:Query.listInventory.request", 56 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:Query.listInventory.response" 57 | }, 58 | { 59 | "typeName": "User", 60 | "fieldName": "orders", 61 | "dataSourceName": "Orders", 62 | "requestMappingTemplate": "{managed-by-awsmobile-cli}:User.orders.request", 63 | "responseMappingTemplate": "{managed-by-awsmobile-cli}:User.orders.response" 64 | } 65 | ] -------------------------------------------------------------------------------- /appsync/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | 7 | type Query { 8 | getMe(userId: ID! ): User 9 | listInventory(after: String,first: Int ): ItemsConnection 10 | getOrder(orderId: ID! ): OrdersConnection 11 | getMyOrders(after: String,first: Int ): OrdersConnection 12 | } 13 | 14 | type Mutation { 15 | registerUser(userId: ID!,username: String! ): User 16 | addInventory(itemDescription: String!,price: Int! ): Item 17 | updateUserBalance(userId: ID!,username: String!,points: Int! ): User 18 | createOrder(orderId: ID!,itemId: ID!,date: String,count: Int,totalOrder: Int ): Order 19 | } 20 | 21 | type Subscription { 22 | subscribeToPoints: User 23 | @aws_subscribe(mutations: ["updateUserBalance"]) 24 | } 25 | 26 | type User { 27 | userId: ID! 28 | username: String 29 | points: Int 30 | orders: [Order] 31 | } 32 | 33 | type Item { 34 | itemId: ID! 35 | itemDescription: String! 36 | price: Int! 37 | count: Int 38 | } 39 | 40 | type ItemsConnection { 41 | items: [Item] 42 | nextToken: String 43 | } 44 | 45 | type Order { 46 | orderId: ID! 47 | itemId: ID! 48 | userId: ID 49 | date: String 50 | count: Int 51 | totalOrder: Int 52 | } 53 | 54 | type OrdersConnection { 55 | items: [Order] 56 | nextToken: String 57 | } 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /img/00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/00.png -------------------------------------------------------------------------------- /img/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/01.png -------------------------------------------------------------------------------- /img/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/02.png -------------------------------------------------------------------------------- /img/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/03.png -------------------------------------------------------------------------------- /img/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/04.png -------------------------------------------------------------------------------- /img/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/05.png -------------------------------------------------------------------------------- /img/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/06.png -------------------------------------------------------------------------------- /img/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/07.png -------------------------------------------------------------------------------- /img/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/08.png -------------------------------------------------------------------------------- /img/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/09.png -------------------------------------------------------------------------------- /img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/10.png -------------------------------------------------------------------------------- /img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/11.png -------------------------------------------------------------------------------- /img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/12.png -------------------------------------------------------------------------------- /img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/img/13.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "aws-amplify": "^0.4.4", 7 | "aws-amplify-react": "^0.1.50", 8 | "bootstrap": "^4.1.1", 9 | "react": "^16.4.1", 10 | "react-dom": "^16.4.1", 11 | "react-scripts": "1.1.4" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Unicorn Loyalty 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /sam/.gitignore: -------------------------------------------------------------------------------- 1 | .application.json 2 | -------------------------------------------------------------------------------- /sam/UnicornLoyalty/index.js: -------------------------------------------------------------------------------- 1 | let AWS = require('aws-sdk'); 2 | let dynamo = new AWS.DynamoDB.DocumentClient(); 3 | let table = process.env.TABLE_NAME; 4 | 5 | exports.handler = (event, context, callback) => { 6 | let now = Date.now()/1000; 7 | let expiry = new Date('2018.12.31').getTime()/1000; 8 | let getParams = { 9 | TableName: table, 10 | Key:{ 11 | "userId": event.arguments.userId 12 | } 13 | }; 14 | let putParams = { 15 | TableName:table, 16 | Item:{ 17 | "userId": event.arguments.userId, 18 | "username": event.arguments.username, 19 | "points": 1000 20 | } 21 | }; 22 | if (expiry < now){ 23 | putParams.Item.points = 0; 24 | } 25 | switch(event.field){ 26 | case "getUser": 27 | dynamo.get(getParams, function(err,data){ 28 | if (err){ 29 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 30 | callback(err); 31 | } else if (data.Item == undefined) { 32 | let result = { 33 | "userId": "", 34 | "username": "", 35 | "points": 1000 36 | }; 37 | callback(null,result); 38 | } else { 39 | console.log("User Exists: ", JSON.stringify(data,null,2)); 40 | let result = { 41 | "userId": data.Item.userId, 42 | "username": data.Item.username, 43 | "points": data.Item.points 44 | }; 45 | callback(null,result); 46 | } 47 | }); 48 | break; 49 | case "registerUser": 50 | dynamo.put(putParams, function(err,data){ 51 | if (err){ 52 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 53 | callback(err); 54 | } else { 55 | console.log("User Added: ", JSON.stringify(data,null,2)); 56 | let result = putParams.Item; 57 | callback(null,result); 58 | } 59 | }); 60 | break; 61 | case "updateBalance": 62 | putParams.Item.points = event.arguments.points; 63 | dynamo.put(putParams, function(err,data){ 64 | if (err){ 65 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 66 | callback(err); 67 | } else { 68 | console.log("Balance Updated: ", JSON.stringify(data,null,2)); 69 | let result = { 70 | "userId": event.arguments.userId, 71 | "username": event.arguments.username, 72 | "points": event.arguments.points 73 | } 74 | callback(null,result); 75 | } 76 | }); 77 | break; 78 | default: 79 | callback("Unknown field, unable to resolve" + event.field, null); 80 | break; 81 | } 82 | }; -------------------------------------------------------------------------------- /sam/UnicornLoyalty/lambda-payloads.json: -------------------------------------------------------------------------------- 1 | { 2 | "UnicornLoyalty": { 3 | "lambda": {} 4 | } 5 | } -------------------------------------------------------------------------------- /sam/cfn-deploy.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: GraphQL Serverless e-Commerce Sample 3 | Parameters: 4 | userPoolId: 5 | Type: String 6 | Description: User Pool ID associated with this project 7 | Outputs: 8 | UnicornApiId: 9 | Description: Unique AWS AppSync GraphQL API Identifier 10 | Value: !GetAtt unicornApi.ApiId 11 | UnicornApiUrl: 12 | Description: The Endpoint URL of your GraphQL API. 13 | Value: !GetAtt unicornApi.GraphQLUrl 14 | Resources: 15 | UnicornFunction: 16 | Type: 'AWS::Lambda::Function' 17 | Properties: 18 | Handler: index.handler 19 | Runtime: nodejs10.x 20 | Code: 21 | ZipFile: !Sub | 22 | let AWS = require('aws-sdk'); 23 | let dynamo = new AWS.DynamoDB.DocumentClient(); 24 | let table = process.env.TABLE_NAME; 25 | 26 | exports.handler = (event, context, callback) => { 27 | let now = Date.now()/1000; 28 | let expiry = new Date('2018.12.31').getTime()/1000; 29 | let getParams = { 30 | TableName: table, 31 | Key:{ 32 | "userId": event.arguments.userId 33 | } 34 | }; 35 | let putParams = { 36 | TableName:table, 37 | Item:{ 38 | "userId": event.arguments.userId, 39 | "username": event.arguments.username, 40 | "points": 1000 41 | } 42 | }; 43 | if (expiry < now){ 44 | putParams.Item.points = 0; 45 | } 46 | switch(event.field){ 47 | case "getUser": 48 | dynamo.get(getParams, function(err,data){ 49 | if (err){ 50 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 51 | callback(err); 52 | } else if (data.Item == undefined) { 53 | let result = { 54 | "userId": "", 55 | "username": "", 56 | "points": 1000 57 | }; 58 | callback(null,result); 59 | } else { 60 | console.log("User Exists: ", JSON.stringify(data,null,2)); 61 | let result = { 62 | "userId": data.Item.userId, 63 | "username": data.Item.username, 64 | "points": data.Item.points 65 | }; 66 | callback(null,result); 67 | } 68 | }); 69 | break; 70 | case "registerUser": 71 | dynamo.put(putParams, function(err,data){ 72 | if (err){ 73 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 74 | callback(err); 75 | } else { 76 | console.log("User Added: ", JSON.stringify(data,null,2)); 77 | let result = putParams.Item; 78 | callback(null,result); 79 | } 80 | }); 81 | break; 82 | case "updateBalance": 83 | putParams.Item.points = event.arguments.points; 84 | dynamo.put(putParams, function(err,data){ 85 | if (err){ 86 | console.error("Error JSON: ", JSON.stringify(err,null,2)); 87 | callback(err); 88 | } else { 89 | console.log("Balance Updated: ", JSON.stringify(data,null,2)); 90 | let result = { 91 | "userId": event.arguments.userId, 92 | "username": event.arguments.username, 93 | "points": event.arguments.points 94 | } 95 | callback(null,result); 96 | } 97 | }); 98 | break; 99 | default: 100 | callback("Unknown field, unable to resolve" + event.field, null); 101 | break; 102 | } 103 | }; 104 | MemorySize: 128 105 | Timeout: 15 106 | Role: !GetAtt lambdaRole.Arn 107 | Environment: 108 | Variables: 109 | TABLE_NAME: 110 | Ref: UsersTable 111 | UsersTable: 112 | Type: "AWS::DynamoDB::Table" 113 | Properties: 114 | AttributeDefinitions: 115 | - 116 | AttributeName: "userId" 117 | AttributeType: "S" 118 | KeySchema: 119 | - 120 | AttributeName: "userId" 121 | KeyType: "HASH" 122 | ProvisionedThroughput: 123 | ReadCapacityUnits: "5" 124 | WriteCapacityUnits: "5" 125 | ItemsTable: 126 | Type: "AWS::DynamoDB::Table" 127 | Properties: 128 | AttributeDefinitions: 129 | - 130 | AttributeName: "itemId" 131 | AttributeType: "S" 132 | KeySchema: 133 | - 134 | AttributeName: "itemId" 135 | KeyType: "HASH" 136 | ProvisionedThroughput: 137 | ReadCapacityUnits: "5" 138 | WriteCapacityUnits: "5" 139 | OrdersTable: 140 | Type: "AWS::DynamoDB::Table" 141 | Properties: 142 | AttributeDefinitions: 143 | - 144 | AttributeName: "orderId" 145 | AttributeType: "S" 146 | - 147 | AttributeName: "itemId" 148 | AttributeType: "S" 149 | KeySchema: 150 | - 151 | AttributeName: "orderId" 152 | KeyType: "HASH" 153 | - 154 | AttributeName: "itemId" 155 | KeyType: "RANGE" 156 | ProvisionedThroughput: 157 | ReadCapacityUnits: "5" 158 | WriteCapacityUnits: "5" 159 | awsAppSyncServiceRole: 160 | Type: "AWS::IAM::Role" 161 | Properties: 162 | AssumeRolePolicyDocument: 163 | Version: "2012-10-17" 164 | Statement: 165 | - 166 | Effect: "Allow" 167 | Principal: 168 | Service: 169 | - "appsync.amazonaws.com" 170 | Action: 171 | - "sts:AssumeRole" 172 | Path: "/" 173 | lambdaRole: 174 | Type: "AWS::IAM::Role" 175 | Properties: 176 | AssumeRolePolicyDocument: 177 | Version: "2012-10-17" 178 | Statement: 179 | - 180 | Effect: "Allow" 181 | Principal: 182 | Service: 183 | - "lambda.amazonaws.com" 184 | Action: 185 | - "sts:AssumeRole" 186 | Path: "/" 187 | Policies: 188 | - PolicyName: root 189 | PolicyDocument: 190 | Version: '2012-10-17' 191 | Statement: 192 | - Effect: Allow 193 | Action: 194 | - logs:* 195 | Resource: arn:aws:logs:*:*:* 196 | dynamodbAccessPolicy: 197 | Type: "AWS::IAM::Policy" 198 | Properties: 199 | PolicyName: "dynamodb-access" 200 | PolicyDocument: 201 | Version: "2012-10-17" 202 | Statement: 203 | - 204 | Effect: "Allow" 205 | Action: "dynamodb:*" 206 | Resource: 207 | - !GetAtt ItemsTable.Arn 208 | - !GetAtt OrdersTable.Arn 209 | - !GetAtt UsersTable.Arn 210 | Roles: 211 | - 212 | Ref: "awsAppSyncServiceRole" 213 | - 214 | Ref: "lambdaRole" 215 | lambdaAccessPolicy: 216 | Type: "AWS::IAM::Policy" 217 | Properties: 218 | PolicyName: "lambda-access" 219 | PolicyDocument: 220 | Version: "2012-10-17" 221 | Statement: 222 | - 223 | Effect: "Allow" 224 | Action: "lambda:invokeFunction" 225 | Resource: 226 | - !GetAtt [ UnicornFunction, Arn ] 227 | - !Join [ '', [ !GetAtt [ UnicornFunction, Arn ], ':*' ] ] 228 | Roles: 229 | - 230 | Ref: "awsAppSyncServiceRole" 231 | unicornApi: 232 | Type: "AWS::AppSync::GraphQLApi" 233 | Properties: 234 | Name: "UnicornLoyalty" 235 | AuthenticationType: "AMAZON_COGNITO_USER_POOLS" 236 | UserPoolConfig: 237 | UserPoolId: !Ref userPoolId 238 | AwsRegion: !Ref "AWS::Region" 239 | DefaultAction: "ALLOW" 240 | usersDataSource: 241 | Type: "AWS::AppSync::DataSource" 242 | Properties: 243 | ApiId: !GetAtt unicornApi.ApiId 244 | Name: "Users" 245 | Description: "Users Lambda Data Source" 246 | Type: "AWS_LAMBDA" 247 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn 248 | LambdaConfig: 249 | LambdaFunctionArn: !GetAtt [ UnicornFunction, Arn ] 250 | itemsDataSource: 251 | Type: "AWS::AppSync::DataSource" 252 | Properties: 253 | ApiId: !GetAtt unicornApi.ApiId 254 | Name: "Items" 255 | Description: "Items DynamoDB Data Source" 256 | Type: "AMAZON_DYNAMODB" 257 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn 258 | DynamoDBConfig: 259 | TableName: !Ref ItemsTable 260 | AwsRegion: !Ref "AWS::Region" 261 | ordersDataSource: 262 | Type: "AWS::AppSync::DataSource" 263 | Properties: 264 | ApiId: !GetAtt unicornApi.ApiId 265 | Name: "Orders" 266 | Description: "Orders DynamoDB Data Source" 267 | Type: "AMAZON_DYNAMODB" 268 | ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn 269 | DynamoDBConfig: 270 | TableName: !Ref OrdersTable 271 | AwsRegion: !Ref "AWS::Region" 272 | unicornSchema: 273 | Type: "AWS::AppSync::GraphQLSchema" 274 | Properties: 275 | ApiId: !GetAtt unicornApi.ApiId 276 | Definition: | 277 | schema { 278 | query: Query 279 | mutation: Mutation 280 | subscription: Subscription 281 | } 282 | 283 | type Query { 284 | getMe(userId: ID! ): User 285 | listInventory(after: String,first: Int ): ItemsConnection 286 | getOrder(orderId: ID! ): OrdersConnection 287 | getMyOrders(after: String,first: Int ): OrdersConnection 288 | } 289 | 290 | type Mutation { 291 | registerUser(userId: ID!,username: String! ): User 292 | addInventory(itemDescription: String!,price: Int! ): Item 293 | updateUserBalance(userId: ID!,username: String!,points: Int! ): User 294 | createOrder(orderId: ID!,itemId: ID!,date: String,count: Int,totalOrder: Int ): Order 295 | } 296 | 297 | type Subscription { 298 | subscribeToPoints: User 299 | @aws_subscribe(mutations: ["updateUserBalance"]) 300 | } 301 | 302 | type User { 303 | userId: ID! 304 | username: String 305 | points: Int 306 | orders: [Order] 307 | } 308 | 309 | type Item { 310 | itemId: ID! 311 | itemDescription: String! 312 | price: Int! 313 | count: Int 314 | } 315 | 316 | type ItemsConnection { 317 | items: [Item] 318 | nextToken: String 319 | } 320 | 321 | type Order { 322 | orderId: ID! 323 | itemId: ID! 324 | userId: ID 325 | date: String 326 | count: Int 327 | totalOrder: Int 328 | } 329 | 330 | type OrdersConnection { 331 | items: [Order] 332 | nextToken: String 333 | } 334 | getMeQueryResolver: 335 | Type: "AWS::AppSync::Resolver" 336 | Properties: 337 | ApiId: !GetAtt unicornApi.ApiId 338 | TypeName: "Query" 339 | FieldName: "getMe" 340 | DataSourceName: !GetAtt usersDataSource.Name 341 | RequestMappingTemplate: | 342 | { 343 | "version" : "2017-02-28", 344 | "operation": "Invoke", 345 | "payload": { 346 | "field": "getUser", 347 | "arguments": $utils.toJson($context.args) 348 | } 349 | } 350 | ResponseMappingTemplate: | 351 | $utils.toJson($context.result) 352 | listInventoryQueryResolver: 353 | Type: "AWS::AppSync::Resolver" 354 | Properties: 355 | ApiId: !GetAtt unicornApi.ApiId 356 | TypeName: "Query" 357 | FieldName: "listInventory" 358 | DataSourceName: !GetAtt itemsDataSource.Name 359 | RequestMappingTemplate: | 360 | { 361 | "version" : "2017-02-28", 362 | "operation" : "Scan", 363 | "limit": $util.defaultIfNull($ctx.args.first, 20), 364 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null)) 365 | } 366 | ResponseMappingTemplate: | 367 | $utils.toJson($context.result) 368 | getOrderQueryResolver: 369 | Type: "AWS::AppSync::Resolver" 370 | Properties: 371 | ApiId: !GetAtt unicornApi.ApiId 372 | TypeName: "Query" 373 | FieldName: "getOrder" 374 | DataSourceName: !GetAtt ordersDataSource.Name 375 | RequestMappingTemplate: | 376 | { 377 | "version" : "2017-02-28", 378 | "operation" : "Scan", 379 | "filter" : { 380 | "expression": "#userId = :userId AND #orderId = :orderId", 381 | "expressionNames": { 382 | "#userId": "userId", 383 | "#orderId": "orderId" 384 | }, 385 | "expressionValues" : { 386 | ":userId" : {"S": "$ctx.identity.sub"}, 387 | ":orderId" : $util.dynamodb.toDynamoDBJson($ctx.args.orderId) 388 | } 389 | } 390 | } 391 | ResponseMappingTemplate: | 392 | $utils.toJson($context.result) 393 | getMyOrdersQueryResolver: 394 | Type: "AWS::AppSync::Resolver" 395 | Properties: 396 | ApiId: !GetAtt unicornApi.ApiId 397 | TypeName: "Query" 398 | FieldName: "getMyOrders" 399 | DataSourceName: !GetAtt ordersDataSource.Name 400 | RequestMappingTemplate: | 401 | { 402 | "version" : "2017-02-28", 403 | "operation" : "Scan", 404 | "filter" : { 405 | "expression": "#userId = :userId", 406 | "expressionNames": { 407 | "#userId": "userId" 408 | }, 409 | "expressionValues" : { 410 | ":userId" : {"S": "$ctx.identity.sub"} 411 | } 412 | }, 413 | "limit": $util.defaultIfNull($ctx.args.first, 20), 414 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null)) 415 | } 416 | ResponseMappingTemplate: | 417 | $utils.toJson($context.result) 418 | registerUserMutationResolver: 419 | Type: "AWS::AppSync::Resolver" 420 | Properties: 421 | ApiId: !GetAtt unicornApi.ApiId 422 | TypeName: "Mutation" 423 | FieldName: "registerUser" 424 | DataSourceName: !GetAtt usersDataSource.Name 425 | RequestMappingTemplate: | 426 | { 427 | "version" : "2017-02-28", 428 | "operation": "Invoke", 429 | "payload": { 430 | "field": "registerUser", 431 | "arguments": $utils.toJson($context.args) 432 | } 433 | } 434 | ResponseMappingTemplate: | 435 | $utils.toJson($context.result) 436 | addInventoryMutationResolver: 437 | Type: "AWS::AppSync::Resolver" 438 | Properties: 439 | ApiId: !GetAtt unicornApi.ApiId 440 | TypeName: "Mutation" 441 | FieldName: "addInventory" 442 | DataSourceName: !GetAtt itemsDataSource.Name 443 | RequestMappingTemplate: | 444 | { 445 | "version" : "2017-02-28", 446 | "operation" : "PutItem", 447 | "key" : { 448 | ## If object "id" should come from GraphQL arguments, change to $util.dynamodb.toDynamoDBJson($ctx.args.id) 449 | "itemId": $util.dynamodb.toDynamoDBJson($util.autoId()), 450 | }, 451 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args) 452 | } 453 | ResponseMappingTemplate: | 454 | $utils.toJson($context.result) 455 | updateUserBalanceMutationResolver: 456 | Type: "AWS::AppSync::Resolver" 457 | Properties: 458 | ApiId: !GetAtt unicornApi.ApiId 459 | TypeName: "Mutation" 460 | FieldName: "updateUserBalance" 461 | DataSourceName: !GetAtt usersDataSource.Name 462 | RequestMappingTemplate: | 463 | { 464 | "version" : "2017-02-28", 465 | "operation": "Invoke", 466 | "payload": { 467 | "field": "updateBalance", 468 | "arguments": $utils.toJson($context.args) 469 | } 470 | } 471 | ResponseMappingTemplate: | 472 | $utils.toJson($context.result) 473 | createOrderMutationResolver: 474 | Type: "AWS::AppSync::Resolver" 475 | Properties: 476 | ApiId: !GetAtt unicornApi.ApiId 477 | TypeName: "Mutation" 478 | FieldName: "createOrder" 479 | DataSourceName: !GetAtt ordersDataSource.Name 480 | RequestMappingTemplate: | 481 | { 482 | "version" : "2017-02-28", 483 | "operation" : "PutItem", 484 | "key" : { 485 | "orderId" : { "S" : "${context.arguments.orderId}" } 486 | }, 487 | "attributeValues" : { 488 | "itemId": { "S": "${context.arguments.itemId}" }, 489 | "userId": { "S": "${context.identity.sub}" }, 490 | "totalOrder": { "N": "${context.arguments.totalOrder}" }, 491 | "date": { "S": "$util.time.nowFormatted("dd-MM-yyyy")" }, 492 | "count": { "S": "${context.arguments.count}" } 493 | } 494 | } 495 | ResponseMappingTemplate: | 496 | $utils.toJson($context.result) 497 | -------------------------------------------------------------------------------- /sam/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: An AWS Serverless Specification template describing your function. 4 | Resources: 5 | UnicornLoyalty: 6 | Type: 'AWS::Serverless::Function' 7 | Properties: 8 | Handler: UnicornLoyalty/index.handler 9 | Runtime: nodejs10.x 10 | Description: '' 11 | MemorySize: 128 12 | Timeout: 15 13 | Policies: AmazonDynamoDBFullAccess 14 | Environment: 15 | Variables: 16 | TABLE_NAME: 17 | Ref: UsersTable 18 | UsersTable: 19 | Type: AWS::Serverless::SimpleTable 20 | Properties: 21 | PrimaryKey: 22 | Name: userId 23 | Type: String 24 | ItemsTable: 25 | Type: AWS::Serverless::SimpleTable 26 | Properties: 27 | PrimaryKey: 28 | Name: itemId 29 | Type: String 30 | OrdersTable: 31 | Type: "AWS::DynamoDB::Table" 32 | Properties: 33 | AttributeDefinitions: 34 | - 35 | AttributeName: "orderId" 36 | AttributeType: "S" 37 | - 38 | AttributeName: "itemId" 39 | AttributeType: "S" 40 | KeySchema: 41 | - 42 | AttributeName: "orderId" 43 | KeyType: "HASH" 44 | - 45 | AttributeName: "itemId" 46 | KeyType: "RANGE" 47 | ProvisionedThroughput: 48 | ReadCapacityUnits: "5" 49 | WriteCapacityUnits: "5" 50 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #222; 7 | color: white; 8 | } 9 | 10 | .App-title { 11 | font-size: 1.5em; 12 | } 13 | 14 | .App-intro { 15 | font-size: large; 16 | } 17 | 18 | .logo { 19 | height:100px; 20 | } 21 | 22 | .center { 23 | text-align: center; 24 | margin: auto; 25 | } 26 | 27 | .container { 28 | text-align: right; 29 | margin: auto; 30 | padding: 10px; 31 | border-radius: 10px; 32 | border: 1px solid black; 33 | } 34 | 35 | .img-thumbnail { 36 | border-radius: 50%; 37 | height:50px; 38 | } 39 | 40 | input { 41 | width: 50px; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | //Styling 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import "./App.css"; 5 | import shadowfax from './images/shadowfax.png'; 6 | //Components 7 | import Unicorns from "./components/Unicorns"; 8 | import Points from "./components/Points"; 9 | import Order from "./components/Order"; 10 | //Amplify 11 | import Amplify,{Auth,Analytics,API,graphqlOperation} from 'aws-amplify'; 12 | import { withAuthenticator } from 'aws-amplify-react'; 13 | import aws_exports from './aws-exports'; // specify the location of aws-exports.js file on your project 14 | Amplify.configure(aws_exports); 15 | 16 | //GraphQL Mutations 17 | const registerUser = `mutation registerUser($userId: ID!, $username: String!) { 18 | registerUser(userId: $userId, username: $username) { 19 | __typename 20 | userId 21 | username 22 | points 23 | } 24 | }`; 25 | 26 | const updateUserBalance = `mutation updateUserBalance($userId: ID!, $username: String!, $points: Int!) { 27 | updateUserBalance(userId: $userId, username: $username, points: $points) { 28 | __typename 29 | points 30 | } 31 | }`; 32 | 33 | const createOrder = `mutation createOrder($orderId:ID!,$itemId:ID!,$totalOrder:Int,$count:Int){ 34 | createOrder(orderId:$orderId,itemId:$itemId,totalOrder:$totalOrder,count:$count){ 35 | orderId 36 | itemId 37 | count 38 | totalOrder 39 | date 40 | } 41 | }` 42 | 43 | 44 | class App extends Component { 45 | 46 | constructor(props){ 47 | super(props); 48 | this.state={ 49 | user:"", 50 | sub:"", 51 | points:"", 52 | order:"", 53 | display: false 54 | } 55 | } 56 | 57 | async componentDidMount(){ 58 | //Get User Details from Cognito Token 59 | this.session = await Auth.currentSession(); 60 | console.log('Decoded Acess Token:'); 61 | console.log(JSON.stringify(this.session.accessToken.payload, null, 2)); 62 | this.setState({user: this.session.accessToken.payload.username}); 63 | this.setState({sub: this.session.accessToken.payload.sub}); 64 | //Query the current User ID 65 | const getUser = await API.graphql(graphqlOperation( 66 | `query getMe{ 67 | getMe(userId: "`+this.state.sub+`"){ 68 | __typename 69 | userId 70 | username 71 | points 72 | } 73 | }`)); 74 | //Retrieve current points balance 75 | this.setState({points: getUser.data.getMe.points}); 76 | this.userDetails = { 77 | userId: this.state.sub, 78 | username: this.state.user, 79 | points: this.state.points 80 | }; 81 | //If the User ID doesn't exist, proceed to register 82 | if (getUser.data.getMe.userId ===""){ 83 | const newUser = await API.graphql(graphqlOperation(registerUser, this.userDetails)); 84 | this.userDetails.points = newUser.data.registerUser.points; 85 | } 86 | //Update the balance, this will trigger the Subscription 87 | this.updateBalance(this.userDetails.points); 88 | } 89 | 90 | async updateBalance(points){ 91 | this.userDetails.points = points; 92 | const lastPoints = await API.graphql(graphqlOperation(updateUserBalance, this.userDetails)); 93 | console.log(lastPoints); 94 | } 95 | 96 | //Retrieve Order data from Unicorns component 97 | orderData = (details,balance,total) => { 98 | //Generate Order ID 99 | let orderId = Math.random().toString(36).substring(2, 15).toUpperCase(); 100 | //Loop through order items 101 | details.forEach((item)=>{ 102 | let orderDetails = { 103 | orderId: orderId, 104 | itemId: "", 105 | unitPrice: "", 106 | count: 0, 107 | totalOrder:0 108 | }; 109 | if (item.count !== null){ 110 | orderDetails.itemId = item.itemId; 111 | orderDetails.count = item.count; 112 | orderDetails.unitPrice = item.price; 113 | orderDetails.totalOrder = total; 114 | //Clean up and add items to Order 115 | this.createOrder(orderDetails); 116 | } 117 | }) 118 | //Update balance after purchase 119 | this.updateBalance(balance); 120 | } 121 | 122 | async createOrder(order){ 123 | //Place the order 124 | console.log(order); 125 | const putOrder = await API.graphql(graphqlOperation(createOrder, order)); 126 | console.log(JSON.stringify(putOrder)); 127 | this.setState({order: putOrder.data.createOrder}); 128 | this.setState({display: true}); //display Order component after a successfull purchase 129 | //Send Analytics data to Pinpoint 130 | Analytics.record({ 131 | name: 'unicornsPurchase', 132 | attributes: {}, 133 | metrics: { totalOrder: order.totalOrder } 134 | }); 135 | Analytics.record('_monetization.purchase', { 136 | _currency: 'USD', 137 | _product_id: order.itemId, 138 | }, { 139 | _item_price: order.unitPrice, 140 | _quantity: order.count, 141 | }) 142 | } 143 | 144 | render() { 145 | return ( 146 |
147 |
148 | Shadowfax
149 | Welcome to Unicorn Loyalty
150 | Powered by Serverless GraphQL 151 |
152 |

153 | Sign up by January 2019 and get 1000 Unicoin points for free! 154 |

155 |
156 |
157 |
158 |
159 | 160 |
161 | 162 |
163 |
164 |
165 |
166 | {this.state.display ? : null } 167 |
168 |
169 | ); 170 | } 171 | } 172 | 173 | export default withAuthenticator(App, { includeGreetings: true }); 174 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Order.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import unicorn from '../images/unicorn.png'; 3 | 4 | class Order extends Component { 5 | 6 | constructor(props){ 7 | super(props); 8 | this.state = { 9 | order: "" 10 | }; 11 | } 12 | 13 | //Receive order data from App component 14 | componentWillReceiveProps(props) { 15 | this.setState({order: props.order}); 16 | } 17 | 18 | render() { 19 | console.log(this.state.order); 20 | console.log(this.props.order); 21 | const order = [].concat(this.props.order) 22 | .map((item,i) => 23 | 24 | 25 | {item.orderId} 26 | {item.date} 27 | {item.totalOrder} 28 | 29 | 30 | ); 31 | return ( 32 |
33 |
34 |
35 |
36 | Unicorn 37 |

38 |

Order Placed!

39 | Unicorns are on the way 40 |

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {order} 50 |
OrderIDDate UniCoins
51 |
52 |
53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | export default Order; -------------------------------------------------------------------------------- /src/components/Points.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import "../App.css"; 3 | import Amplify,{API,graphqlOperation} from 'aws-amplify'; 4 | import aws_exports from '../aws-exports'; // specify the location of aws-exports.js file on your project 5 | Amplify.configure(aws_exports); 6 | 7 | //GraphQL 8 | const subscribeToPoints = `subscription subscribeToPoints { 9 | subscribeToPoints { 10 | __typename 11 | points 12 | } 13 | }`; 14 | 15 | class Points extends Component { 16 | 17 | constructor(props){ 18 | super(props); 19 | this.state = { 20 | points: "" 21 | }; 22 | } 23 | 24 | async componentDidMount(){ 25 | //Create subscription for real-time points balance update 26 | this.subscription = API.graphql(graphqlOperation(subscribeToPoints)).subscribe({ 27 | next: (event) => { 28 | console.log("Subscription: "+event.value.data); 29 | this.setState({points: event.value.data.subscribeToPoints.points}); 30 | } 31 | }); 32 | } 33 | 34 | //Set points from App Component 35 | componentWillReceiveProps(props) { 36 | this.setState({points: props.points}); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |
Balance: {this.state.points} Unicoins
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default Points; -------------------------------------------------------------------------------- /src/components/Unicorns.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import unicorn_small from '../images/unicorn_small.png'; 3 | import Amplify,{API,graphqlOperation} from 'aws-amplify'; 4 | import aws_exports from '../aws-exports'; // specify the location of aws-exports.js file on your project 5 | Amplify.configure(aws_exports); 6 | 7 | //GraphQL 8 | const listInventory = `query { 9 | listInventory{ 10 | items{ 11 | itemId 12 | itemDescription 13 | price 14 | count 15 | } 16 | } 17 | }` 18 | 19 | class Unicorns extends Component { 20 | 21 | constructor(props){ 22 | super(props); 23 | this.state={ 24 | unicorns: [] 25 | } 26 | } 27 | 28 | //Retrieve Unicorns available with details 29 | async componentDidMount() { 30 | this.unicorns = await API.graphql(graphqlOperation(listInventory)); 31 | this.setState({unicorns:this.unicorns.data.listInventory.items}); 32 | console.log(this.state.unicorns); 33 | } 34 | 35 | //Update order values on change 36 | onChange = (index, val) => { 37 | this.setState({ 38 | unicorns: this.state.unicorns.map((unicorn, i) => ( 39 | i === index ? {...unicorn, count: val} : unicorn 40 | )) 41 | }) 42 | } 43 | 44 | //Send order to main App component 45 | purchase = () => { 46 | let total = this.state.unicorns.reduce((sum, i) => (sum += i.count * i.price), 0); 47 | let balance = this.props.points; 48 | let orderDetails = this.state.unicorns; 49 | //Check balance and deny purchase if there's not enough points 50 | balance = balance - total; 51 | if (balance < 0){ 52 | alert("Not enough Unicoins :(") 53 | } else { 54 | this.props.order(orderDetails,balance,total); 55 | } 56 | } 57 | 58 | render () { 59 | return ( 60 |
61 | 62 | 63 |

64 | 65 |

66 | ) 67 | } 68 | }; 69 | 70 | //Map Unicorn Data 71 | const UnicornList = ({ unicorns, onChange }) => ( 72 |
73 | {unicorns.map((unicorn, i) => ( 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 87 |
Unicorn{unicorn.itemDescription} (Uni${unicorn.price}) onChange(i, parseInt(e.target.value) || 0)} 84 | />
88 | ))} 89 |
90 |
91 | ); 92 | 93 | //Calculate Total 94 | const Total = ({ unicorns }) => ( 95 | 96 | 97 | 103 | 104 |
98 | Total: 99 | 100 | {unicorns.reduce((sum, i) => (sum += i.count * i.price), 0) } 101 | 102 |
105 | ) 106 | 107 | 108 | export default Unicorns; -------------------------------------------------------------------------------- /src/images/shadowfax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/shadowfax.png -------------------------------------------------------------------------------- /src/images/unicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/unicorn.png -------------------------------------------------------------------------------- /src/images/unicorn_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-serverless-appsync-loyalty/6d571d81dc7835b9a34220f3c653904e48b6debb/src/images/unicorn_small.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | 10 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------