├── .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 | ![Stripe Payments In Practice](https://angular-university.s3-us-west-1.amazonaws.com/course-images/stripe-course.jpg) 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 | ![Modern Angular With Signals Course](https://d3vigmphadbn9b.cloudfront.net/course-images/large-images/angular-signals-course.jpg) 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 | ![Ngrx (with NgRx Data) - The Complete Guide](https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png) 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 | ![Angular Core Deep Dive](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png) 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 | ![RxJs In Practice Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png) 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 | ![NestJs In Practice Course](https://angular-university.s3-us-west-1.amazonaws.com/course-images/nestjs-v2.png) 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 | ![Angular Testing Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png) 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 | ![Serverless Angular with Firebase Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png) 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 | ![Angular Universal Course](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-universal-small.png) 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 | ![Angular PWA Course - Build the future of the Web Today](https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png) 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 | ![Angular Security Masterclass](https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png) 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 | ![Angular Advanced Library Laboratory Course: Build Your Own Library](https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png) 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 | ![RxJs and Reactive Patterns Angular Architecture Course](https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png) 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 | ![Complete Typescript Course](https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png) 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("

API is up and running!

"); 19 | }); 20 | 21 | app.route("/api/checkout").post( 22 | bodyParser.json(), getUserMiddleware, createCheckoutSession); 23 | 24 | app.route("/stripe-webhooks").post( 25 | bodyParser.raw({type:'application/json'}), stripeWebhooks); 26 | 27 | const PORT = process.env.PORT || 9000; 28 | 29 | app.listen(PORT, () => { 30 | console.log("HTTP REST API Server running at port " + PORT); 31 | }); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /server/server.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "removeComments": true, 5 | "skipLibCheck": true, 6 | "target": "es2017", 7 | "lib": ["es2017"], 8 | "sourceMap": true, 9 | "outDir": "./dist" 10 | }, 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /server/stripe-webhooks.route.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Request, Response} from 'express'; 3 | import {db, getDocData} from './database'; 4 | 5 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 6 | 7 | 8 | export async function stripeWebhooks(req: Request, res:Response) { 9 | 10 | try { 11 | 12 | const signature = req.headers["stripe-signature"]; 13 | 14 | const event = stripe.webhooks.constructEvent( 15 | req.body, signature, process.env.STRIPE_WEBHOOK_SECRET); 16 | 17 | if (event.type == "checkout.session.completed") { 18 | const session = event.data.object; 19 | await onCheckoutSessionCompleted(session); 20 | 21 | } 22 | 23 | res.json({received:true}); 24 | 25 | } 26 | catch(err) { 27 | console.log('Error processing webhook event, reason: ', err); 28 | return res.status(400).send(`Webhook Error: ${err.message}`); 29 | } 30 | } 31 | 32 | 33 | async function onCheckoutSessionCompleted(session) { 34 | 35 | const purchaseSessionId = session.client_reference_id; 36 | 37 | const {userId, courseId, pricingPlanId} = 38 | await getDocData(`purchaseSessions/${purchaseSessionId}`); 39 | 40 | if (courseId) { 41 | await fulfillCoursePurchase(userId, courseId, purchaseSessionId, session.customer); 42 | } 43 | else if (pricingPlanId) { 44 | await fulfillSubscriptionPurchase(purchaseSessionId, userId, 45 | session.customer, pricingPlanId); 46 | } 47 | } 48 | 49 | async function fulfillSubscriptionPurchase(purchaseSessionId:string, userId:string, 50 | stripeCustomerId:string, pricingPlanId:string) { 51 | 52 | const batch = db.batch(); 53 | 54 | const purchaseSessionRef = db.doc(`purchaseSessions/${purchaseSessionId}`); 55 | 56 | batch.update(purchaseSessionRef, {status: "completed"}); 57 | 58 | const userRef = db.doc(`users/${userId}`); 59 | 60 | batch.set(userRef, {pricingPlanId, stripeCustomerId}, {merge: true} ); 61 | 62 | return batch.commit(); 63 | } 64 | 65 | async function fulfillCoursePurchase(userId:string, courseId:string, 66 | purchaseSessionId:string, 67 | stripeCustomerId:string) { 68 | 69 | const batch = db.batch(); 70 | 71 | const purchaseSessionRef = db.doc(`purchaseSessions/${purchaseSessionId}`); 72 | 73 | batch.update(purchaseSessionRef, {status: "completed"}); 74 | 75 | const userCoursesOwnedRef = db.doc(`users/${userId}/coursesOwned/${courseId}`); 76 | 77 | batch.create(userCoursesOwnedRef, {}); 78 | 79 | const userRef = db.doc(`users/${userId}`); 80 | 81 | batch.set(userRef, {stripeCustomerId}, {merge: true}); 82 | 83 | return batch.commit(); 84 | 85 | } 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/app/about/about.component.css: -------------------------------------------------------------------------------- 1 | .course-image { 2 | max-width: 350px; 3 | border-radius: 4px; 4 | } 5 | 6 | 7 | .about { 8 | padding: 40px; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Serverless Angular with Firebase Course

5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ViewEncapsulation} from '@angular/core'; 2 | 3 | import * as firebase from 'firebase/app'; 4 | import 'firebase/firestore'; 5 | import {Course} from '../model/course'; 6 | import {AngularFirestore} from '@angular/fire/firestore'; 7 | import {of} from 'rxjs'; 8 | 9 | 10 | @Component({ 11 | selector: 'about', 12 | templateUrl: './about.component.html', 13 | styleUrls: ['./about.component.css'] 14 | }) 15 | export class AboutComponent implements OnInit { 16 | 17 | constructor(private db: AngularFirestore) { 18 | 19 | } 20 | 21 | ngOnInit() { 22 | 23 | 24 | } 25 | 26 | save() { 27 | 28 | const firebaseCourseRef = 29 | this.db.doc('/courses/JVXlcA6ph98c7Vg2nc4E').ref; 30 | 31 | const rxjsCourseRef = 32 | this.db.doc('/courses/MsU0Mz7pNSbnhzYSkt9y').ref; 33 | 34 | const batch = this.db.firestore.batch(); 35 | 36 | batch.update(firebaseCourseRef, {titles: {description:'Firebase Course'}}); 37 | 38 | batch.update(rxjsCourseRef, {titles: {description:'RxJs Course'}}); 39 | 40 | const batch$ = of(batch.commit()); 41 | 42 | batch$.subscribe(); 43 | 44 | } 45 | 46 | async runTransaction() { 47 | 48 | const newCounter = await this.db.firestore 49 | .runTransaction(async transaction => { 50 | 51 | console.log('Running transaction...'); 52 | 53 | const courseRef = this.db.doc('/courses/JVXlcA6ph98c7Vg2nc4E').ref; 54 | 55 | const snap = await transaction.get(courseRef); 56 | 57 | const course = snap.data(); 58 | 59 | const lessonsCount = course.lessonsCount + 1; 60 | 61 | transaction.update(courseRef, {lessonsCount}); 62 | 63 | return lessonsCount; 64 | 65 | }); 66 | 67 | console.log("result lessons count = ",newCounter); 68 | 69 | } 70 | 71 | } 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import {HomeComponent} from "./home/home.component"; 4 | import {AboutComponent} from "./about/about.component"; 5 | import {CourseComponent} from "./course/course.component"; 6 | import {CourseResolver} from "./services/course.resolver"; 7 | import {LoginComponent} from './login/login.component'; 8 | import {StripeCheckoutComponent} from './stripe-checkout/stripe-checkout.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: "", 13 | component: HomeComponent 14 | 15 | }, 16 | { 17 | path: "about", 18 | component: AboutComponent 19 | }, 20 | { 21 | path: "login", 22 | component: LoginComponent 23 | }, 24 | { 25 | path: 'courses/:courseUrl', 26 | component: CourseComponent, 27 | resolve: { 28 | course: CourseResolver 29 | } 30 | }, 31 | { 32 | path:"stripe-checkout", 33 | component: StripeCheckoutComponent 34 | }, 35 | { 36 | path: "**", 37 | redirectTo: '/' 38 | } 39 | ]; 40 | 41 | @NgModule({ 42 | imports: [RouterModule.forRoot(routes)], 43 | exports: [RouterModule] 44 | }) 45 | export class AppRoutingModule { } 46 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | 2 | >>> body { 3 | margin: 0; 4 | } 5 | 6 | main { 7 | margin: 30px; 8 | } 9 | 10 | .user-avatar { 11 | max-height: 35px; 12 | border-radius: 4px; 13 | 14 | } 15 | 16 | .toolbar-tools { 17 | display: flex; 18 | width: 100%; 19 | } 20 | 21 | 22 | .filler { 23 | flex: 1 1 auto; 24 | width: 100%; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | library_books 8 | Courses 9 | 10 | 11 | 12 | question_answer 13 | About 14 | 15 | 16 | person_add 17 | Register 18 | 19 | 20 | 21 | account_circle 22 | Login 23 | 24 | 25 | 26 | exit_to_app 27 | Logout 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 | 57 |
58 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AngularFireAuth} from '@angular/fire/auth'; 3 | import {Observable} from 'rxjs'; 4 | import {map, tap} from 'rxjs/operators'; 5 | 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.css'] 11 | }) 12 | export class AppComponent implements OnInit { 13 | 14 | isLoggedIn$: Observable; 15 | 16 | isLoggedOut$:Observable; 17 | 18 | pictureUrl$: Observable; 19 | 20 | constructor( 21 | private afAuth: AngularFireAuth) { 22 | 23 | afAuth.idToken.subscribe(jwt => console.log(jwt)); 24 | 25 | } 26 | 27 | ngOnInit() { 28 | 29 | this.isLoggedIn$ = this.afAuth.authState.pipe(map(user => !!user)); 30 | 31 | this.isLoggedOut$ = this.isLoggedIn$.pipe(map(loggedIn => !loggedIn)); 32 | 33 | this.pictureUrl$ = 34 | this.afAuth.authState.pipe(map(user => user ? user.photoURL: null)); 35 | } 36 | 37 | logout() { 38 | 39 | this.afAuth.auth.signOut(); 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppRoutingModule} from './app-routing.module'; 5 | import {AppComponent} from './app.component'; 6 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | import {MatMenuModule} from '@angular/material/menu'; 8 | import {MatButtonModule} from '@angular/material/button'; 9 | import {MatIconModule} from '@angular/material/icon'; 10 | import {MatCardModule} from '@angular/material/card'; 11 | import {HomeComponent} from './home/home.component'; 12 | import {AboutComponent} from './about/about.component'; 13 | import {MatTabsModule} from '@angular/material/tabs'; 14 | import {CoursesCardListComponent} from './courses-card-list/courses-card-list.component'; 15 | import {CourseComponent} from './course/course.component'; 16 | import {MatDatepickerModule} from '@angular/material/datepicker'; 17 | import {MatDialogModule} from '@angular/material/dialog'; 18 | import {MatInputModule} from '@angular/material/input'; 19 | import {MatListModule} from '@angular/material/list'; 20 | import {MatPaginatorModule} from '@angular/material/paginator'; 21 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 22 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 23 | import {MatSelectModule} from '@angular/material/select'; 24 | import {MatSidenavModule} from '@angular/material/sidenav'; 25 | import {MatSortModule} from '@angular/material/sort'; 26 | import {MatTableModule} from '@angular/material/table'; 27 | import {MatToolbarModule} from '@angular/material/toolbar'; 28 | import {CourseResolver} from './services/course.resolver'; 29 | import {CourseDialogComponent} from './course-dialog/course-dialog.component'; 30 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 31 | import {MatMomentDateModule} from '@angular/material-moment-adapter'; 32 | import {AngularFireModule} from '@angular/fire'; 33 | 34 | import {environment} from '../environments/environment'; 35 | import {AngularFireAuthModule} from '@angular/fire/auth'; 36 | import {AngularFirestoreModule} from '@angular/fire/firestore'; 37 | import {AngularFireStorageModule} from '@angular/fire/storage'; 38 | import {LoginComponent} from './login/login.component'; 39 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 40 | import { StripeCheckoutComponent } from './stripe-checkout/stripe-checkout.component'; 41 | 42 | 43 | @NgModule({ 44 | declarations: [ 45 | AppComponent, 46 | HomeComponent, 47 | AboutComponent, 48 | CourseComponent, 49 | CoursesCardListComponent, 50 | CourseDialogComponent, 51 | LoginComponent, 52 | StripeCheckoutComponent 53 | ], 54 | imports: [ 55 | BrowserModule, 56 | BrowserAnimationsModule, 57 | MatMenuModule, 58 | MatButtonModule, 59 | MatIconModule, 60 | MatCardModule, 61 | MatTabsModule, 62 | MatSidenavModule, 63 | MatListModule, 64 | MatToolbarModule, 65 | MatInputModule, 66 | MatTableModule, 67 | MatPaginatorModule, 68 | MatSortModule, 69 | MatProgressSpinnerModule, 70 | MatProgressBarModule, 71 | MatDialogModule, 72 | AppRoutingModule, 73 | MatSelectModule, 74 | MatDatepickerModule, 75 | MatMomentDateModule, 76 | ReactiveFormsModule, 77 | AngularFireModule.initializeApp(environment.firebase), 78 | AngularFireAuthModule, 79 | AngularFirestoreModule, 80 | HttpClientModule 81 | ], 82 | providers: [ 83 | CourseResolver, 84 | ], 85 | bootstrap: [AppComponent], 86 | entryComponents: [CourseDialogComponent] 87 | }) 88 | export class AppModule { 89 | } 90 | -------------------------------------------------------------------------------- /src/app/course-dialog/course-dialog.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .mat-form-field { 4 | display: block; 5 | } 6 | 7 | textarea { 8 | height: 100px; 9 | resize: vertical; 10 | } 11 | 12 | .course-image-upload { 13 | margin-top: 20px; 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | 19 | .course-image-upload span { 20 | margin-bottom:10px; 21 | } 22 | 23 | .upload-progress { 24 | display: flex; 25 | margin: 10px; 26 | } 27 | 28 | .progress-bar { 29 | margin: 10px; 30 | width: 300px; 31 | } 32 | 33 | .uploaded-image { 34 | max-width: 250px; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/course-dialog/course-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

{{description}}

3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 |
25 | 26 | Course: 27 | 28 | 29 | 30 |
31 | 32 | Upload Progress: 33 | 34 | 35 | 36 | 37 | 38 | {{percentage / 100 | percent}} 39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 | 48 | 49 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/app/course-dialog/course-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; 3 | import {Course} from "../model/course"; 4 | import {FormBuilder, Validators, FormGroup} from "@angular/forms"; 5 | import {CoursesService} from '../services/courses.service'; 6 | import {AngularFirestore} from '@angular/fire/firestore'; 7 | import {AngularFireStorage} from '@angular/fire/storage'; 8 | import {Observable} from 'rxjs'; 9 | import {concatMap, last, tap} from 'rxjs/operators'; 10 | 11 | 12 | @Component({ 13 | selector: 'course-dialog', 14 | templateUrl: './course-dialog.component.html', 15 | styleUrls: ['./course-dialog.component.css'] 16 | }) 17 | export class CourseDialogComponent implements OnInit { 18 | 19 | form: FormGroup; 20 | description:string; 21 | 22 | course: Course; 23 | 24 | uploadPercent$ : Observable; 25 | 26 | 27 | constructor( 28 | private fb: FormBuilder, 29 | private dialogRef: MatDialogRef, 30 | @Inject(MAT_DIALOG_DATA) course:Course, 31 | private coursesService: CoursesService, 32 | private storage: AngularFireStorage) { 33 | 34 | this.course = course; 35 | 36 | const titles = course.titles; 37 | 38 | this.form = fb.group({ 39 | description: [titles.description, Validators.required], 40 | longDescription: [titles.longDescription,Validators.required] 41 | }); 42 | 43 | } 44 | 45 | uploadFile(event) { 46 | 47 | const file: File = event.target.files[0]; 48 | 49 | const filePath = `courses/${this.course.id}/${file.name}`; 50 | 51 | const task = this.storage.upload(filePath, file); 52 | 53 | this.uploadPercent$ = task.percentageChanges(); 54 | 55 | } 56 | 57 | ngOnInit() { 58 | 59 | } 60 | 61 | 62 | save() { 63 | 64 | const changes = this.form.value; 65 | 66 | this.coursesService.saveCourse(this.course.id, {titles: changes}) 67 | .subscribe( 68 | () => this.dialogRef.close(this.form.value) 69 | ); 70 | } 71 | 72 | close() { 73 | this.dialogRef.close(); 74 | } 75 | 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/app/course/course.component.css: -------------------------------------------------------------------------------- 1 | 2 | .course { 3 | text-align: center; 4 | max-width: 390px; 5 | margin: 0 auto; 6 | } 7 | 8 | .course-thumbnail { 9 | width: 200px; 10 | border-radius: 4px; 11 | margin: 20px auto; 12 | display: block; 13 | } 14 | 15 | .description-cell { 16 | text-align: left; 17 | margin: 10px auto; 18 | } 19 | 20 | .duration-cell { 21 | text-align: center; 22 | } 23 | 24 | .duration-cell mat-icon { 25 | display: inline-block; 26 | vertical-align: middle; 27 | font-size: 20px; 28 | } 29 | 30 | .spinner-container { 31 | height: 360px; 32 | width: 390px; 33 | position: fixed; 34 | } 35 | 36 | .lessons-table { 37 | min-height: 360px; 38 | margin-top: 10px; 39 | } 40 | 41 | .spinner-container mat-spinner { 42 | margin: 130px auto 0 auto; 43 | } 44 | 45 | .bottom-toolbar { 46 | margin: 30px 0; 47 | font-size: 18px; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/course/course.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{course?.titles.description}}

4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | # 18 | 19 | {{lesson.seqNo}} 20 | 21 | 22 | 23 | 24 | 25 | Description 26 | 27 | {{lesson.description}} 29 | 30 | 31 | 32 | 33 | 34 | Duration 35 | 36 | {{lesson.duration}} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/app/course/course.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Course} from '../model/course'; 4 | import {finalize, tap} from 'rxjs/operators'; 5 | import {Observable} from 'rxjs'; 6 | import {Lesson} from '../model/lesson'; 7 | import {CoursesService} from '../services/courses.service'; 8 | 9 | 10 | @Component({ 11 | selector: 'course', 12 | templateUrl: './course.component.html', 13 | styleUrls: ['./course.component.css'] 14 | }) 15 | export class CourseComponent implements OnInit { 16 | 17 | course: Course; 18 | 19 | lessons: Lesson[]; 20 | 21 | lastPageLoaded = 0; 22 | 23 | loading = false; 24 | 25 | displayedColumns = ['seqNo', 'description', 'duration']; 26 | 27 | 28 | constructor(private route: ActivatedRoute, 29 | private coursesService: CoursesService) { 30 | 31 | } 32 | 33 | ngOnInit() { 34 | 35 | this.course = this.route.snapshot.data['course']; 36 | 37 | this.loading = true; 38 | 39 | this.coursesService.findLessons(this.course.id) 40 | .pipe( 41 | finalize(() => this.loading = false) 42 | ) 43 | .subscribe( 44 | lessons => this.lessons = lessons 45 | ); 46 | 47 | } 48 | 49 | loadMore() { 50 | 51 | this.lastPageLoaded++; 52 | 53 | this.loading = true; 54 | 55 | this.coursesService.findLessons(this.course.id, 'asc', 56 | this.lastPageLoaded) 57 | .pipe( 58 | finalize(() => this.loading = false) 59 | ) 60 | .subscribe(lessons => this.lessons = this.lessons.concat(lessons)); 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/app/courses-card-list/courses-card-list.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .course-card { 4 | margin: 20px 10px; 5 | } 6 | 7 | .course-actions { 8 | text-align: center; 9 | } -------------------------------------------------------------------------------- /src/app/courses-card-list/courses-card-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{course.titles.description}} 8 | 9 | 10 | 11 | 12 | 13 | 14 |

{{course.titles.longDescription}}

15 |
16 | 17 | 18 | 19 | 22 | 23 | 28 | 29 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/app/courses-card-list/courses-card-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; 2 | import {Course} from '../model/course'; 3 | import {MatDialog, MatDialogConfig} from '@angular/material/dialog'; 4 | import {CourseDialogComponent} from '../course-dialog/course-dialog.component'; 5 | import {AngularFireAuth} from '@angular/fire/auth'; 6 | import {map} from 'rxjs/operators'; 7 | import {Observable} from 'rxjs'; 8 | import {CheckoutService} from '../services/checkout.service'; 9 | 10 | @Component({ 11 | selector: 'courses-card-list', 12 | templateUrl: './courses-card-list.component.html', 13 | styleUrls: ['./courses-card-list.component.css'] 14 | }) 15 | export class CoursesCardListComponent implements OnInit { 16 | 17 | @Input() 18 | courses: Course[]; 19 | 20 | @Output() 21 | courseEdited = new EventEmitter(); 22 | 23 | isLoggedIn: boolean; 24 | 25 | purchaseStarted = false; 26 | 27 | constructor( 28 | private dialog: MatDialog, 29 | private afAuth: AngularFireAuth, 30 | private checkout: CheckoutService) { 31 | } 32 | 33 | ngOnInit() { 34 | 35 | this.afAuth.authState 36 | .pipe( 37 | map(user => !!user) 38 | ) 39 | .subscribe(isLoggedIn => this.isLoggedIn = isLoggedIn); 40 | 41 | } 42 | 43 | purchaseCourse(course: Course, isLoggedIn: boolean) { 44 | 45 | if (!isLoggedIn) { 46 | alert("Please login first."); 47 | } 48 | 49 | this.purchaseStarted = true; 50 | 51 | 52 | this.checkout.startCourseCheckoutSession(course.id) 53 | .subscribe( 54 | session => { 55 | this.checkout.redirectToCheckout(session); 56 | }, 57 | err => { 58 | console.log('Error creating checkout session', err); 59 | this.purchaseStarted = false; 60 | } 61 | ); 62 | 63 | 64 | } 65 | 66 | } 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .courses-panel { 4 | max-width: 400px; 5 | margin: 0 auto; 6 | } 7 | 8 | 9 | .page-title { 10 | font-family: "Roboto"; 11 | } 12 | 13 | 14 | .top-toolbar { 15 | margin-top: 20px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | 22 | .toolbar-btn { 23 | margin-left: 15px; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |

All Courses

7 | 8 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Course} from '../model/course'; 3 | import {Observable} from 'rxjs'; 4 | import {map} from 'rxjs/operators'; 5 | import {CoursesService} from '../services/courses.service'; 6 | import {CheckoutService} from '../services/checkout.service'; 7 | 8 | 9 | @Component({ 10 | selector: 'home', 11 | templateUrl: './home.component.html', 12 | styleUrls: ['./home.component.css'] 13 | }) 14 | export class HomeComponent implements OnInit { 15 | 16 | courses$: Observable; 17 | 18 | beginnersCourses$: Observable; 19 | 20 | advancedCourses$: Observable; 21 | 22 | processingOngoing = false; 23 | 24 | constructor( 25 | private coursesService: CoursesService, 26 | private checkout: CheckoutService) { 27 | 28 | } 29 | 30 | ngOnInit() { 31 | 32 | this.reloadCourses(); 33 | 34 | } 35 | 36 | reloadCourses() { 37 | 38 | this.courses$ = this.coursesService.loadAllCourses(); 39 | 40 | this.beginnersCourses$ = this.courses$.pipe( 41 | map(courses => courses.filter( 42 | course => course.categories.includes("BEGINNER")))); 43 | 44 | this.advancedCourses$ = this.courses$.pipe( 45 | map(courses => courses.filter( 46 | course => course.categories.includes("ADVANCED")))); 47 | } 48 | 49 | 50 | subscribeToPlan() { 51 | this.checkout.startSubscriptionCheckoutSession("STRIPE_MONTHLY") 52 | .subscribe( 53 | session => this.checkout.redirectToCheckout(session), 54 | err => { 55 | console.log("Error creating checkout session", err); 56 | this.processingOngoing = false; 57 | } 58 | ); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .auth-container { 3 | margin-top: 40px; 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, NgZone, OnDestroy, OnInit} from '@angular/core'; 2 | import * as firebaseui from 'firebaseui'; 3 | import * as firebase from 'firebase/app'; 4 | import {AngularFireAuth} from '@angular/fire/auth'; 5 | import {Router} from '@angular/router'; 6 | 7 | 8 | @Component({ 9 | selector: 'login', 10 | templateUrl: './login.component.html', 11 | styleUrls: ['./login.component.scss'] 12 | }) 13 | export class LoginComponent implements OnInit, OnDestroy { 14 | 15 | ui: firebaseui.auth.AuthUI; 16 | 17 | constructor(private afAuth: AngularFireAuth, 18 | private router:Router, 19 | private ngZone: NgZone) { 20 | 21 | 22 | } 23 | 24 | ngOnInit() { 25 | 26 | const uiConfig = { 27 | signInOptions: [ 28 | firebase.auth.GoogleAuthProvider.PROVIDER_ID, 29 | firebase.auth.EmailAuthProvider.PROVIDER_ID 30 | ], 31 | callbacks: { 32 | 33 | signInSuccessWithAuthResult: this 34 | .onLoginSuccessful 35 | .bind(this) 36 | } 37 | 38 | }; 39 | 40 | this.ui = new firebaseui.auth.AuthUI(this.afAuth.auth); 41 | 42 | this.ui.start('#firebaseui-auth-container', uiConfig); 43 | 44 | 45 | } 46 | 47 | ngOnDestroy() { 48 | this.ui.delete(); 49 | } 50 | 51 | onLoginSuccessful(result) { 52 | 53 | console.log("Firebase UI result:", result); 54 | 55 | this.ngZone.run(() => this.router.navigateByUrl('/courses')); 56 | 57 | } 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/app/model/checkout-session.model.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export interface CheckoutSession { 5 | stripeCheckoutSessionId:string, 6 | stripePublicKey: string; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/app/model/course.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Course { 4 | id:string; 5 | titles: { 6 | description:string; 7 | longDescription: string; 8 | }; 9 | iconUrl: string; 10 | price:number; 11 | uploadedImageUrl:string; 12 | courseListIcon: string; 13 | categories:string[]; 14 | lessonsCount:number; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/model/lesson.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface Lesson { 4 | id: number; 5 | description: string; 6 | duration: string; 7 | seqNo: number; 8 | courseId: number; 9 | } -------------------------------------------------------------------------------- /src/app/services/checkout.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpHeaders} from '@angular/common/http'; 3 | import {Observable} from 'rxjs'; 4 | import {CheckoutSession} from '../model/checkout-session.model'; 5 | import {AngularFireAuth} from '@angular/fire/auth'; 6 | import {AngularFirestore} from '@angular/fire/firestore'; 7 | import {filter, first} from 'rxjs/operators'; 8 | import {environment} from '../../environments/environment'; 9 | 10 | declare const Stripe; 11 | 12 | 13 | @Injectable({ 14 | providedIn: "root" 15 | }) 16 | export class CheckoutService { 17 | 18 | private jwtAuth:string; 19 | 20 | constructor(private http:HttpClient, 21 | private afAuth: AngularFireAuth, 22 | private afs: AngularFirestore) { 23 | 24 | afAuth.idToken.subscribe(jwt => this.jwtAuth = jwt); 25 | 26 | } 27 | 28 | startCourseCheckoutSession(courseId:string): Observable { 29 | 30 | const headers = new HttpHeaders().set("Authorization", this.jwtAuth); 31 | 32 | return this.http.post(environment.api.baseUrl + "/api/checkout", { 33 | courseId, 34 | callbackUrl: this.buildCallbackUrl() 35 | }, {headers}) 36 | } 37 | 38 | startSubscriptionCheckoutSession(pricingPlanId:string): Observable { 39 | 40 | const headers = new HttpHeaders().set("Authorization", this.jwtAuth); 41 | 42 | return this.http.post(environment.api.baseUrl + "/api/checkout", { 43 | pricingPlanId, 44 | callbackUrl: this.buildCallbackUrl() 45 | }, {headers}) 46 | } 47 | 48 | buildCallbackUrl() { 49 | 50 | const protocol = window.location.protocol, 51 | hostName = window.location.hostname, 52 | port = window.location.port; 53 | 54 | let callBackUrl = `${protocol}//${hostName}`; 55 | 56 | if (port) { 57 | callBackUrl += ":" + port; 58 | } 59 | 60 | callBackUrl+= "/stripe-checkout"; 61 | 62 | return callBackUrl; 63 | } 64 | 65 | redirectToCheckout(session: CheckoutSession) { 66 | 67 | const stripe = Stripe(session.stripePublicKey); 68 | 69 | stripe.redirectToCheckout({ 70 | sessionId: session.stripeCheckoutSessionId 71 | }); 72 | } 73 | 74 | waitForPurchaseCompleted(ongoingPurchaseSessionId: string):Observable { 75 | return this.afs.doc(`purchaseSessions/${ongoingPurchaseSessionId}`) 76 | .valueChanges() 77 | .pipe( 78 | filter(purchase => purchase.status == "completed"), 79 | first() 80 | ) 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/app/services/course.resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import {Injectable} from "@angular/core"; 5 | import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router"; 6 | import {Course} from "../model/course"; 7 | import {Observable, of} from 'rxjs'; 8 | import {CoursesService} from './courses.service'; 9 | import {first} from 'rxjs/operators'; 10 | 11 | 12 | 13 | @Injectable() 14 | export class CourseResolver implements Resolve { 15 | 16 | constructor(private coursesService:CoursesService) { 17 | 18 | } 19 | 20 | resolve(route: ActivatedRouteSnapshot, 21 | state: RouterStateSnapshot): Observable { 22 | 23 | const courseUrl = route.paramMap.get('courseUrl'); 24 | 25 | 26 | return this.coursesService.findCourseByUrl(courseUrl); 27 | 28 | } 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/app/services/courses.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CoursesService } from './courses.service'; 4 | 5 | describe('CoursesService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: CoursesService = TestBed.get(CoursesService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/services/courses.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {AngularFirestore} from '@angular/fire/firestore'; 3 | import {Course} from '../model/course'; 4 | import {from, Observable, of} from 'rxjs'; 5 | import {first, map} from 'rxjs/operators'; 6 | import {convertSnaps} from './db-utils'; 7 | import {Lesson} from '../model/lesson'; 8 | import OrderByDirection = firebase.firestore.OrderByDirection; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class CoursesService { 14 | 15 | constructor(private db: AngularFirestore) { 16 | 17 | 18 | } 19 | 20 | saveCourse(courseId:string, changes: Partial): Observable { 21 | return from(this.db.doc(`courses/${courseId}`).update(changes)); 22 | } 23 | 24 | loadAllCourses(): Observable { 25 | return this.db.collection( 26 | 'courses', 27 | ref=> ref.orderBy("seqNo") 28 | ) 29 | .snapshotChanges() 30 | .pipe( 31 | map(snaps => convertSnaps(snaps)), 32 | first()); 33 | } 34 | 35 | 36 | findCourseByUrl(courseUrl: string):Observable { 37 | return this.db.collection('courses', 38 | ref=> ref.where("url", "==", courseUrl)) 39 | .snapshotChanges() 40 | .pipe( 41 | map(snaps => { 42 | 43 | const courses = convertSnaps(snaps); 44 | 45 | return courses.length == 1 ? courses[0]: undefined; 46 | }), 47 | first() 48 | ) 49 | } 50 | 51 | findLessons(courseId:string, sortOrder: OrderByDirection = 'asc', 52 | pageNumber = 0, pageSize = 3):Observable { 53 | 54 | return this.db.collection(`courses/${courseId}/lessons`, 55 | ref => ref.orderBy('seqNo', sortOrder) 56 | .limit(pageSize) 57 | .startAfter(pageNumber * pageSize)) 58 | .snapshotChanges() 59 | .pipe( 60 | map(snaps => convertSnaps(snaps)), 61 | first() 62 | ) 63 | 64 | } 65 | 66 | 67 | 68 | 69 | } 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/app/services/db-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export function convertSnaps(snaps) { 5 | return snaps.map(snap => { 6 | return { 7 | id: snap.payload.doc.id, 8 | ...snap.payload.doc.data() 9 | }; 10 | 11 | }); 12 | } -------------------------------------------------------------------------------- /src/app/stripe-checkout/stripe-checkout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{message}}

4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/stripe-checkout/stripe-checkout.component.scss: -------------------------------------------------------------------------------- 1 | 2 | :host { 3 | margin-top: 100px; 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | p { 11 | font-family: "Roboto"; 12 | margin-bottom: 40px; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/stripe-checkout/stripe-checkout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ActivatedRoute, Router} from '@angular/router'; 3 | import {CheckoutService} from '../services/checkout.service'; 4 | 5 | @Component({ 6 | selector: 'stripe-checkout', 7 | templateUrl: './stripe-checkout.component.html', 8 | styleUrls: ['./stripe-checkout.component.scss'] 9 | }) 10 | export class StripeCheckoutComponent implements OnInit { 11 | 12 | message = "Waiting for purchase to complete..."; 13 | 14 | waiting = true; 15 | 16 | constructor( 17 | private route: ActivatedRoute, 18 | private router: Router, 19 | private checkout: CheckoutService) { 20 | 21 | } 22 | 23 | ngOnInit() { 24 | 25 | const result = this.route.snapshot.queryParamMap.get("purchaseResult"); 26 | 27 | if (result == "success") { 28 | 29 | const ongoingPurchaseSessionId = this.route.snapshot.queryParamMap.get("ongoingPurchaseSessionId"); 30 | 31 | this.checkout.waitForPurchaseCompleted(ongoingPurchaseSessionId) 32 | .subscribe( 33 | () => { 34 | this.waiting = false; 35 | this.message = "Purchase SUCCESSFUL, redirecting..."; 36 | setTimeout(() => this.router.navigateByUrl("/courses"), 3000); 37 | }) 38 | 39 | } 40 | else { 41 | this.waiting = false; 42 | this.message = "Purchase CANCELED or FAILED, redirecting..."; 43 | setTimeout(() => this.router.navigateByUrl("/courses"), 3000); 44 | } 45 | 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/stripe-course/b47a050125a519e5a0bd10c7ddf254a2df958786/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: "AIzaSyBC9AIbgFfJQPKHBgGg7xULHNWjlnW-3vs", 5 | authDomain: "stripe-course-recording.firebaseapp.com", 6 | databaseURL: "https://stripe-course-recording.firebaseio.com", 7 | projectId: "stripe-course-recording", 8 | storageBucket: "stripe-course-recording.appspot.com", 9 | messagingSenderId: "909700347297", 10 | appId: "1:909700347297:web:0e9e7105baf123acdd87e0" 11 | }, 12 | api: { 13 | baseUrl: "https://stripe-course-recording.appspot.com" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | 2 | export const environment = { 3 | production: false, 4 | firebase: { 5 | // TODO copy-paste here your own config, taken from the Firebase dashboard 6 | apiKey: "AIzaSyBC9AIbgFfJQPKHBgGg7xULHNWjlnW-3vs", 7 | authDomain: "stripe-course-recording.firebaseapp.com", 8 | databaseURL: "https://stripe-course-recording.firebaseio.com", 9 | projectId: "stripe-course-recording", 10 | storageBucket: "stripe-course-recording.appspot.com", 11 | messagingSenderId: "909700347297", 12 | appId: "1:909700347297:web:0e9e7105baf123acdd87e0" 13 | }, 14 | api: { 15 | baseUrl: "" 16 | } 17 | }; 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-university/stripe-course/b47a050125a519e5a0bd10c7ddf254a2df958786/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Stripe Payments In Practice 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | 4 | @import "~firebaseui/dist/firebaseui.css"; 5 | 6 | 7 | @import "~@angular/material/theming"; 8 | 9 | // Include non-theme styles for core. 10 | @include mat-core(); 11 | 12 | 13 | $mat-custom-blue: ( 14 | 50: #e3f2fd, 15 | 100: #bbdefb, 16 | 200: #90caf9, 17 | 300: #64b5f6, 18 | 400: #42a5f5, 19 | 500: #20a2ff, 20 | 600: #1e88e5, 21 | 700: #1976d2, 22 | 800: #1565c0, 23 | 900: #0d47a1, 24 | A100: #82b1ff, 25 | A200: #448aff, 26 | A400: #2979ff, 27 | A700: #2962ff, 28 | contrast: ( 29 | 50: $dark-primary-text, 30 | 100: $dark-primary-text, 31 | 200: $dark-primary-text, 32 | 300: $dark-primary-text, 33 | 400: $dark-primary-text, 34 | 500: $light-primary-text, 35 | 600: $light-primary-text, 36 | 700: $light-primary-text, 37 | 800: $light-primary-text, 38 | 900: $light-primary-text, 39 | A100: $dark-primary-text, 40 | A200: $light-primary-text, 41 | A400: $light-primary-text, 42 | A700: $light-primary-text, 43 | ) 44 | ); 45 | 46 | // Define a theme. 47 | $primary: mat-palette($mat-custom-blue); 48 | $accent: mat-palette($mat-pink, A200, A100, A400); 49 | 50 | $theme: mat-light-theme($primary, $accent); 51 | 52 | // Include all theme styles for the components. 53 | @include angular-material-theme($theme); 54 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "module": "esnext", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es2015", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom", 19 | "ES2017.object" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./src/tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./src/tsconfig.spec.json" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warning" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-var-keyword": true, 76 | "object-literal-sort-keys": false, 77 | "one-line": [ 78 | true, 79 | "check-open-brace", 80 | "check-catch", 81 | "check-else", 82 | "check-whitespace" 83 | ], 84 | "prefer-const": true, 85 | "quotemark": [ 86 | true, 87 | "single" 88 | ], 89 | "radix": true, 90 | "semicolon": [ 91 | true, 92 | "always" 93 | ], 94 | "triple-equals": [ 95 | true, 96 | "allow-null-check" 97 | ], 98 | "typedef-whitespace": [ 99 | true, 100 | { 101 | "call-signature": "nospace", 102 | "index-signature": "nospace", 103 | "parameter": "nospace", 104 | "property-declaration": "nospace", 105 | "variable-declaration": "nospace" 106 | } 107 | ], 108 | "typeof-compare": true, 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | "app", 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | "app", 129 | "kebab-case" 130 | ], 131 | "no-inputs-metadata-property": true, 132 | "no-outputs-metadata-property": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-output-rename": true, 136 | "use-lifecycle-interface": true, 137 | "use-pipe-transform-interface": true, 138 | "component-class-suffix": true, 139 | "directive-class-suffix": true, 140 | "invoke-injectable": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------