├── .gitignore ├── src ├── client │ ├── css │ │ └── main.css │ └── js │ │ └── main.js └── server │ ├── views │ ├── index.html │ ├── error.html │ ├── layout.html │ └── product.html │ ├── routes │ └── index.js │ ├── app.js │ └── bin │ └── www ├── TODO.md ├── README.md ├── package.json └── .jshintrc /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | .env 5 | dist/ 6 | -------------------------------------------------------------------------------- /src/client/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /src/server/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{% endblock %} 4 | 5 | 6 | {% block content %} 7 | 8 |
9 | 10 |

{{ title }}

11 |

Welcome to {{ title }}

12 | 13 |
14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | ## Server 4 | 5 | 1. Create a GET route `/products/:name` along with a view 6 | 1. Create a POST route `/charge` 7 | 8 | ## Client 9 | 10 | 1. *main.js* - handle form submission, create stripe token, throw any errors that we get back stripe 11 | 12 | 13 | LOTS MORE! 14 | -------------------------------------------------------------------------------- /src/server/views/error.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{% endblock %} 4 | 5 | 6 | {% block content %} 7 | 8 |
9 | 10 |

{{ message }}

11 |

{{ error.status }}

12 |
{{ error.stack }}
13 | 14 |
15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /src/server/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 | {% block content %} 11 | {% endblock %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Stripe Example 2 | 3 | Node + Express + Stripe 4 | 5 | > Looking for a more complex example? [Node Stripe Charge](https://github.com/mjhea0/node-stripe-charge) 6 | 7 | ## Quick Start 8 | 9 | 1. Fork/Clone 10 | 11 | 1. Install dependencies: 12 | 13 | ```sh 14 | $ npm install 15 | ``` 16 | 17 | 1. create a *.env* and add the following env variable: 18 | 19 | ``` 20 | STRIPE_SECRET_KEY=ADD-YOUR-OWN-KEY 21 | ``` 22 | 23 | 1. update *src/client/js/main.js* with your publishable key: 24 | 25 | ```javascript 26 | Stripe.setPublishableKey('UPDATE ME'); 27 | ``` 28 | 29 | 1. Fire up the app - `npm start`. Then, navigate to [http://localhost:3000/products/1](http://localhost:3000/products/1) in your browser of choice. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "_example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./src/server/bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.2.0", 12 | "dotenv": "^2.0.0", 13 | "express": "~4.13.1", 14 | "morgan": "~1.6.1", 15 | "serve-favicon": "~2.3.0", 16 | "stripe": "^4.4.0", 17 | "swig": "^1.4.2" 18 | }, 19 | "devDependencies": { 20 | "browser-sync": "2.9.6", 21 | "gulp": "^3.9.0", 22 | "gulp-clean": "^0.3.1", 23 | "gulp-concat": "^2.6.0", 24 | "gulp-connect": "^2.2.0", 25 | "gulp-jshint": "^1.11.2", 26 | "gulp-minify-css": "^1.2.1", 27 | "gulp-nodemon": "^2.0.4", 28 | "gulp-uglify": "^1.4.2", 29 | "jshint-stylish": "^2.0.1", 30 | "run-sequence": "^1.1.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "esnext": true, 4 | "jasmine": false, 5 | "spyOn": false, 6 | "it": false, 7 | "console": false, 8 | "describe": false, 9 | "Stripe": false, 10 | "expect": false, 11 | "beforeEach": false, 12 | "afterEach": false, 13 | "waits": false, 14 | "waitsFor": false, 15 | "runs": false, 16 | "$": false, 17 | "confirm": false 18 | }, 19 | "esnext": true, 20 | "node" : true, 21 | "browser" : true, 22 | "boss" : false, 23 | "curly": false, 24 | "debug": false, 25 | "devel": false, 26 | "eqeqeq": true, 27 | "evil": true, 28 | "forin": false, 29 | "immed": true, 30 | "laxbreak": false, 31 | "newcap": true, 32 | "noarg": true, 33 | "noempty": false, 34 | "nonew": false, 35 | "nomen": false, 36 | "onevar": true, 37 | "plusplus": false, 38 | "regexp": false, 39 | "undef": true, 40 | "sub": true, 41 | "strict": false, 42 | "white": true, 43 | "unused": false 44 | } 45 | -------------------------------------------------------------------------------- /src/client/js/main.js: -------------------------------------------------------------------------------- 1 | Stripe.setPublishableKey("UPDATE ME"); 2 | 3 | $(document).on("ready", () => { $("form-errors").hide(); }); 4 | 5 | $("#product-form").on("submit", event => { 6 | event.preventDefault(); 7 | 8 | $("form-errors").hide(); 9 | 10 | Stripe.card.createToken( 11 | { 12 | number: $("#card-number").val(), 13 | cvc: $("#cvv").val(), 14 | exp_month: $("#expiry-month").val(), 15 | exp_year: $("#expiry-year").val() 16 | }, 17 | stripeResponseHandler 18 | ); 19 | 20 | $("#submit-btn").prop("disabled", true); 21 | 22 | }); 23 | 24 | const stripeResponseHandler = (status, response) => { 25 | const $form = $("#product-form"); 26 | if (response.error) { 27 | // Show the errors on the form 28 | $("#form-errors").show(); 29 | $("#form-errors").html(response.error.message); 30 | $("#submit-btn").prop("disabled", false); 31 | } else { 32 | // response contains id and card, which contains additional card details 33 | const token = response.id; 34 | // Insert the token into the form so it gets submitted to the server 35 | $form.append($('').val(token)); 36 | // and submit 37 | console.log($form.get(0)); 38 | $form.get(0).submit(); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 4 | 5 | const products = [ 6 | { 7 | uuid: 1, 8 | productName: 'Soylent', 9 | productDescription: 'Meal replacement that tastes like pancake batter.', 10 | productPrice: 10.99 11 | } 12 | ]; 13 | 14 | router.get('/', (req, res, next) => { 15 | res.render('index', { title: 'Express' }); 16 | }); 17 | 18 | router.get('/products/:uuid', (req, res, next) => { 19 | const productID = req.params.uuid; 20 | const product = products.filter((product) => { 21 | return parseInt(productID) === product.uuid; 22 | }); 23 | if (product[0]) { 24 | return res.render('product', {productInfo: product[0]}); 25 | } 26 | return res.send('Product does not exist.'); 27 | }); 28 | 29 | router.post('/charge', (req, res,next) => { 30 | const stripeToken = req.body.stripeToken; 31 | const price = req.body.price; 32 | const amount = req.body.price * 100; 33 | const productName = req.body.name; 34 | // ensure amount === actual product amount to avoid fraud 35 | const product = products.filter((product) => { 36 | return productName === product.productName && parseFloat(price) === parseFloat(product.productPrice); 37 | }); 38 | 39 | if (product[0]) { 40 | stripe.charges.create({ 41 | card: stripeToken, 42 | currency: 'usd', 43 | amount: amount 44 | }, (err, charge) => { 45 | if (err) { 46 | console.log('here'); 47 | // console.log(err); 48 | res.send('error'); 49 | } else { 50 | res.send('success'); 51 | } 52 | 53 | }); 54 | } else { 55 | console.log('Product name or price mismatch'); 56 | res.send('error'); 57 | } 58 | }); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | // *** load env variables *** // 2 | require('dotenv').config(); 3 | 4 | // *** main dependencies *** // 5 | const express = require('express'); 6 | const path = require('path'); 7 | const favicon = require('serve-favicon'); 8 | const logger = require('morgan'); 9 | const cookieParser = require('cookie-parser'); 10 | const bodyParser = require('body-parser'); 11 | const { Swig } = require('swig'); 12 | 13 | // *** routes *** // 14 | const routes = require('./routes/index.js'); 15 | 16 | // *** express instance *** // 17 | const app = express(); 18 | 19 | // *** view engine *** // 20 | const swig = new Swig(); 21 | app.engine('html', swig.renderFile); 22 | app.set('view engine', 'html'); 23 | 24 | // *** static directory *** // 25 | app.set('views', path.join(__dirname, 'views')); 26 | 27 | // *** config middleware *** // 28 | app.use(logger('dev')); 29 | app.use(bodyParser.json()); 30 | app.use(bodyParser.urlencoded({ extended: false })); 31 | app.use(cookieParser()); 32 | app.use(express.static(path.join(__dirname, '../client'))); 33 | 34 | // *** main routes *** // 35 | app.use('/', routes); 36 | 37 | // catch 404 and forward to error handler 38 | app.use((req, res, next) => { 39 | const err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | }); 43 | 44 | // *** error handlers *** // 45 | 46 | // development error handler 47 | // will print stacktrace 48 | if (app.get('env') === 'development') { 49 | app.use((err, req, res, next) => { 50 | res.status(err.status || 500); 51 | res.render('error', { 52 | message: err.message, 53 | error: err 54 | }); 55 | }); 56 | } 57 | 58 | // production error handler 59 | // no stacktraces leaked to user 60 | app.use((err, req, res, next) => { 61 | res.status(err.status || 500); 62 | res.render('error', { 63 | message: err.message, 64 | error: {} 65 | }); 66 | }); 67 | 68 | module.exports = app; 69 | -------------------------------------------------------------------------------- /src/server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require("../app"); 8 | const debug = require("debug")("_example:server"); 9 | const http = require("http"); 10 | 11 | /** 12 | * Normalize a port into a number, string, or false. 13 | */ 14 | 15 | const normalizePort = (val) => { 16 | const port = parseInt(val, 10); 17 | 18 | if (isNaN(port)) { 19 | // named pipe 20 | return val; 21 | } 22 | 23 | if (port >= 0) { 24 | // port number 25 | return port; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * Event listener for HTTP server "error" event. 33 | */ 34 | 35 | const onError = error => { 36 | if (error.syscall !== "listen") { 37 | throw error; 38 | } 39 | 40 | const bind = typeof port === "string" ? `Pipe ${port}` : `Port ${port}`; 41 | 42 | // handle specific listen errors with friendly messages 43 | switch (error.code) { 44 | case "EACCES": 45 | console.error(`${bind} requires elevated privileges`); 46 | process.exit(1); 47 | break; 48 | case "EADDRINUSE": 49 | console.error(`${bind} is already in use`); 50 | process.exit(1); 51 | break; 52 | default: 53 | throw error; 54 | } 55 | }; 56 | 57 | /** 58 | * Event listener for HTTP server "listening" event. 59 | */ 60 | 61 | const onListening = () => { 62 | const addr = server.address(); 63 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 64 | debug(`Listening on ${bind}`); 65 | console.log(`Listening on ${bind}`); 66 | }; 67 | 68 | /** 69 | * Get port from environment and store in Express. 70 | */ 71 | 72 | const port = normalizePort(process.env.PORT || "3000"); 73 | app.set("port", port); 74 | 75 | /** 76 | * Create HTTP server. 77 | */ 78 | 79 | const server = http.createServer(app); 80 | 81 | /** 82 | * Listen on provided port, on all network interfaces. 83 | */ 84 | 85 | server.listen(port); 86 | server.on("error", onError); 87 | server.on("listening", onListening); 88 | -------------------------------------------------------------------------------- /src/server/views/product.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{% endblock %} 4 | 5 | 6 | {% block content %} 7 | 8 |
9 | 10 |

{{ productInfo.productName }}

11 |

{{ productInfo.productDescription }}

12 |

{{ productInfo.productPrice }}

13 | 14 |
15 | 16 |

17 | 18 |
19 |
20 | Payment 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | 53 |
54 |
55 | 65 |
66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 | 87 | {% endblock %} 88 | --------------------------------------------------------------------------------