diff --git a/.gitignore b/.gitignore
index e950c8e4..af9eba2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,6 @@ typings/
# dotenv environment variables file
.env
+
+# serverless
+serverless/
\ No newline at end of file
diff --git a/README.md b/README.md
index ee4bd1db..56b804eb 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
# Stripe Payments Demo
-This demo features a sample e-commerce store that uses [Stripe Elements](https://stripe.com/docs/elements) and the [Sources API](https://stripe.com/docs/sources) to illustrate how to accept both card payments and additional payment methods on the web.
+This demo features a sample e-commerce store that uses [Stripe Elements](https://stripe.com/docs/elements), [PaymentIntents](https://stripe.com/docs/payments/payment-intents) for [Dynamic 3D Secure](https://stripe.com/docs/payments/dynamic-3ds), and the [Sources API](https://stripe.com/docs/sources) to illustrate how to accept both card payments and additional payment methods on the web.
If you’re running a compatible browser, this demo also showcases the [Payment Request API](https://stripe.com/docs/payment-request-api), [Apple Pay](https://stripe.com/docs/apple-pay), [Google Pay](https://stripe.com/docs/google-pay), and [Microsoft Pay](https://stripe.com/docs/microsoft-pay) for a seamless payment experience.
**You can see this demo app running in test mode on [stripe-payments-demo.appspot.com](https://stripe-payments-demo.appspot.com).**
+️⚠️ [️PaymentIntents](https://stripe.com/docs/payments/payment-intents) is now the recommended integration path for 3D Secure authentication. It lets you benefit from [Dynamic 3D Secure](https://stripe.com/docs/payments/dynamic-3ds) and helps you prepare for [Strong Customer Authentication](https://stripe.com/guides/strong-customer-authentication) regulation in Europe. If you integrate 3D Secure on PaymentIntents today, we’ll seamlessly transition you to [3D Secure 2](https://stripe.com/guides/3d-secure-2) once supported—without requiring any changes to your integration. As a reference you can find the previous integration that uses the Sources API for 3D Secure on [this branch](https://github.com/stripe/stripe-payments-demo/tree/legacy-cards-3d-secure).
+
## Overview
@@ -19,7 +21,7 @@ This demo provides an all-in-one example for integrating with Stripe on the web:
💳 | **Card payments with Payment Request, Apple Pay, Google Pay, and Microsoft Pay.** The app offers frictionless card payment experiences with a single integration using the new [Payment Request Button Element](https://stripe.com/docs/elements/payment-request-button).
🌍 | **Payment methods for Europe and Asia.** A dozen redirect-based payment methods are supported through the [Sources API](https://stripe.com/docs/sources), from iDEAL to WeChat Pay.
🎩 | **Automatic payment methods suggestion.** Picking a country will automatically show relevant payment methods. For example, selecting “Germany” will suggest SOFORT, Giropay, and SEPA Debit.
-🔐 | **Dynamic 3D Secure for Visa and Mastercard.** The app automatically handles the correct flow to complete card payments with [3D Secure](https://stripe.com/docs/sources/three-d-secure), whether it’s required by the card or by the app above a certain amount.
+🔐 | **Dynamic 3D Secure for Visa and Mastercard.** The app automatically handles the correct flow to complete card payments with [3D Secure](https://stripe.com/docs/payments/dynamic-3ds), whether it’s required by the card or encoded in one of your [3D Secure Radar rules](https://dashboard.stripe.com/radar/rules).
📲 | **QR code generation for WeChat Pay.** During the payment process for [WeChat Pay](https://stripe.com/payments/payment-methods-guide#wechat-pay), a QR code is generated for the WeChat Pay URL to authorize the payment in the WeChat app.
🚀 | **Built-in proxy for local HTTPS and webhooks.** Card payments require HTTPS and asynchronous payment methods with redirects rely on webhooks to complete transactions—[ngrok](https://ngrok.com/) is integrated so the app is served locally over HTTPS and an endpoint is publicly exposed for webhooks.
🔧 | **Webhook signing and idempotency keys**. We verify webhook signatures and pass idempotency keys to charge creations, two recommended practices for asynchronous payment flows.
diff --git a/package-lock.json b/package-lock.json
index c97216bc..48798fa1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,15 +16,15 @@
"integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
"requires": {
"bytes": "3.0.0",
- "content-type": "1.0.4",
+ "content-type": "~1.0.4",
"debug": "2.6.9",
- "depd": "1.1.2",
- "http-errors": "1.6.2",
+ "depd": "~1.1.1",
+ "http-errors": "~1.6.2",
"iconv-lite": "0.4.19",
- "on-finished": "2.3.0",
+ "on-finished": "~2.3.0",
"qs": "6.5.1",
"raw-body": "2.3.2",
- "type-is": "1.6.15"
+ "type-is": "~1.6.15"
},
"dependencies": {
"bytes": {
@@ -63,7 +63,7 @@
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
- "statuses": "1.4.0"
+ "statuses": ">= 1.3.1 < 2"
},
"dependencies": {
"depd": {
@@ -98,7 +98,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"requires": {
- "mime-db": "1.30.0"
+ "mime-db": "~1.30.0"
}
},
"ms": {
@@ -146,7 +146,7 @@
"integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
"requires": {
"media-typer": "0.3.0",
- "mime-types": "2.1.17"
+ "mime-types": "~2.1.15"
}
},
"unpipe": {
@@ -171,36 +171,36 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz",
"integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=",
"requires": {
- "accepts": "1.3.4",
+ "accepts": "~1.3.4",
"array-flatten": "1.1.1",
"body-parser": "1.18.2",
"content-disposition": "0.5.2",
- "content-type": "1.0.4",
+ "content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
- "depd": "1.1.2",
- "encodeurl": "1.0.1",
- "escape-html": "1.0.3",
- "etag": "1.8.1",
+ "depd": "~1.1.1",
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
"finalhandler": "1.1.0",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
- "methods": "1.1.2",
- "on-finished": "2.3.0",
- "parseurl": "1.3.2",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
- "proxy-addr": "2.0.2",
+ "proxy-addr": "~2.0.2",
"qs": "6.5.1",
- "range-parser": "1.2.0",
+ "range-parser": "~1.2.0",
"safe-buffer": "5.1.1",
"send": "0.16.1",
"serve-static": "1.13.1",
"setprototypeof": "1.1.0",
- "statuses": "1.3.1",
- "type-is": "1.6.15",
+ "statuses": "~1.3.1",
+ "type-is": "~1.6.15",
"utils-merge": "1.0.1",
- "vary": "1.1.2"
+ "vary": "~1.1.2"
},
"dependencies": {
"accepts": {
@@ -208,7 +208,7 @@
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
"integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=",
"requires": {
- "mime-types": "2.1.17",
+ "mime-types": "~2.1.16",
"negotiator": "0.6.1"
}
},
@@ -281,12 +281,12 @@
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
"requires": {
"debug": "2.6.9",
- "encodeurl": "1.0.1",
- "escape-html": "1.0.3",
- "on-finished": "2.3.0",
- "parseurl": "1.3.2",
- "statuses": "1.3.1",
- "unpipe": "1.0.0"
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.3.1",
+ "unpipe": "~1.0.0"
}
},
"forwarded": {
@@ -307,7 +307,7 @@
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
- "statuses": "1.3.1"
+ "statuses": ">= 1.3.1 < 2"
},
"dependencies": {
"depd": {
@@ -362,7 +362,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"requires": {
- "mime-db": "1.30.0"
+ "mime-db": "~1.30.0"
}
},
"ms": {
@@ -398,7 +398,7 @@
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz",
"integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=",
"requires": {
- "forwarded": "0.1.2",
+ "forwarded": "~0.1.2",
"ipaddr.js": "1.5.2"
}
},
@@ -423,18 +423,18 @@
"integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==",
"requires": {
"debug": "2.6.9",
- "depd": "1.1.2",
- "destroy": "1.0.4",
- "encodeurl": "1.0.1",
- "escape-html": "1.0.3",
- "etag": "1.8.1",
+ "depd": "~1.1.1",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
"fresh": "0.5.2",
- "http-errors": "1.6.2",
+ "http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
- "on-finished": "2.3.0",
- "range-parser": "1.2.0",
- "statuses": "1.3.1"
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.3.1"
}
},
"serve-static": {
@@ -442,9 +442,9 @@
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz",
"integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==",
"requires": {
- "encodeurl": "1.0.1",
- "escape-html": "1.0.3",
- "parseurl": "1.3.2",
+ "encodeurl": "~1.0.1",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
"send": "0.16.1"
}
},
@@ -464,7 +464,7 @@
"integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
"requires": {
"media-typer": "0.3.0",
- "mime-types": "2.1.17"
+ "mime-types": "~2.1.15"
}
},
"unpipe": {
@@ -502,16 +502,21 @@
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+ },
"morgan": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz",
"integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=",
"requires": {
- "basic-auth": "2.0.0",
+ "basic-auth": "~2.0.0",
"debug": "2.6.9",
- "depd": "1.1.2",
- "on-finished": "2.3.0",
- "on-headers": "1.0.1"
+ "depd": "~1.1.1",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.1"
},
"dependencies": {
"basic-auth": {
@@ -571,12 +576,12 @@
"integrity": "sha512-MSqiI2gGMIvGABwm6UUbJkwX9zW0grAqSjxS8NxBsvyXDSHYkHaxEjyAuOaKn/LpNRiZR3PilKZ4dp5pY0oPpg==",
"dev": true,
"requires": {
- "@types/node": "8.5.9",
- "async": "2.6.0",
- "decompress-zip": "0.3.0",
- "lock": "0.1.4",
- "request": "2.83.0",
- "uuid": "3.2.1"
+ "@types/node": "^8.0.19",
+ "async": "^2.3.0",
+ "decompress-zip": "^0.3.0",
+ "lock": "^0.1.2",
+ "request": "^2.55.0",
+ "uuid": "^3.0.0"
},
"dependencies": {
"abbrev": {
@@ -591,10 +596,10 @@
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true,
"requires": {
- "co": "4.6.0",
- "fast-deep-equal": "1.0.0",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.3.1"
+ "co": "^4.6.0",
+ "fast-deep-equal": "^1.0.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.3.0"
}
},
"asn1": {
@@ -615,7 +620,7 @@
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
"dev": true,
"requires": {
- "lodash": "4.17.4"
+ "lodash": "^4.14.0"
}
},
"asynckit": {
@@ -643,7 +648,7 @@
"dev": true,
"optional": true,
"requires": {
- "tweetnacl": "0.14.5"
+ "tweetnacl": "^0.14.3"
}
},
"binary": {
@@ -652,8 +657,8 @@
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
"dev": true,
"requires": {
- "buffers": "0.1.1",
- "chainsaw": "0.1.0"
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
}
},
"boom": {
@@ -662,7 +667,7 @@
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
"dev": true,
"requires": {
- "hoek": "4.2.0"
+ "hoek": "4.x.x"
}
},
"buffers": {
@@ -683,7 +688,7 @@
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
"dev": true,
"requires": {
- "traverse": "0.3.9"
+ "traverse": ">=0.3.0 <0.4"
}
},
"co": {
@@ -698,7 +703,7 @@
"integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
"dev": true,
"requires": {
- "delayed-stream": "1.0.0"
+ "delayed-stream": "~1.0.0"
}
},
"core-util-is": {
@@ -713,7 +718,7 @@
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
"dev": true,
"requires": {
- "boom": "5.2.0"
+ "boom": "5.x.x"
},
"dependencies": {
"boom": {
@@ -722,7 +727,7 @@
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"dev": true,
"requires": {
- "hoek": "4.2.0"
+ "hoek": "4.x.x"
}
}
}
@@ -733,7 +738,7 @@
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"dev": true,
"requires": {
- "assert-plus": "1.0.0"
+ "assert-plus": "^1.0.0"
}
},
"decompress-zip": {
@@ -742,12 +747,12 @@
"integrity": "sha1-rjvLfjTGWHmt/nfhnDD4ZgK0vbA=",
"dev": true,
"requires": {
- "binary": "0.3.0",
- "graceful-fs": "4.1.11",
- "mkpath": "0.1.0",
- "nopt": "3.0.6",
- "q": "1.5.1",
- "readable-stream": "1.1.14",
+ "binary": "^0.3.0",
+ "graceful-fs": "^4.1.3",
+ "mkpath": "^0.1.0",
+ "nopt": "^3.0.1",
+ "q": "^1.1.2",
+ "readable-stream": "^1.1.8",
"touch": "0.0.3"
}
},
@@ -764,7 +769,7 @@
"dev": true,
"optional": true,
"requires": {
- "jsbn": "0.1.1"
+ "jsbn": "~0.1.0"
}
},
"extend": {
@@ -791,9 +796,9 @@
"integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
"dev": true,
"requires": {
- "asynckit": "0.4.0",
- "combined-stream": "1.0.5",
- "mime-types": "2.1.17"
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.5",
+ "mime-types": "^2.1.12"
}
},
"getpass": {
@@ -802,7 +807,7 @@
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"dev": true,
"requires": {
- "assert-plus": "1.0.0"
+ "assert-plus": "^1.0.0"
}
},
"graceful-fs": {
@@ -823,8 +828,8 @@
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"dev": true,
"requires": {
- "ajv": "5.5.2",
- "har-schema": "2.0.0"
+ "ajv": "^5.1.0",
+ "har-schema": "^2.0.0"
}
},
"hawk": {
@@ -833,10 +838,10 @@
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
"dev": true,
"requires": {
- "boom": "4.3.1",
- "cryptiles": "3.1.2",
- "hoek": "4.2.0",
- "sntp": "2.1.0"
+ "boom": "4.x.x",
+ "cryptiles": "3.x.x",
+ "hoek": "4.x.x",
+ "sntp": "2.x.x"
}
},
"hoek": {
@@ -851,9 +856,9 @@
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"dev": true,
"requires": {
- "assert-plus": "1.0.0",
- "jsprim": "1.4.1",
- "sshpk": "1.13.1"
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
}
},
"inherits": {
@@ -935,7 +940,7 @@
"integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
"dev": true,
"requires": {
- "mime-db": "1.30.0"
+ "mime-db": "~1.30.0"
}
},
"mkpath": {
@@ -950,7 +955,7 @@
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
- "abbrev": "1.1.1"
+ "abbrev": "1"
}
},
"oauth-sign": {
@@ -989,10 +994,10 @@
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dev": true,
"requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
"isarray": "0.0.1",
- "string_decoder": "0.10.31"
+ "string_decoder": "~0.10.x"
}
},
"request": {
@@ -1001,28 +1006,28 @@
"integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
"dev": true,
"requires": {
- "aws-sign2": "0.7.0",
- "aws4": "1.6.0",
- "caseless": "0.12.0",
- "combined-stream": "1.0.5",
- "extend": "3.0.1",
- "forever-agent": "0.6.1",
- "form-data": "2.3.1",
- "har-validator": "5.0.3",
- "hawk": "6.0.2",
- "http-signature": "1.2.0",
- "is-typedarray": "1.0.0",
- "isstream": "0.1.2",
- "json-stringify-safe": "5.0.1",
- "mime-types": "2.1.17",
- "oauth-sign": "0.8.2",
- "performance-now": "2.1.0",
- "qs": "6.5.1",
- "safe-buffer": "5.1.1",
- "stringstream": "0.0.5",
- "tough-cookie": "2.3.3",
- "tunnel-agent": "0.6.0",
- "uuid": "3.2.1"
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.6.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.5",
+ "extend": "~3.0.1",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.1",
+ "har-validator": "~5.0.3",
+ "hawk": "~6.0.2",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.17",
+ "oauth-sign": "~0.8.2",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.1",
+ "safe-buffer": "^5.1.1",
+ "stringstream": "~0.0.5",
+ "tough-cookie": "~2.3.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.1.0"
}
},
"safe-buffer": {
@@ -1037,7 +1042,7 @@
"integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==",
"dev": true,
"requires": {
- "hoek": "4.2.0"
+ "hoek": "4.x.x"
}
},
"sshpk": {
@@ -1046,14 +1051,14 @@
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
"dev": true,
"requires": {
- "asn1": "0.2.3",
- "assert-plus": "1.0.0",
- "bcrypt-pbkdf": "1.0.1",
- "dashdash": "1.14.1",
- "ecc-jsbn": "0.1.1",
- "getpass": "0.1.7",
- "jsbn": "0.1.1",
- "tweetnacl": "0.14.5"
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "tweetnacl": "~0.14.0"
}
},
"string_decoder": {
@@ -1074,7 +1079,7 @@
"integrity": "sha1-Ua7z1ElXHU8oel2Hyci0kYGg2x0=",
"dev": true,
"requires": {
- "nopt": "1.0.10"
+ "nopt": "~1.0.10"
},
"dependencies": {
"nopt": {
@@ -1083,7 +1088,7 @@
"integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
"dev": true,
"requires": {
- "abbrev": "1.1.1"
+ "abbrev": "1"
}
}
}
@@ -1094,7 +1099,7 @@
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
"dev": true,
"requires": {
- "punycode": "1.4.1"
+ "punycode": "^1.4.1"
}
},
"traverse": {
@@ -1109,7 +1114,7 @@
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"dev": true,
"requires": {
- "safe-buffer": "5.1.1"
+ "safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
@@ -1131,44 +1136,31 @@
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"dev": true,
"requires": {
- "assert-plus": "1.0.0",
+ "assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
- "extsprintf": "1.3.0"
+ "extsprintf": "^1.2.0"
}
}
}
},
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
"stripe": {
- "version": "4.25.0",
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-4.25.0.tgz",
- "integrity": "sha512-sSRPSQ4BTSbdcevVSrtIJzlOCTIAXm8T38DE4zPL6ysYpIWGfIBdo2XnhouLK12/6cuLvaEInlfCZQgoEVzXpQ==",
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.13.0.tgz",
+ "integrity": "sha512-F2xSq3A7AuPTt+TW/dVCtagmCVUJpgzytUd49Hq7ncZQ7T11SFgbShhi/Ki0RpxfcKcIBBAm7f5V5jvB4bzvpQ==",
"requires": {
- "bluebird": "2.11.0",
- "lodash.isplainobject": "4.0.6",
- "object-assign": "4.1.1",
- "qs": "6.0.4"
- },
- "dependencies": {
- "bluebird": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
- "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE="
- },
- "lodash.isplainobject": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
- "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
- },
- "qs": {
- "version": "6.0.4",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.4.tgz",
- "integrity": "sha1-UQGdhHIMk5uCc36EVWp4Izjs6ns="
- }
+ "lodash.isplainobject": "^4.0.6",
+ "qs": "~6.5.1",
+ "safe-buffer": "^5.1.1"
}
}
}
diff --git a/package.json b/package.json
index 63592005..a34e1b7d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,6 @@
{
"name": "stripe-payments-demo",
- "description":
- "Sample store accepting universal payments on the web with Stripe Elements, Payment Request, Apple Pay, Google Pay, Microsoft Pay, and the Sources API.",
+ "description": "Sample store accepting universal payments on the web with Stripe Elements, Payment Request, Apple Pay, Google Pay, Microsoft Pay, and the Sources API.",
"version": "0.0.1",
"private": true,
"author": "Romain Huet",
@@ -22,7 +21,7 @@
"ejs": "^2.5.7",
"express": "^4.15.2",
"morgan": "^1.8.1",
- "stripe": "^4.18.0"
+ "stripe": "^6.13.0"
},
"devDependencies": {
"ngrok": "^2.2.9"
diff --git a/public/javascripts/payments.js b/public/javascripts/payments.js
index e5f1812b..9791fb2a 100644
--- a/public/javascripts/payments.js
+++ b/public/javascripts/payments.js
@@ -25,7 +25,10 @@
*/
// Create a Stripe client.
- const stripe = Stripe(config.stripePublishableKey);
+ const stripe = Stripe(
+ config.stripePublishableKey,
+ {betas: ['payment_intent_beta_3']},
+ );
// Create an instance of Elements.
const elements = stripe.elements();
@@ -169,11 +172,37 @@
postal_code: event.shippingAddress.postalCode,
state: event.shippingAddress.region,
},
+ },
+ true,
+ );
+ // Confirm the PaymentIntent with the source returned on the event.
+ const {paymentIntent, error} = await stripe.confirmPaymentIntent(
+ order.paymentIntent.client_secret,
+ {
+ source: event.source.id,
+ use_stripe_sdk: true,
}
);
- // Complete the order using the payment source generated by Payment Request.
- await handleOrder(order, event.source);
- event.complete('success');
+ if (error) {
+ event.complete('fail');
+ await handleOrder({metadata: {status: 'failed'}}, null, error);
+ } else {
+ event.complete('success');
+ if (paymentIntent.status === 'succeeded') {
+ // No authentication required, show success message.
+ await handleOrder({metadata: {status: 'paid'}}, null, null);
+ } else if (paymentIntent.status === 'requires_source_action') {
+ // We need to perform authentication.
+ const {error: handleError} = await stripe.handleCardPayment(order.paymentIntent.client_secret);
+ if (handleError) {
+ // 3D Secure authentication failed.
+ await handleOrder({metadata: {status: 'failed'}}, null, error);
+ } else {
+ // 3D Secure authentication successful.
+ await handleOrder({metadata: {status: 'paid'}}, null, null);
+ }
+ }
+ }
} catch (error) {
event.complete('fail');
}
@@ -242,23 +271,38 @@
};
// Disable the Pay button to prevent multiple click events.
submitButton.disabled = true;
+ submitButton.textContent = 'Processing Payment…';
// Create the order using the email and shipping information from the form.
+ // For Demo purposes we only create an order / a PaymentIntent on form submit.
+ // In a real application you should create this before entering the checkout, see
+ // https://stripe.com/docs/payments/payment-intents
+ const usesPaymentIntent = payment === 'card';
+ // Note: PaymentIntents Beta currently only support card sources to enable dynamic authentication:
+ // https://stripe.com/docs/payments/dynamic-3ds
const order = await store.createOrder(
config.currency,
store.getOrderItems(),
email,
- shipping
+ shipping,
+ usesPaymentIntent,
);
- if (payment === 'card') {
- // Create a Stripe source from the card information and the owner name.
- const {source} = await stripe.createSource(card, {
- owner: {
- name,
+ if (usesPaymentIntent) {
+ // Let Stripe handle source activation
+ const {paymentIntent, error} = await stripe
+ .handleCardPayment(order.paymentIntent.client_secret, card, {
+ source_data: {
+ owner: {
+ name,
+ },
},
});
- await handleOrder(order, source);
+ if (error) {
+ await handleOrder({metadata: {status: 'failed'}}, null, error);
+ } else if (paymentIntent.status === 'succeeded') {
+ await handleOrder({metadata: {status: 'paid'}}, null, null);
+ }
} else if (payment === 'sepa_debit') {
// Create a SEPA Debit source from the IBAN information.
const sourceData = {
@@ -273,6 +317,9 @@
// once the source is charged.
notification_method: 'email',
},
+ metadata: {
+ order: order.id,
+ },
};
const {source} = await stripe.createSource(iban, sourceData);
await handleOrder(order, source);
diff --git a/public/javascripts/store.js b/public/javascripts/store.js
index d062e652..91b011a9 100644
--- a/public/javascripts/store.js
+++ b/public/javascripts/store.js
@@ -60,7 +60,7 @@ class Store {
}
// Create an order object to represent the line items.
- async createOrder(currency, items, email, shipping) {
+ async createOrder(currency, items, email, shipping, createIntent=false) {
try {
const response = await fetch('/orders', {
method: 'POST',
@@ -70,6 +70,7 @@ class Store {
items,
email,
shipping,
+ createIntent
}),
});
const data = await response.json();
@@ -152,7 +153,7 @@ class Store {
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
- const quantity = randomQuantity(1, 3);
+ const quantity = randomQuantity(1, 2);
let sku = product.skus.data[0];
let skuPrice = this.formatPrice(sku.price, sku.currency);
let lineItemPrice = this.formatPrice(sku.price * quantity, sku.currency);
diff --git a/server/node/inventory.js b/server/node/inventory.js
index effe9a50..ed7d43ac 100644
--- a/server/node/inventory.js
+++ b/server/node/inventory.js
@@ -14,8 +14,9 @@ const stripe = require('stripe')(config.stripe.secretKey);
stripe.setApiVersion(config.stripe.apiVersion);
// Create an order.
-const createOrder = async (currency, items, email, shipping) => {
- return await stripe.orders.create({
+const createOrder = async (currency, items, email, shipping, createIntent) => {
+ // Create order
+ let order = await stripe.orders.create({
currency,
items,
email,
@@ -24,6 +25,24 @@ const createOrder = async (currency, items, email, shipping) => {
status: 'created',
},
});
+ if (createIntent) {
+ // Create PaymentIntent to represent your customer's intent to pay this order.
+ // Note: PaymentIntents currently only support card sources to enable dynamic authentication:
+ // // https://stripe.com/docs/payments/dynamic-3ds
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: order.amount,
+ currency: order.currency,
+ metadata: {
+ order: order.id,
+ },
+ allowed_source_types: ['card'],
+ });
+ // Add PaymentIntent to order object so our frontend can access the client_secret.
+ // The client_secret is used on the frontend to confirm the PaymentIntent and create a payment.
+ // Therefore, do not log, store, or append the client_secret to a URL.
+ order.paymentIntent = paymentIntent;
+ }
+ return order;
};
// Retrieve an order by ID.
diff --git a/server/node/routes.js b/server/node/routes.js
index 24689444..19c12bc2 100644
--- a/server/node/routes.js
+++ b/server/node/routes.js
@@ -34,9 +34,9 @@ router.get('/', (req, res) => {
// Create an order on the backend.
router.post('/orders', async (req, res, next) => {
- let {currency, items, email, shipping} = req.body;
+ let {currency, items, email, shipping, createIntent} = req.body;
try {
- let order = await orders.create(currency, items, email, shipping);
+ let order = await orders.create(currency, items, email, shipping, createIntent);
return res.status(200).json({order});
} catch (err) {
return res.status(500).json({error: err.message});
@@ -56,15 +56,6 @@ router.post('/orders/:id/pay', async (req, res, next) => {
) {
return res.status(403).json({order, source});
}
- // Dynamically evaluate if 3D Secure should be used.
- if (source && source.type === 'card') {
- // A 3D Secure source may be created referencing the card source.
- source = await dynamic3DS(source, order, req);
- }
- // Demo: In test mode, replace the source with a test token so charges can work.
- if (source.type === 'card' && !source.livemode) {
- source.id = 'tok_visa';
- }
// Pay the order using the Stripe source.
if (source && source.status === 'chargeable') {
let charge, status;
@@ -106,6 +97,7 @@ router.post('/orders/:id/pay', async (req, res, next) => {
// Webhook handler to process payments for sources asynchronously.
router.post('/webhook', async (req, res) => {
let data;
+ let eventType;
// Check if webhook signing is configured.
if (config.stripe.webhookSecret) {
// Retrieve the event by verifying the signature using the raw body and secret.
@@ -123,13 +115,35 @@ router.post('/webhook', async (req, res) => {
}
// Extract the object from the event.
data = event.data;
+ eventType = event.type;
} else {
// Webhook signing is recommended, but if the secret is not configured in `config.js`,
// retrieve the event data directly from the request body.
data = req.body.data;
+ eventType = req.body.type;
}
const object = data.object;
+ // PaymentIntent Beta, see https://stripe.com/docs/payments/payment-intents
+ // Monitor payment_intent.succeeded & payment_intent.payment_failed events.
+ if (
+ object.object === 'payment_intent' &&
+ object.metadata.order
+ ) {
+ const paymentIntent = object;
+ // Find the corresponding order this source is for by looking in its metadata.
+ const order = await orders.retrieve(paymentIntent.metadata.order);
+ if (eventType === 'payment_intent.succeeded') {
+ console.log(`🔔 Webhook received! Payment for PaymentIntent ${paymentIntent.id} succeeded.`);
+ // Update the order status to mark it as paid.
+ await orders.update(order.id, {metadata: {status: 'paid'}});
+ } else if (eventType === 'payment_intent.payment_failed') {
+ console.log(`🔔 Webhook received! Payment on source ${paymentIntent.last_payment_error.source.id} for PaymentIntent ${paymentIntent.id} failed.`);
+ // Note: you can use the existing PaymentIntent to prompt your customer to try again by attaching a newly created source:
+ // https://stripe.com/docs/payments/payment-intents#lifecycle
+ }
+ }
+
// Monitor `source.chargeable` events.
if (
object.object === 'source' &&
@@ -222,28 +236,6 @@ router.post('/webhook', async (req, res) => {
res.sendStatus(200);
});
-// Dynamically create a 3D Secure source.
-const dynamic3DS = async (source, order, req) => {
- // Check if 3D Secure is required, or trigger it based on a custom rule (in this case, if the amount is above a threshold).
- if (source.card.three_d_secure === 'required' || order.amount > 5000) {
- source = await stripe.sources.create({
- amount: order.amount,
- currency: order.currency,
- type: 'three_d_secure',
- three_d_secure: {
- card: source.id,
- },
- metadata: {
- order: order.id,
- },
- redirect: {
- return_url: req.headers.origin,
- },
- });
- }
- return source;
-};
-
/**
* Routes exposing the config as well as the ability to retrieve products and orders.
*/
diff --git a/server/python/app.py b/server/python/app.py
index 0e073b00..e3b5f3cb 100644
--- a/server/python/app.py
+++ b/server/python/app.py
@@ -24,17 +24,6 @@
app = Flask(__name__, static_folder=static_dir)
-def dynamic_3ds(source: Source, order: Order) -> Source:
- """
- Create a 3DS Secure payment Source if the Source is a card that requires it or if the Order is over 5000.
- """
- if source['card']['three_d_secure'] == 'required' or order['amount'] > 5000:
- source = stripe.Source.create(amount=order['amount'], currency=order['currency'], type='three_d_secure',
- three_d_secure={'card': source['id']}, metadata={'order': order['id']},
- redirect={'return_url': request.headers.get('origin')})
- return source
-
-
@app.route('/')
def home():
return send_from_directory(static_dir, 'index.html')
@@ -91,7 +80,7 @@ def make_order():
data = json.loads(request.data)
try:
order = Inventory.create_order(currency=data['currency'], items=data['items'], email=data['email'],
- shipping=data['shipping'])
+ shipping=data['shipping'], create_intent=data['createIntent'])
return jsonify({'order': order})
except Exception as e:
@@ -113,13 +102,6 @@ def pay_order(order_id):
# Somehow this Order has already been paid for -- abandon request.
return jsonify({'source': source, 'order': order}), 403
- if source['type'] == 'card':
- source = dynamic_3ds(source, order)
-
- if not source['livemode']:
- # Demo: In test mode, replace the Source with a test token so Charges can work.
- source['id'] = 'tok_visa'
-
if source['status'] == 'chargeable':
# Yay! Our user gave us a valid payment Source we can charge.
charge = stripe.Charge.create(source=source['id'], amount=order['amount'], currency=order['currency'],
@@ -133,7 +115,8 @@ def pay_order(order_id):
status = 'failed'
# Update the Order with a new status based on what happened with the Charge.
- Inventory.update_order(properties={'metadata': {'status': status}}, order=order)
+ Inventory.update_order(
+ properties={'metadata': {'status': status}}, order=order)
return jsonify({'order': order, 'source': source})
@@ -149,15 +132,31 @@ def webhook_received():
# Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.
signature = request.headers.get('stripe-signature')
try:
- event = stripe.Webhook.construct_event(payload=request.data, sig_header=signature, secret=webhook_secret)
+ event = stripe.Webhook.construct_event(
+ payload=request.data, sig_header=signature, secret=webhook_secret)
data = event['data']
except Exception as e:
return e
+ # Get the type of webhook event sent - used to check the status of PaymentIntents.
+ event_type = event['type']
else:
data = request_data['data']
-
+ event_type = request_data['type']
data_object = data['object']
+ # PaymentIntent Beta, see https://stripe.com/docs/payments/payment-intents
+ # Monitor payment_intent.succeeded & payment_intent.payment_failed events.
+ if data_object['object'] == 'payment_intent' and 'order' in data_object['metadata']:
+ payment_intent = data_object
+ order = stripe.Order.retrieve(payment_intent['metadata']['order'])
+
+ if event_type == 'payment_intent.succeeded':
+ print('🔔 Webhook received! Payment for PaymentIntent ' +
+ payment_intent['id']+' succeeded')
+ elif event_type == 'payment_intent.payment_failed':
+ print('🔔 Webhook received! Payment on source ' + payment_intent['last_payment_error']['source']['id'] +
+ ' for PaymentIntent ' + payment_intent['id'] + ' failed.')
+
# Monitor `source.chargeable` events.
if data_object['object'] == 'source' \
and data_object['status'] == 'chargeable' \
@@ -190,7 +189,8 @@ def webhook_received():
# For the demo, we simply set the status to mark the Order as failed.
status = 'failed'
- Inventory.update_order(properties={'metadata': {'status': status}}, order=order)
+ Inventory.update_order(
+ properties={'metadata': {'status': status}}, order=order)
# Monitor `charge.succeeded` events.
if data_object['object'] == 'charge' \
@@ -199,7 +199,7 @@ def webhook_received():
charge = data_object
print(f'Webhook received! The charge {charge["id"]} succeeded.')
Inventory.update_order(properties={'metadata': {'status': 'paid'}},
- order_id=charge['source']['metadata']['order'])
+ order_id=charge['source']['metadata']['order'])
# Monitor `source.failed`, `source.canceled`, and `charge.failed` events.
if data_object['object'] in ['source', 'charge'] and data_object['status'] in ['failed', 'canceled']:
@@ -208,7 +208,7 @@ def webhook_received():
if source['metadata']['order']:
Inventory.update_order(properties={'metadata': {'status': 'failed'}},
- order_id=source['metadata']['order']['id'])
+ order_id=source['metadata']['order']['id'])
return jsonify({'status': 'success'})
diff --git a/server/python/inventory.py b/server/python/inventory.py
index 26de2ff5..fab43fa0 100644
--- a/server/python/inventory.py
+++ b/server/python/inventory.py
@@ -18,11 +18,23 @@
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
stripe.api_version = '2018-02-06'
+
class Inventory:
@staticmethod
- def create_order(currency: str, items: list, email: str, shipping: dict) -> Order:
- return stripe.Order.create(currency=currency, items=items, email=email, shipping=shipping,
- metadata={'status': 'created'})
+ def create_order(currency: str, items: list, email: str, shipping: dict, create_intent: bool) -> Order:
+ order = stripe.Order.create(currency=currency, items=items,
+ email=email, shipping=shipping, metadata={'status': 'created'})
+ if create_intent:
+ # Create PaymentIntent to represent customers intent to pay this order.
+ # Note: PaymentIntents currently only support card sources to enable dynamic authentication:
+ # https://stripe.com/docs/payments/dynamic-authentication
+ payment_intent = stripe.PaymentIntent.create(
+ amount=order['amount'], currency=currency, metadata={'order': order['id']}, allowed_source_types=['card'])
+ # Add PaymentIntent to order object so our frontend can access the client_secret.
+ # The client_secret is used on the frontend to confirm the PaymentIntent and create a payment.
+ # Therefore, do not log, store, or append the client_secret to a URL.
+ order['paymentIntent'] = payment_intent
+ return order
@staticmethod
def retrieve_order(order_id: str) -> Order:
diff --git a/server/python/requirements.txt b/server/python/requirements.txt
index 919caa60..43fd7ad9 100644
--- a/server/python/requirements.txt
+++ b/server/python/requirements.txt
@@ -6,7 +6,7 @@ click==6.7
Flask==0.12.4
gunicorn==19.7.1
idna==2.6
-idna-ssl==1.0.1
+idna-ssl==1.1.0
itsdangerous==0.24
Jinja2==2.10
livereload==2.5.1
@@ -17,9 +17,9 @@ pluggy==0.6.0
py==1.5.3
pytest==3.5.0
python-dotenv==0.8.2
-requests==2.18.4
+requests==2.20
six==1.11.0
-stripe==1.79.1
+stripe==2.12.0
tornado==5.0.2
urllib3==1.22
Werkzeug==0.14.1