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 Demo on Google ChromeDemo on Safari iPhone X @@ -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