├── .browserslistrc ├── .editorconfig ├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── db-data.ts ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── firebase.json ├── firestore.rules ├── images ├── serverless-angular-play-button.png └── serverless-angular.png ├── init-db.ts ├── init-db.tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── proxy.json ├── server ├── .env ├── .gcloudignore ├── app.yaml ├── auth.ts ├── checkout.route.ts ├── database.ts ├── get-user.middleware.ts ├── main.ts ├── package-lock.json ├── package.json ├── server.ts ├── server.tsconfig.json └── stripe-webhooks.route.ts ├── src ├── app │ ├── about │ │ ├── about.component.css │ │ ├── about.component.html │ │ └── about.component.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── course-dialog │ │ ├── course-dialog.component.css │ │ ├── course-dialog.component.html │ │ └── course-dialog.component.ts │ ├── course │ │ ├── course.component.css │ │ ├── course.component.html │ │ └── course.component.ts │ ├── courses-card-list │ │ ├── courses-card-list.component.css │ │ ├── courses-card-list.component.html │ │ └── courses-card-list.component.ts │ ├── home │ │ ├── home.component.css │ │ ├── home.component.html │ │ └── home.component.ts │ ├── login │ │ ├── login.component.html │ │ ├── login.component.scss │ │ ├── login.component.spec.ts │ │ └── login.component.ts │ ├── model │ │ ├── checkout-session.model.ts │ │ ├── course.ts │ │ └── lesson.ts │ ├── services │ │ ├── checkout.service.ts │ │ ├── course.resolver.ts │ │ ├── courses.service.spec.ts │ │ ├── courses.service.ts │ │ └── db-utils.ts │ └── stripe-checkout │ │ ├── stripe-checkout.component.html │ │ ├── stripe-checkout.component.scss │ │ └── stripe-checkout.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.base.json ├── tsconfig.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "stripe-course-recording" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /server/dist 6 | /server/node_modules 7 | /server/service-accounts 8 | /tmp 9 | /out-tsc 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | tmp.json 47 | .firebase 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Angular University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Stripe Payments In Practice 3 | 4 | This repository contains the code of the [Stripe Payments In Practice Course](https://angular-university.io/course/stripe-course). 5 | 6 |  7 | 8 | 9 | # Installation pre-requisites 10 | 11 | IMPORTANT: Please make sure to use **Node 12** (LTS - Long Term Support) for this course. 12 | 13 | You can switch between node versions without having to overwrite your global installation by using a tool like [nave](https://www.npmjs.com/package/nave). 14 | 15 | npm install -g nave 16 | 17 | nave use 12.3.1 18 | 19 | node -v 20 | 12.3.1 21 | 22 | 23 | # Installing the Angular CLI 24 | 25 | With the following command the angular-cli will be installed globally in your machine: 26 | 27 | npm install -g @angular/cli 28 | 29 | 30 | # How To install this repository 31 | 32 | We can install the master branch using the following commands: 33 | 34 | git clone https://github.com/angular-university/stripe-course.git 35 | 36 | This repository is made of several separate npm modules, that are installable separately. For example, to run the au-input module, we can do the following: 37 | 38 | cd stripe-course 39 | npm install 40 | 41 | This should take a couple of minutes. If there are issues, please post the complete error message in the Questions section of the course. 42 | 43 | # To Run the Development Backend Server 44 | 45 | For enabling order fulfillment, we need a backend that Stripe can call to notify that a purchase was successful. We can start the backend with the following command: 46 | 47 | cd backend 48 | 49 | npm install 50 | 51 | npm run server 52 | 53 | This will start a small Node REST API server. 54 | 55 | # To run the Development UI Server 56 | 57 | To run the frontend part of our code, we will use the Angular CLI: 58 | 59 | npm start 60 | 61 | The application is visible at port 4200: [http://localhost:4200](http://localhost:4200) 62 | 63 | 64 | 65 | # Important 66 | 67 | This repository has multiple branches, have a look at the beginning of each section to see the name of the branch. 68 | 69 | At certain points along the course, you will be asked to checkout other remote branches other than master. You can view all branches that you have available remotely using the following command: 70 | 71 | git branch -a 72 | 73 | The remote branches have their starting in origin, such as for example 1-start. 74 | 75 | We can checkout that particular remote branch, by using the following command: 76 | 77 | git checkout -b 1-start origin/1-start 78 | 79 | It's also possible to download a ZIP file for a given branch, using the branch dropdown of this page on the top left, and then selecting the Clone or Download / Download as ZIP button. 80 | 81 | # Other Courses 82 | # Modern Angular With Signals 83 | 84 | If you are looking for the [Modern Angular With Signals Course](https://angular-university.io/course/angular-signals-course), the repo with the full code can be found here: 85 | 86 |  87 | 88 | # NgRx (with NgRx Data) - The Complete Guide 89 | 90 | If you are looking for the [Ngrx (with NgRx Data) - The Complete Guide](https://angular-university.io/course/ngrx-course), the repo with the full code can be found here: 91 | 92 |  93 | 94 | 95 | # Angular Core Deep Dive Course 96 | 97 | If you are looking for the [Angular Core Deep Dive Course](https://angular-university.io/course/angular-course), the repo with the full code can be found here: 98 | 99 |  100 | 101 | # RxJs In Practice 102 | 103 | If you are looking for the [RxJs In Practice](https://angular-university.io/course/rxjs-course), the repo with the full code can be found here: 104 | 105 |  106 | 107 | # NestJs In Practice (with MongoDB) 108 | 109 | If you are looking for the [NestJs In Practice Course](https://angular-university.io/course/nestjs-course), the repo with the full code can be found here: 110 | 111 |  112 | 113 | # Angular Testing Course 114 | 115 | If you are looking for the [Angular Testing Course](https://angular-university.io/course/angular-testing-course), the repo with the full code can be found here: 116 | 117 |  118 | 119 | # Serverless Angular with Firebase Course 120 | 121 | If you are looking for the [Serverless Angular with Firebase Course](https://angular-university.io/course/firebase-course), the repo with the full code can be found here: 122 | 123 |  124 | 125 | # Angular Universal Course 126 | 127 | If you are looking for the [Angular Universal Course](https://angular-university.io/course/angular-universal-course), the repo with the full code can be found here: 128 | 129 |  130 | 131 | # Angular PWA Course 132 | 133 | If you are looking for the [Angular PWA Course](https://angular-university.io/course/angular-pwa-course), the repo with the full code can be found here: 134 | 135 |  136 | 137 | # Angular Security Masterclass 138 | 139 | If you are looking for the [Angular Security Masterclass](https://angular-university.io/course/angular-security-course), the repo with the full code can be found here: 140 | 141 | [Angular Security Masterclass](https://github.com/angular-university/angular-security-course). 142 | 143 |  144 | 145 | # Angular Advanced Library Laboratory Course 146 | 147 | If you are looking for the Angular Advanced Course, the repo with the full code can be found here: 148 | 149 | [Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-university.io/course/angular-advanced-course). 150 | 151 |  152 | 153 | 154 | ## RxJs and Reactive Patterns Angular Architecture Course 155 | 156 | If you are looking for the RxJs and Reactive Patterns Angular Architecture Course code, the repo with the full code can be found here: 157 | 158 | [RxJs and Reactive Patterns Angular Architecture Course](https://angular-university.io/course/reactive-angular-architecture-course) 159 | 160 |  161 | 162 | 163 | ## Complete Typescript Course - Build A REST API 164 | 165 | If you are looking for the Complete Typescript 2 Course - Build a REST API, the repo with the full code can be found here: 166 | 167 | [https://angular-university.io/course/typescript-2-tutorial](https://github.com/angular-university/complete-typescript-course) 168 | 169 | [Github repo for this course](https://github.com/angular-university/complete-typescript-course) 170 | 171 |  172 | 173 | 174 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "firebase-course": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "aot": true, 15 | "outputPath": "dist", 16 | "index": "src/index.html", 17 | "main": "src/main.ts", 18 | "tsConfig": "src/tsconfig.app.json", 19 | "polyfills": "src/polyfills.ts", 20 | "assets": [ 21 | "src/assets", 22 | "src/favicon.ico" 23 | ], 24 | "styles": [ 25 | "src/styles.scss" 26 | ], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "budgets": [ 32 | { 33 | "type": "anyComponentStyle", 34 | "maximumWarning": "6kb" 35 | } 36 | ], 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ] 52 | } 53 | } 54 | }, 55 | "serve": { 56 | "builder": "@angular-devkit/build-angular:dev-server", 57 | "options": { 58 | "browserTarget": "firebase-course:build" 59 | }, 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "firebase-course:build:production" 63 | } 64 | } 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "browserTarget": "firebase-course:build" 70 | } 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "main": "src/test.ts", 76 | "karmaConfig": "./karma.conf.js", 77 | "polyfills": "src/polyfills.ts", 78 | "tsConfig": "src/tsconfig.spec.json", 79 | "scripts": [], 80 | "styles": [ 81 | "src/styles.scss" 82 | ], 83 | "assets": [ 84 | "src/assets", 85 | "src/favicon.ico" 86 | ] 87 | } 88 | }, 89 | "lint": { 90 | "builder": "@angular-devkit/build-angular:tslint", 91 | "options": { 92 | "tsConfig": [ 93 | "src/tsconfig.app.json", 94 | "src/tsconfig.spec.json" 95 | ], 96 | "exclude": [ 97 | "**/node_modules/**" 98 | ] 99 | } 100 | } 101 | } 102 | }, 103 | "firebase-course-e2e": { 104 | "root": "", 105 | "sourceRoot": "", 106 | "projectType": "application", 107 | "architect": { 108 | "e2e": { 109 | "builder": "@angular-devkit/build-angular:protractor", 110 | "options": { 111 | "protractorConfig": "./protractor.conf.js", 112 | "devServerTarget": "firebase-course:serve" 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": [ 119 | "e2e/tsconfig.e2e.json" 120 | ], 121 | "exclude": [ 122 | "**/node_modules/**" 123 | ] 124 | } 125 | } 126 | } 127 | } 128 | }, 129 | "defaultProject": "firebase-course", 130 | "schematics": { 131 | "@schematics/angular:component": { 132 | "style": "scss" 133 | }, 134 | "@schematics/angular:directive": { 135 | "prefix": "" 136 | } 137 | }, 138 | "cli": { 139 | "analytics": "b538da17-512f-4711-a708-a48cea82250a" 140 | } 141 | } -------------------------------------------------------------------------------- /db-data.ts: -------------------------------------------------------------------------------- 1 | export const COURSES: any = { 2 | 3 | 16: { 4 | id: 16, 5 | titles: { 6 | description: 'Stripe Payments In Practice', 7 | longDescription: 'Build your own ecommerce store & membership website with Firebase, Stripe and Express' 8 | }, 9 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/stripe-course.jpg', 10 | lessonsCount: 10, 11 | categories: ['BEGINNER'], 12 | seqNo: 0, 13 | url: 'stripe-course', 14 | price: 50 15 | }, 16 | 17 | 4: { 18 | id: 4, 19 | titles: { 20 | description: 'NgRx (with NgRx Data) - The Complete Guide', 21 | longDescription: 'Learn the modern Ngrx Ecosystem, including NgRx Data, Store, Effects, Router Store, Ngrx Entity, and Dev Tools.' 22 | }, 23 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png', 24 | categories: ['BEGINNER'], 25 | lessonsCount: 10, 26 | seqNo: 1, 27 | url: 'ngrx-course', 28 | promo: false, 29 | price: 50 30 | }, 31 | 32 | 14: { 33 | id: 14, 34 | titles: { 35 | description: 'NestJs In Practice (with MongoDB)', 36 | longDescription: 'Build a modern REST backend using Typescript, MongoDB and the familiar Angular API.', 37 | }, 38 | iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png', 39 | categories: ['BEGINNER'], 40 | lessonsCount: 10, 41 | seqNo: 2, 42 | url: 'nestjs-course', 43 | promo: false, 44 | price: 50 45 | }, 46 | 47 | 12: { 48 | id: 12, 49 | titles: { 50 | description: 'Angular Testing Course', 51 | longDescription: 'In-depth guide to Unit Testing and E2E Testing of Angular Applications', 52 | }, 53 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png', 54 | categories: ['BEGINNER'], 55 | seqNo: 3, 56 | url: 'angular-testing-course', 57 | lessonsCount: 10, 58 | promo: false, 59 | price: 50 60 | }, 61 | 62 | 63 | 1: { 64 | id: 1, 65 | titles: { 66 | description: 'Serverless Angular with Firebase Course', 67 | longDescription: 'Serveless Angular with Firestore, Firebase Storage & Hosting, Firebase Cloud Functions & AngularFire' 68 | }, 69 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png', 70 | lessonsCount: 10, 71 | categories: ['BEGINNER'], 72 | seqNo: 4, 73 | url: 'serverless-angular', 74 | price: 50 75 | }, 76 | 77 | 2: { 78 | id: 2, 79 | titles: { 80 | description: 'Angular Core Deep Dive', 81 | longDescription: 'A detailed walk-through of the most important part of Angular - the Core and Common modules' 82 | }, 83 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png', 84 | lessonsCount: 10, 85 | categories: ['BEGINNER'], 86 | seqNo: 5, 87 | url: 'angular-core-course', 88 | price: 50 89 | }, 90 | 91 | 3: { 92 | id: 3, 93 | titles: { 94 | description: 'RxJs In Practice Course', 95 | longDescription: 'Understand the RxJs Observable pattern, learn the RxJs Operators via practical examples' 96 | }, 97 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png', 98 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 99 | categories: ['BEGINNER'], 100 | lessonsCount: 10, 101 | seqNo: 6, 102 | url: 'rxjs-course', 103 | price: 50 104 | }, 105 | 106 | 5: { 107 | id: 5, 108 | titles: { 109 | description: 'Angular for Beginners', 110 | longDescription: 'Establish a solid layer of fundamentals, learn what\'s under the hood of Angular' 111 | }, 112 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small-v2.png', 113 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/main-logo/main-page-logo-small-hat.png', 114 | categories: ['BEGINNER'], 115 | lessonsCount: 10, 116 | seqNo: 7, 117 | url: 'angular-for-beginners', 118 | price: 50 119 | }, 120 | 121 | 6: { 122 | id: 6, 123 | titles: { 124 | description: 'Angular Security Course - Web Security Fundamentals', 125 | longDescription: 'Learn Web Security Fundamentals and apply them to defend an Angular / Node Application from multiple types of attacks.' 126 | }, 127 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png', 128 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/lock-v2.png', 129 | categories: ['ADVANCED'], 130 | lessonsCount: 11, 131 | seqNo: 8, 132 | url: 'angular-security-course', 133 | price: 50 134 | }, 135 | 136 | 7: { 137 | id: 7, 138 | titles: { 139 | description: 'Angular PWA - Progressive Web Apps Course', 140 | longDescription: 'Learn Angular Progressive Web Applications, build the future of the Web Today.' 141 | }, 142 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png', 143 | courseListIcon: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/alien.png', 144 | categories: ['ADVANCED'], 145 | lessonsCount: 8, 146 | seqNo: 9, 147 | url: 'angular-pwa-course', 148 | price: 50 149 | }, 150 | 151 | 8: { 152 | id: 8, 153 | titles: { 154 | description: 'Angular Advanced Library Laboratory: Build Your Own Library', 155 | longDescription: 'Learn Advanced Angular functionality typically used in Library Development. Advanced Components, Directives, Testing, Npm' 156 | }, 157 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png', 158 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular-advanced-lesson-icon.png', 159 | categories: ['INTERMEDIATE', 'ADVANCED'], 160 | seqNo: 10, 161 | url: 'angular-advanced-course', 162 | price: 50 163 | }, 164 | 165 | 9: { 166 | id: 9, 167 | titles: { 168 | description: 'The Complete Typescript Course', 169 | longDescription: 'Complete Guide to Typescript From Scratch: Learn the language in-depth and use it to build a Node REST API.' 170 | }, 171 | iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png', 172 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-lesson.png', 173 | categories: ['BEGINNER'], 174 | seqNo: 11, 175 | url: 'typescript-course', 176 | price: 50 177 | }, 178 | 179 | 10: { 180 | id: 10, 181 | titles: { 182 | description: 'Rxjs and Reactive Patterns Angular Architecture Course', 183 | longDescription: 'Learn the core RxJs Observable Pattern as well and many other Design Patterns for building Reactive Angular Applications.' 184 | }, 185 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png', 186 | courseListIcon: 'https://angular-academy.s3.amazonaws.com/course-logos/observables_rxjs.png', 187 | categories: ['BEGINNER'], 188 | seqNo: 12, 189 | url: 'rxjs-patterns-course', 190 | price: 50 191 | }, 192 | 193 | 11: { 194 | id: 11, 195 | titles: { 196 | description: 'Angular Material Course', 197 | longDescription: 'Build Applications with the official Angular Widget Library' 198 | }, 199 | iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/material_design.png', 200 | categories: ['BEGINNER'], 201 | seqNo: 13, 202 | url: 'angular-material-course', 203 | price: 50 204 | } 205 | 206 | }; 207 | 208 | 209 | export const LESSONS = { 210 | 211 | 1: { 212 | id: 1, 213 | 'description': 'Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step', 214 | 'duration': '4:17', 215 | 'seqNo': 1, 216 | courseId: 5 217 | }, 218 | 2: { 219 | id: 2, 220 | 'description': 'Building Your First Component - Component Composition', 221 | 'duration': '2:07', 222 | 'seqNo': 2, 223 | courseId: 5 224 | }, 225 | 3: { 226 | id: 3, 227 | 'description': 'Component @Input - How To Pass Input Data To an Component', 228 | 'duration': '2:33', 229 | 'seqNo': 3, 230 | courseId: 5 231 | }, 232 | 4: { 233 | id: 4, 234 | 'description': ' Component Events - Using @Output to create custom events', 235 | 'duration': '4:44', 236 | 'seqNo': 4, 237 | courseId: 5 238 | }, 239 | 5: { 240 | id: 5, 241 | 'description': ' Component Templates - Inline Vs External', 242 | 'duration': '2:55', 243 | 'seqNo': 5, 244 | courseId: 5 245 | }, 246 | 6: { 247 | id: 6, 248 | 'description': 'Styling Components - Learn About Component Style Isolation', 249 | 'duration': '3:27', 250 | 'seqNo': 6, 251 | courseId: 5 252 | }, 253 | 7: { 254 | id: 7, 255 | 'description': ' Component Interaction - Extended Components Example', 256 | 'duration': '9:22', 257 | 'seqNo': 7, 258 | courseId: 5 259 | }, 260 | 8: { 261 | id: 8, 262 | 'description': ' Components Tutorial For Beginners - Components Exercise !', 263 | 'duration': '1:26', 264 | 'seqNo': 8, 265 | courseId: 5 266 | }, 267 | 9: { 268 | id: 9, 269 | 'description': ' Components Tutorial For Beginners - Components Exercise Solution Inside', 270 | 'duration': '2:08', 271 | 'seqNo': 9, 272 | courseId: 5 273 | }, 274 | 10: { 275 | id: 10, 276 | 'description': ' Directives - Inputs, Output Event Emitters and How To Export Template References', 277 | 'duration': '4:01', 278 | 'seqNo': 10, 279 | courseId: 5 280 | }, 281 | 282 | 283 | // Security Course 284 | 11: { 285 | id: 11, 286 | 'description': 'Course Helicopter View', 287 | 'duration': '08:19', 288 | 'seqNo': 1, 289 | courseId: 6 290 | }, 291 | 292 | 12: { 293 | id: 12, 294 | 'description': 'Installing Git, Node, NPM and Choosing an IDE', 295 | 'duration': '04:17', 296 | 'seqNo': 2, 297 | courseId: 6 298 | }, 299 | 300 | 13: { 301 | id: 13, 302 | 'description': 'Installing The Lessons Code - Learn Why Its Essential To Use NPM 5', 303 | 'duration': '06:05', 304 | 'seqNo': 3, 305 | courseId: 6 306 | }, 307 | 308 | 14: { 309 | id: 14, 310 | 'description': 'How To Run Node In TypeScript With Hot Reloading', 311 | 'duration': '03:57', 312 | 'seqNo': 4, 313 | courseId: 6 314 | }, 315 | 316 | 15: { 317 | id: 15, 318 | 'description': 'Guided Tour Of The Sample Application', 319 | 'duration': '06:00', 320 | 'seqNo': 5, 321 | courseId: 6 322 | }, 323 | 16: { 324 | id: 16, 325 | 'description': 'Client Side Authentication Service - API Design', 326 | 'duration': '04:53', 327 | 'seqNo': 6, 328 | courseId: 6 329 | }, 330 | 17: { 331 | id: 17, 332 | 'description': 'Client Authentication Service - Design and Implementation', 333 | 'duration': '09:14', 334 | 'seqNo': 7, 335 | courseId: 6 336 | }, 337 | 18: { 338 | id: 18, 339 | 'description': 'The New Angular HTTP Client - Doing a POST Call To The Server', 340 | 'duration': '06:08', 341 | 'seqNo': 8, 342 | courseId: 6 343 | }, 344 | 19: { 345 | id: 19, 346 | 'description': 'User Sign Up Server-Side Implementation in Express', 347 | 'duration': '08:50', 348 | 'seqNo': 9, 349 | courseId: 6 350 | }, 351 | 20: { 352 | id: 20, 353 | 'description': 'Introduction To Cryptographic Hashes - A Running Demo', 354 | 'duration': '05:46', 355 | 'seqNo': 10, 356 | courseId: 6 357 | }, 358 | 21: { 359 | id: 21, 360 | 'description': 'Some Interesting Properties Of Hashing Functions - Validating Passwords', 361 | 'duration': '06:31', 362 | 'seqNo': 11, 363 | courseId: 6 364 | }, 365 | 366 | 367 | // PWA course 368 | 369 | 22: { 370 | id: 22, 371 | 'description': 'Course Kick-Off - Install Node, NPM, IDE And Service Workers Section Code', 372 | 'duration': '07:19', 373 | 'seqNo': 1, 374 | courseId: 7 375 | }, 376 | 23: { 377 | id: 23, 378 | 'description': 'Service Workers In a Nutshell - Service Worker Registration', 379 | 'duration': '6:59', 380 | 'seqNo': 2, 381 | courseId: 7 382 | }, 383 | 24: { 384 | id: 24, 385 | 'description': 'Service Workers Hello World - Lifecycle Part 1 and PWA Chrome Dev Tools', 386 | 'duration': '7:28', 387 | 'seqNo': 3, 388 | courseId: 7 389 | }, 390 | 25: { 391 | id: 25, 392 | 'description': 'Service Workers and Application Versioning - Install & Activate Lifecycle Phases', 393 | 'duration': '10:17', 394 | 'seqNo': 4, 395 | courseId: 7 396 | }, 397 | 398 | 26: { 399 | id: 26, 400 | 'description': 'Downloading The Offline Page - The Service Worker Installation Phase', 401 | 'duration': '09:50', 402 | 'seqNo': 5, 403 | courseId: 7 404 | }, 405 | 27: { 406 | id: 27, 407 | 'description': 'Introduction to the Cache Storage PWA API', 408 | 'duration': '04:44', 409 | 'seqNo': 6, 410 | courseId: 7 411 | }, 412 | 28: { 413 | id: 28, 414 | 'description': 'View Service Workers HTTP Interception Features In Action', 415 | 'duration': '06:07', 416 | 'seqNo': 7, 417 | courseId: 7 418 | }, 419 | 29: { 420 | id: 29, 421 | 'description': 'Service Workers Error Handling - Serving The Offline Page', 422 | 'duration': '5:38', 423 | 'seqNo': 8, 424 | courseId: 7 425 | }, 426 | 427 | // Serverless Angular with Firebase Course 428 | 429 | 30: { 430 | id: 30, 431 | description: 'Development Environment Setup', 432 | 'duration': '5:38', 433 | 'seqNo': 1, 434 | courseId: 1 435 | }, 436 | 437 | 31: { 438 | id: 31, 439 | description: 'Introduction to the Firebase Ecosystem', 440 | 'duration': '5:12', 441 | 'seqNo': 2, 442 | courseId: 1 443 | }, 444 | 445 | 32: { 446 | id: 32, 447 | description: 'Importing Data into Firestore', 448 | 'duration': '4:07', 449 | 'seqNo': 3, 450 | courseId: 1 451 | }, 452 | 453 | 33: { 454 | id: 33, 455 | description: 'Firestore Documents in Detail', 456 | 'duration': '7:32', 457 | 'seqNo': 4, 458 | courseId: 1 459 | }, 460 | 461 | 34: { 462 | id: 34, 463 | description: 'Firestore Collections in Detail', 464 | 'duration': '6:28', 465 | 'seqNo': 5, 466 | courseId: 1 467 | }, 468 | 469 | 35: { 470 | id: 35, 471 | description: 'Firestore Unique Identifiers', 472 | 'duration': '4:38', 473 | 'seqNo': 6, 474 | courseId: 1 475 | }, 476 | 477 | 36: { 478 | id: 36, 479 | description: 'Querying Firestore Collections', 480 | 'duration': '7:54', 481 | 'seqNo': 7, 482 | courseId: 1 483 | }, 484 | 485 | 37: { 486 | id: 37, 487 | description: 'Firebase Security Rules In Detail', 488 | 'duration': '5:31', 489 | 'seqNo': 8, 490 | courseId: 1 491 | }, 492 | 493 | 38: { 494 | id: 38, 495 | description: 'Firebase Cloud Functions In Detail', 496 | 'duration': '8:19', 497 | 'seqNo': 9, 498 | courseId: 1 499 | }, 500 | 501 | 39: { 502 | id: 39, 503 | description: 'Firebase Storage In Detail', 504 | 'duration': '7:05', 505 | 'seqNo': 10, 506 | courseId: 1 507 | }, 508 | 509 | 510 | // Angular Testing Course 511 | 512 | 40: { 513 | id: 40, 514 | description: 'Angular Testing Course - Helicopter View', 515 | 'duration': '5:38', 516 | 'seqNo': 1, 517 | courseId: 12 518 | }, 519 | 520 | 41: { 521 | id: 41, 522 | description: 'Setting Up the Development Environment', 523 | 'duration': '5:12', 524 | 'seqNo': 2, 525 | courseId: 12 526 | }, 527 | 528 | 42: { 529 | id: 42, 530 | description: 'Introduction to Jasmine, Spies and specs', 531 | 'duration': '4:07', 532 | 'seqNo': 3, 533 | courseId: 12 534 | }, 535 | 536 | 43: { 537 | id: 43, 538 | description: 'Introduction to Service Testing', 539 | 'duration': '7:32', 540 | 'seqNo': 4, 541 | courseId: 12 542 | }, 543 | 544 | 44: { 545 | id: 44, 546 | description: 'Settting up the Angular TestBed', 547 | 'duration': '6:28', 548 | 'seqNo': 5, 549 | courseId: 12 550 | }, 551 | 552 | 45: { 553 | id: 45, 554 | description: 'Mocking Angular HTTP requests', 555 | 'duration': '4:38', 556 | 'seqNo': 6, 557 | courseId: 12 558 | }, 559 | 560 | 46: { 561 | id: 46, 562 | description: 'Simulating Failing HTTP Requests', 563 | 'duration': '7:54', 564 | 'seqNo': 7, 565 | courseId: 12 566 | }, 567 | 568 | 47: { 569 | id: 47, 570 | description: 'Introduction to Angular Component Testing', 571 | 'duration': '5:31', 572 | 'seqNo': 8, 573 | courseId: 12 574 | }, 575 | 576 | 48: { 577 | id: 48, 578 | description: 'Testing Angular Components without the DOM', 579 | 'duration': '8:19', 580 | 'seqNo': 9, 581 | courseId: 12 582 | }, 583 | 584 | 49: { 585 | id: 49, 586 | description: 'Testing Angular Components with the DOM', 587 | 'duration': '7:05', 588 | 'seqNo': 10, 589 | courseId: 12 590 | }, 591 | 592 | 593 | // Ngrx Course 594 | 50: { 595 | id: 50, 596 | 'description': 'Welcome to the Angular Ngrx Course', 597 | 'duration': '6:53', 598 | 'seqNo': 1, 599 | courseId: 4 600 | 601 | }, 602 | 51: { 603 | id: 51, 604 | 'description': 'The Angular Ngrx Architecture Course - Helicopter View', 605 | 'duration': '5:52', 606 | 'seqNo': 2, 607 | courseId: 4 608 | }, 609 | 52: { 610 | id: 52, 611 | 'description': 'The Origins of Flux - Understanding the Famous Facebook Bug Problem', 612 | 'duration': '8:17', 613 | 'seqNo': 3, 614 | courseId: 4 615 | }, 616 | 53: { 617 | id: 53, 618 | 'description': 'Custom Global Events - Why Don\'t They Scale In Complexity?', 619 | 'duration': '7:47', 620 | 'seqNo': 4, 621 | courseId: 4 622 | }, 623 | 54: { 624 | id: 54, 625 | 'description': 'The Flux Architecture - How Does it Solve Facebook Counter Problem?', 626 | 'duration': '9:22', 627 | 'seqNo': 5, 628 | courseId: 4 629 | }, 630 | 55: { 631 | id: 55, 632 | 'description': 'Unidirectional Data Flow And The Angular Development Mode', 633 | 'duration': '7:07', 634 | 'seqNo': 6, 635 | courseId: 4 636 | }, 637 | 638 | 56: { 639 | id: 56, 640 | 'description': 'Dispatching an Action - Implementing the Login Component', 641 | 'duration': '4:39', 642 | 'seqNo': 7, 643 | courseId: 4 644 | }, 645 | 57: { 646 | id: 57, 647 | 'description': 'Setting Up the Ngrx DevTools - Demo', 648 | 'duration': '4:44', 649 | 'seqNo': 8, 650 | courseId: 4 651 | }, 652 | 58: { 653 | id: 58, 654 | 'description': 'Understanding Reducers - Writing Our First Reducer', 655 | 'duration': '9:10', 656 | 'seqNo': 9, 657 | courseId: 4 658 | }, 659 | 59: { 660 | id: 59, 661 | 'description': 'How To Define the Store Initial State', 662 | 'duration': '9:10', 663 | 'seqNo': 10, 664 | courseId: 4 665 | }, 666 | 667 | // NestJs Course 668 | 669 | 60: { 670 | id: 60, 671 | 'description': 'Introduction to NestJs', 672 | 'duration': '4:29', 673 | 'seqNo': 1, 674 | courseId: 14 675 | }, 676 | 61: { 677 | id: 61, 678 | 'description': 'Development Environment Setup', 679 | 'duration': '6:37', 680 | 'seqNo': 2, 681 | courseId: 14 682 | }, 683 | 62: { 684 | id: 62, 685 | 'description': 'Setting up a MongoDB Database', 686 | 'duration': '6:38', 687 | 'seqNo': 3, 688 | courseId: 14 689 | }, 690 | 63: { 691 | id: 63, 692 | 'description': 'CRUD with NestJs - Controllers and Repositories', 693 | 'duration': '12:12', 694 | 'seqNo': 4, 695 | courseId: 14 696 | }, 697 | 64: { 698 | id: 64, 699 | 'description': 'First REST endpoint - Get All Courses', 700 | 'duration': '3:42', 701 | 'seqNo': 5, 702 | courseId: 14 703 | }, 704 | 65: { 705 | id: 65, 706 | 'description': 'Error Handling', 707 | 'duration': '5:15', 708 | 'seqNo': 6, 709 | courseId: 14 710 | }, 711 | 66: { 712 | id: 66, 713 | 'description': 'NestJs Middleware', 714 | 'duration': '7:08', 715 | 'seqNo': 7, 716 | courseId: 14 717 | }, 718 | 67: { 719 | id: 67, 720 | 'description': 'Authentication in NestJs', 721 | 'duration': '13:22', 722 | 'seqNo': 8, 723 | courseId: 14 724 | }, 725 | 68: { 726 | id: 68, 727 | 'description': 'Authorization in NestJs', 728 | 'duration': '6:43', 729 | 'seqNo': 9, 730 | courseId: 14 731 | }, 732 | 69: { 733 | id: 69, 734 | 'description': 'Guards & Interceptors', 735 | 'duration': '8:16', 736 | 'seqNo': 10, 737 | courseId: 14 738 | }, 739 | 740 | // Stripe Course 741 | 742 | 70: { 743 | id: 70, 744 | 'description': 'Introduction to Stripe Payments', 745 | 'duration': '03:45', 746 | 'seqNo': 0, 747 | courseId: 16 748 | }, 749 | 71: { 750 | id: 71, 751 | 'description': 'The advantages of Stripe Checkout', 752 | 'duration': '08:36', 753 | 'seqNo': 1, 754 | courseId: 16 755 | }, 756 | 72: { 757 | id: 72, 758 | 'description': 'Setting up the development environment', 759 | 'duration': '09:10', 760 | 'seqNo': 2, 761 | courseId: 16 762 | }, 763 | 73: { 764 | id: 73, 765 | 'description': 'Creating a server Checkout Session', 766 | 'duration': '07:20', 767 | 'seqNo': 3, 768 | courseId: 16 769 | }, 770 | 74: { 771 | id: 74, 772 | 'description': 'Redirecting to the Stripe Checkout page', 773 | 'duration': '11:47', 774 | 'seqNo': 4, 775 | courseId: 16 776 | }, 777 | 75: { 778 | id: 75, 779 | 'description': 'Order fulfillment webhook', 780 | 'duration': '06:30', 781 | 'seqNo': 5, 782 | courseId: 16 783 | }, 784 | 76: { 785 | id: 76, 786 | 'description': 'Installing the Stripe CLI', 787 | 'duration': '4:13', 788 | 'seqNo': 6, 789 | courseId: 16 790 | }, 791 | 77: { 792 | id: 77, 793 | 'description': 'Firestore Security Rules for protecting Premium content', 794 | 'duration': '05:47', 795 | 'seqNo': 7, 796 | courseId: 16 797 | }, 798 | 78: { 799 | id: 78, 800 | 'description': 'Stripe Subscriptions with Stripe Checkout', 801 | 'duration': '05:17', 802 | 'seqNo': 8, 803 | courseId: 16 804 | }, 805 | 79: { 806 | id: 79, 807 | 'description': 'Stripe Subscription Fulfillment', 808 | 'duration': '07:50', 809 | 'seqNo': 9, 810 | courseId: 16 811 | }, 812 | 813 | 814 | }; 815 | 816 | export function findCourseById(courseId: number) { 817 | return COURSES[courseId]; 818 | } 819 | 820 | export function findLessonsForCourse(courseId: number) { 821 | return Object.values(LESSONS).filter(lesson => lesson.courseId == courseId); 822 | } 823 | 824 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('stripe-course App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules" 4 | }, 5 | "hosting": { 6 | "public": "dist", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | 3 | match /databases/{database}/documents { 4 | 5 | function userOwnsCourse(userId, courseId) { 6 | return exists(/databases/$(database)/documents/users/$(userId)/coursesOwned/$(courseId)) 7 | } 8 | 9 | function isSubscriber(userId) { 10 | return "pricingPlanId" in get(/databases/$(database)/documents/users/$(userId)).data 11 | } 12 | 13 | function isUserWithId(userId) { 14 | return request.auth.uid == userId; 15 | } 16 | 17 | match /courses/{courseId} { 18 | allow read: if true; 19 | 20 | match /lessons/{lessonId} { 21 | allow read: if userOwnsCourse(request.auth.uid,courseId) || isSubscriber(request.auth.uid) 22 | } 23 | } 24 | 25 | match /purchaseSessions/{purchaseId} { 26 | allow read: if request.auth.uid == resource.data.userId; 27 | } 28 | 29 | match /users/{userId} { 30 | allow read: if isUserWithId(userId); 31 | 32 | match /coursesOwned/{courseId} { 33 | allow read: if isUserWithId(userId); 34 | } 35 | 36 | } 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /images/serverless-angular-play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/stripe-course/b47a050125a519e5a0bd10c7ddf254a2df958786/images/serverless-angular-play-button.png -------------------------------------------------------------------------------- /images/serverless-angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/stripe-course/b47a050125a519e5a0bd10c7ddf254a2df958786/images/serverless-angular.png -------------------------------------------------------------------------------- /init-db.ts: -------------------------------------------------------------------------------- 1 | 2 | import {COURSES, findLessonsForCourse} from './db-data'; 3 | 4 | import * as firebase from 'firebase'; 5 | 6 | var config = { 7 | // TODO copy-paste here your own config, taken from the Firebase dashboard 8 | apiKey: "AIzaSyBC9AIbgFfJQPKHBgGg7xULHNWjlnW-3vs", 9 | authDomain: "stripe-course-recording.firebaseapp.com", 10 | databaseURL: "https://stripe-course-recording.firebaseio.com", 11 | projectId: "stripe-course-recording", 12 | storageBucket: "stripe-course-recording.appspot.com", 13 | messagingSenderId: "909700347297", 14 | appId: "1:909700347297:web:0e9e7105baf123acdd87e0" 15 | }; 16 | 17 | console.log("Uploading data to the database with the following config:\n"); 18 | 19 | console.log(JSON.stringify(config)); 20 | 21 | console.log("\n\n\n\nMake sure that this is your own database, so that you have write access to it.\n\n\n"); 22 | 23 | firebase.initializeApp(config); 24 | 25 | const db = firebase.firestore(); 26 | 27 | async function uploadData() { 28 | 29 | var batch = db.batch(); 30 | 31 | const courses = db.collection('courses'); 32 | 33 | 34 | Object.values(COURSES) 35 | .sort((c1:any, c2:any) => c1.seqNo - c2.seqNo) 36 | .forEach(async (course:any) => { 37 | 38 | const newCourse = removeId(course); 39 | 40 | const courseRef = await courses.add(newCourse); 41 | 42 | const lessons = courseRef.collection("lessons"); 43 | 44 | const courseLessons = findLessonsForCourse(course.id); 45 | 46 | //console.log(`Adding ${courseLessons.length} lessons to ${course.description}`); 47 | 48 | courseLessons.forEach(async lesson => { 49 | 50 | const newLesson = removeId(lesson); 51 | 52 | await lessons.add(newLesson); 53 | 54 | }); 55 | 56 | }); 57 | 58 | return batch.commit(); 59 | } 60 | 61 | 62 | function removeId(data:any) { 63 | 64 | const newData: any = {...data}; 65 | 66 | delete newData.id; 67 | 68 | return newData; 69 | } 70 | 71 | 72 | uploadData() 73 | .then(() => { 74 | console.log("Writing data, exiting in 10 seconds ...\n\n"); 75 | 76 | setTimeout(() => { 77 | 78 | console.log("\n\n\nData Upload Completed.\n\n\n"); 79 | process.exit(0); 80 | 81 | }, 10000); 82 | 83 | }) 84 | .catch(err => { 85 | console.log("Data upload failed, reason:", err, '\n\n\n'); 86 | process.exit(-1); 87 | }); 88 | 89 | 90 | -------------------------------------------------------------------------------- /init-db.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-course", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --proxy-config ./proxy.json", 8 | "init-db": "ts-node -P ./init-db.tsconfig.json ./init-db.ts", 9 | "build:prod": "ng build --prod", 10 | "firebase-deploy:prod": "firebase deploy", 11 | "build-and-deploy:prod": "run-s build:prod firebase-deploy:prod", 12 | "build": "ng build", 13 | "test": "ng test", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "^10.0.2", 20 | "@angular/cdk": "^10.0.1", 21 | "@angular/common": "^10.0.2", 22 | "@angular/compiler": "^10.0.2", 23 | "@angular/core": "^10.0.2", 24 | "@angular/fire": "^5.2.1", 25 | "@angular/forms": "^10.0.2", 26 | "@angular/material": "^10.0.1", 27 | "@angular/material-moment-adapter": "^10.0.1", 28 | "@angular/platform-browser": "^10.0.2", 29 | "@angular/platform-browser-dynamic": "^10.0.2", 30 | "@angular/router": "^10.0.2", 31 | "core-js": "^2.5.7", 32 | "firebase": "^6.1.0", 33 | "firebaseui": "^4.0.0", 34 | "moment": "^2.24.0", 35 | "rxjs": "6.5.4", 36 | "tslib": "^2.0.0", 37 | "zone.js": "~0.10.3" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "~0.1000.0", 41 | "@angular/cli": "^10.0.0", 42 | "@angular/compiler-cli": "^10.0.2", 43 | "@angular/language-service": "10.0.2", 44 | "@types/jasmine": "~3.3.0", 45 | "@types/jasminewd2": "~2.0.6", 46 | "@types/node": "^12.11.1", 47 | "codelyzer": "^5.1.2", 48 | "jasmine-core": "~3.5.0", 49 | "jasmine-spec-reporter": "~5.0.0", 50 | "karma": "~5.0.0", 51 | "karma-chrome-launcher": "~3.1.0", 52 | "karma-cli": "~1.0.1", 53 | "karma-coverage-istanbul-reporter": "~3.0.2", 54 | "karma-jasmine": "~3.3.0", 55 | "karma-jasmine-html-reporter": "^1.5.0", 56 | "npm-run-all": "^4.1.5", 57 | "protractor": "~7.0.0", 58 | "ts-node": "~7.0.1", 59 | "tslint": "~6.1.0", 60 | "typescript": "3.9.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:9000", 4 | "secure": false 5 | } 6 | } -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | STRIPE_SECRET_KEY="sk_live_Sni4Hyrm9nVybngtmM7jyAv100e467KXbz" 2 | STRIPE_PUBLIC_KEY="pk_live_7NsDZC3rMEp7JISQCjQzbacS" 3 | SERVICE_ACCOUNT_FILE_NAME="stripe-course-recording-ebbda60a91a3.json" 4 | PROJECT_ID="stripe-course-recording" 5 | FIRESTORE_DATABASE_URL="https://stripe-course-recording.firebaseio.com" 6 | STRIPE_WEBHOOK_SECRET="whsec_ZTaBiFzLLteWishrjiMyXRUB3I1k8e5m" 7 | -------------------------------------------------------------------------------- /server/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /server/app.yaml: -------------------------------------------------------------------------------- 1 | 2 | runtime: nodejs10 3 | 4 | -------------------------------------------------------------------------------- /server/auth.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const admin = require('firebase-admin'); 4 | 5 | const serviceAccountPath = `./service-accounts/${process.env.SERVICE_ACCOUNT_FILE_NAME}`; 6 | 7 | admin.initializeApp({ 8 | credential: admin.credential.cert(serviceAccountPath), 9 | databaseURL:process.env.FIRESTORE_DATABASE_URL 10 | }); 11 | 12 | 13 | export const auth = admin.auth(); 14 | -------------------------------------------------------------------------------- /server/checkout.route.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from 'express'; 2 | import {db, getDocData} from './database'; 3 | import {Timestamp} from '@google-cloud/firestore'; 4 | 5 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 6 | 7 | interface RequestInfo { 8 | courseId: string; 9 | callbackUrl: string; 10 | userId:string; 11 | pricingPlanId:string; 12 | } 13 | 14 | export async function createCheckoutSession(req: Request, res: Response) { 15 | 16 | try { 17 | 18 | const info: RequestInfo = { 19 | courseId: req.body.courseId, 20 | pricingPlanId: req.body.pricingPlanId, 21 | callbackUrl: req.body.callbackUrl, 22 | userId: req["uid"] 23 | }; 24 | 25 | if (!info.userId) { 26 | const message = 'User must be authenticated.'; 27 | console.log(message); 28 | res.status(403).json({message}); 29 | return; 30 | } 31 | 32 | const purchaseSession = await db.collection('purchaseSessions').doc(); 33 | 34 | const checkoutSessionData: any = { 35 | status: 'ongoing', 36 | created: Timestamp.now(), 37 | userId: info.userId 38 | }; 39 | 40 | if (info.courseId) { 41 | checkoutSessionData.courseId = info.courseId; 42 | } 43 | else { 44 | checkoutSessionData.pricingPlanId = info.pricingPlanId; 45 | } 46 | 47 | await purchaseSession.set(checkoutSessionData); 48 | 49 | const user = await getDocData(`users/${info.userId}`); 50 | 51 | let sessionConfig, 52 | stripeCustomerId = user ? user.stripeCustomerId : undefined; 53 | 54 | if (info.courseId) { 55 | const course = await getDocData(`courses/${info.courseId}`); 56 | sessionConfig = setupPurchaseCourseSession(info, course, 57 | purchaseSession.id, stripeCustomerId); 58 | } 59 | else if (info.pricingPlanId) { 60 | sessionConfig = setupSubscriptionSession(info, purchaseSession.id, 61 | stripeCustomerId, info.pricingPlanId); 62 | } 63 | 64 | console.log(sessionConfig); 65 | 66 | const session = await stripe.checkout.sessions.create(sessionConfig); 67 | 68 | res.status(200).json({ 69 | stripeCheckoutSessionId: session.id, 70 | stripePublicKey: process.env.STRIPE_PUBLIC_KEY 71 | }); 72 | 73 | } catch (error) { 74 | console.log('Unexpected error occurred while purchasing course: ', error); 75 | res.status(500).json({error: 'Could not initiate Stripe checkout session'}); 76 | } 77 | 78 | } 79 | 80 | function setupSubscriptionSession(info: RequestInfo, sessionId: string,stripeCustomerId, 81 | pricingPlanId) { 82 | 83 | const config = setupBaseSessionConfig(info, sessionId, stripeCustomerId); 84 | 85 | config.subscription_data = { 86 | items: [{plan: pricingPlanId}] 87 | }; 88 | 89 | return config; 90 | } 91 | 92 | function setupPurchaseCourseSession(info: RequestInfo, course, sessionId: string, 93 | stripeCustomerId:string) { 94 | const config = setupBaseSessionConfig(info, sessionId, stripeCustomerId); 95 | config.line_items = [ 96 | { 97 | name: course.titles.description, 98 | description: course.titles.longDescription, 99 | amount: course.price * 100, 100 | currency: 'usd', 101 | quantity: 1 102 | } 103 | ]; 104 | return config; 105 | } 106 | 107 | 108 | function setupBaseSessionConfig(info: RequestInfo, sessionId: string, 109 | stripeCustomerId:string) { 110 | const config: any = { 111 | payment_method_types: ['card'], 112 | success_url: `${info.callbackUrl}/?purchaseResult=success&ongoingPurchaseSessionId=${sessionId}`, 113 | cancel_url: `${info.callbackUrl}/?purchaseResult=failed`, 114 | client_reference_id: sessionId 115 | }; 116 | 117 | if (stripeCustomerId) { 118 | config.customer = stripeCustomerId; 119 | } 120 | 121 | return config; 122 | } 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /server/database.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Firestore = require('@google-cloud/firestore'); 4 | 5 | const serviceAccountPath = `./service-accounts/${process.env.SERVICE_ACCOUNT_FILE_NAME}`; 6 | 7 | 8 | export const db = new Firestore({ 9 | projectId: process.env.PROJECT_ID, 10 | keyFilename: serviceAccountPath 11 | }); 12 | 13 | 14 | export async function getDocData(docPath) { 15 | const snap = await db.doc(docPath).get(); 16 | return snap.data(); 17 | } 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/get-user.middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Request, Response, NextFunction} from 'express'; 3 | import {auth} from './auth'; 4 | 5 | 6 | export function getUserMiddleware(req: Request, res: Response, next: NextFunction) { 7 | 8 | const jwt = req.headers.authorization; 9 | 10 | if (jwt) { 11 | auth.verifyIdToken(jwt) 12 | .then(jwtPayload => { 13 | req["uid"] = jwtPayload.uid; 14 | next(); 15 | }) 16 | .catch(error => { 17 | const message = 'Error verifying Firebase Id token'; 18 | console.log(message, error); 19 | res.status(403).json({message}); 20 | }); 21 | } 22 | else { 23 | next(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/main.ts: -------------------------------------------------------------------------------- 1 | 2 | const dotenv = require("dotenv"); 3 | 4 | const result = dotenv.config(); 5 | 6 | if (result.error) { 7 | throw result.error; 8 | } 9 | // uncomment to see the content of your environment variables 10 | // console.log("Loaded environment config: ", result.parsed); 11 | 12 | import {initServer} from './server'; 13 | 14 | initServer(); 15 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "Backend for the Stripe Payments in Practice Course", 5 | "scripts": { 6 | "clean": "rimraf dist", 7 | "copy": "copyfiles service-accounts/*.json dist", 8 | "start:local": "tsc-watch -P ./server.tsconfig.json --onSuccess \"node dist/main.js\"", 9 | "build": "tsc -P ./server.tsconfig.json", 10 | "start": "node dist/main.js", 11 | "start:debug": "node --inspect-brk dist/main.js", 12 | "debug": "run-s clean build copy start:debug", 13 | "server": "run-s clean copy start:local", 14 | "deploy:prod": "gcloud app deploy", 15 | "build-and-deploy:prod": "run-s clean build copy deploy:prod", 16 | "webhooks": "stripe listen --forward-to localhost:9000/stripe-webhooks" 17 | }, 18 | "dependencies": { 19 | "@google-cloud/firestore": "^2.6.0", 20 | "@types/express": "^4.17.2", 21 | "body-parser": "^1.19.0", 22 | "cors": "^2.8.5", 23 | "dotenv": "^8.2.0", 24 | "express": "^4.17.1", 25 | "firebase-admin": "^8.8.0", 26 | "stripe": "^7.13.0" 27 | }, 28 | "devDependencies": { 29 | "copyfiles": "^2.1.1", 30 | "npm-run-all": "^4.1.5", 31 | "rimraf": "^3.0.0", 32 | "ts-node": "^8.5.2", 33 | "tsc-watch": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as express from 'express'; 3 | import {Application} from "express"; 4 | import {createCheckoutSession} from './checkout.route'; 5 | import {getUserMiddleware} from './get-user.middleware'; 6 | import {stripeWebhooks} from './stripe-webhooks.route'; 7 | import * as cors from "cors"; 8 | 9 | export function initServer() { 10 | 11 | const bodyParser = require('body-parser'); 12 | 13 | const app:Application = express(); 14 | 15 | app.use(cors()); 16 | 17 | app.route("/").get((req, res) => { 18 | res.status(200).send("
{{course.titles.longDescription}}
15 |{{message}}
4 | 5 |