diff --git a/.env.example b/.env.example index 48ebb058..4d13e9d8 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,12 @@ STRIPE_WEBHOOK_SECRET= # Stripe account country (required for Payment Request). STRIPE_ACCOUNT_COUNTRY=US +# Supported payment methods for the store. +# Some payment methods support only a subset of currencies. +# Make sure to check the docs: https://stripe.com/docs/sources +# Only used for the python server. For Node.js see the server/node/config.js file! +PAYMENT_METHODS="alipay, bancontact, card, eps, ideal, giropay, multibanco, sofort, wechat" + # Optional ngrok configuration for development (if you have a paid ngrok account). NGROK_SUBDOMAIN= NGROK_AUTHTOKEN= \ No newline at end of file diff --git a/README.md b/README.md index 56b804eb..44015ddd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This demo provides an all-in-one example for integrating with Stripe on the web: 🚀 | **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. 📱 | **Responsive design**. The checkout experience works on all screen sizes. Apple Pay works on Safari for iPhone and iPad if the Wallet is enabled, and Payment Request works on Chrome for Android. -📦 | **No datastore required.** Products, SKUs, and Orders are stored using the [Stripe Orders API](https://stripe.com/docs/orders), which you can replace with your own database to keep track of orders and inventory. +📦 | **No datastore required.** Products, and SKUs are stored using the [Stripe API](https://stripe.com/docs/api/products), which you can replace with your own database to keep track of products and inventory. ## Payments Integration @@ -67,9 +67,9 @@ There are a couple server implementations in the [`server`](/server) directory. You’ll need the following: -* [Node.js](http://nodejs.org) >= 8.x. -* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). -* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free). +- [Node.js](http://nodejs.org) >= 8.x. +- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). +- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free). In your Stripe Dashboard, you can [enable the payment methods](https://dashboard.stripe.com/payments/settings) you’d like to test with one click. @@ -87,7 +87,7 @@ Install dependencies using npm: npm install -This demo uses the Stripe API as a datastore for products and orders, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products within Stripe. +This demo uses the Stripe API as a datastore for products and SKUs, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products and SKUs within Stripe. Run the app: @@ -111,5 +111,5 @@ Use this second URL in your browser to start the demo. ## Credits -* Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/thorwebdev) -* Design: [Tatiana Van Campenhout](https://twitter.com/tatsvc) +- Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/thorwebdev) +- Design: [Tatiana Van Campenhout](https://twitter.com/tatsvc) diff --git a/server/node/README.md b/server/node/README.md index d081e4f1..d8a33253 100644 --- a/server/node/README.md +++ b/server/node/README.md @@ -6,9 +6,9 @@ This directory contains the main Node implementation of the payments server. You’ll need the following: -* [Node.js](http://nodejs.org) >= 8.x. -* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). -* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free). +- [Node.js](http://nodejs.org) >= 8.x. +- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). +- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free). In your Stripe Dashboard, you can [enable the payment methods](https://dashboard.stripe.com/payments/settings) you’d like to test with one click. @@ -26,7 +26,7 @@ Install dependencies using npm: npm install -This demo uses the Stripe API as a datastore for products and orders, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products within Stripe. +This demo uses the Stripe API as a datastore for products and SKUs, but you can always choose to use your own datastore instead. When starting the app for the first time, the initial loading can take a couple of seconds as it will automatically set up the products and SKUs within Stripe. Run the app: @@ -50,4 +50,4 @@ Use this second URL in your browser to start the demo. ## Credits -* Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/schaeff_t) +- Code: [Romain Huet](https://twitter.com/romainhuet) and [Thorsten Schaeff](https://twitter.com/schaeff_t) diff --git a/server/python/README.md b/server/python/README.md index 80f57931..1715a573 100644 --- a/server/python/README.md +++ b/server/python/README.md @@ -4,19 +4,19 @@ This demo uses a simple [Flask](http://flask.pocoo.org/) application as the serv ## Payments Integration -* [`app.py`](app.py) contains the routes that interface with Stripe to create charges and receive webhook events. -* [`setup.py`](setup.py) a simple setup script to make some fake Products and SKUs for our Stripe store. -* [`tests/tests.py`](tests/tests.py) some unit tests that test the logic of our heavier APIs like `orders//pay` and `/webhook`. -* [`test_data.py`](tests/tests.py) contains some hardcoded mocked responses to test with. -* [`inventory_manager.py`](stripe_lib.py) a minimal wrapper over the Stripe Python SDK that handles creating/fetching orders and products. You can override this class with your own order management system code. +- [`app.py`](app.py) contains the routes that interface with Stripe to create charges and receive webhook events. +- [`setup.py`](setup.py) a simple setup script to make some fake Products and SKUs for our Stripe store. +- [`tests/tests.py`](tests/tests.py) some unit tests that test the logic of our heavier APIs like `/webhook`. +- [`test_data.py`](tests/tests.py) contains some hardcoded mocked responses to test with. +- [`inventory_manager.py`](stripe_lib.py) a minimal wrapper over the Stripe Python SDK that handles creating/fetching products and caluclating payment amounts from SKUs. You can override this class with your own product and order management system code. ## Requirements You’ll need the following: -* [Python 3.6.5](https://www.python.org/downloads/release/python-365/) -* Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). -* Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!) +- [Python 3.6.5](https://www.python.org/downloads/release/python-365/) +- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). +- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!) ## Getting Started @@ -87,4 +87,4 @@ python tests.py ## Credits -* Code: [Adrienne Dreyfus](http://twitter.com/adrind) +- Code: [Adrienne Dreyfus](http://twitter.com/adrind) diff --git a/server/python/app.py b/server/python/app.py index e3b5f3cb..f791ac77 100644 --- a/server/python/app.py +++ b/server/python/app.py @@ -5,7 +5,7 @@ Stripe Payments Demo. Created by Adrienne Dreyfus (@adrind). This is our Flask server that handles requests from our Stripe checkout flow. -It has all the endpoints you need to accept payments and manage orders. +It has all the endpoints you need to accept payments. Python 3.6 or newer required. """ @@ -16,7 +16,7 @@ import os from inventory import Inventory -from stripe_types import Source, Order +from stripe_types import Source from flask import Flask, render_template, jsonify, request, send_from_directory from dotenv import load_dotenv, find_dotenv @@ -53,7 +53,8 @@ def get_config(): 'stripePublishableKey': os.getenv('STRIPE_PUBLISHABLE_KEY'), 'stripeCountry': os.getenv('STRIPE_ACCOUNT_COUNTRY') or 'US', 'country': 'US', - 'currency': 'eur' + 'currency': 'eur', + 'paymentMethods': os.getenv('PAYMENT_METHODS').split(', ') or ['card'] }) @@ -74,51 +75,37 @@ def retrieve_product(product_id): return jsonify(Inventory.retrieve_product(product_id)) -@app.route('/orders', methods=['POST']) -def make_order(): - # Creates a new Order with items that the user selected. +@app.route('/payment_intents', methods=['POST']) +def make_payment_intent(): + # Creates a new PaymentIntent with items from the cart. data = json.loads(request.data) try: - order = Inventory.create_order(currency=data['currency'], items=data['items'], email=data['email'], - shipping=data['shipping'], create_intent=data['createIntent']) - - return jsonify({'order': order}) + payment_intent = stripe.PaymentIntent.create( + amount=Inventory.calculate_payment_amount(items=data['items']), + currency=data['currency'], + payment_method_types=os.getenv( + 'PAYMENT_METHODS').split(', ') or ['card'] + ) + + return jsonify({'paymentIntent': payment_intent}) except Exception as e: return jsonify(e), 403 -@app.route('/orders//pay', methods=['POST']) -def pay_order(order_id): - """ - Creates a Charge for an Order using a payment Source provided by the user. - """ - +@app.route('/payment_intents//shipping_change', methods=['POST']) +def update_payment_intent(id): data = json.loads(request.data) - source = data['source'] - - order = Inventory.retrieve_order(order_id) - - if order['metadata']['status'] == 'pending' or order['metadata']['status'] == 'paid': - # Somehow this Order has already been paid for -- abandon request. - return jsonify({'source': source, 'order': order}), 403 - - 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'], - receipt_email=order['email'], idempotency_key=order['id']) - - if charge and charge['status'] == 'succeeded': - status = 'paid' - elif charge and 'status' in charge: - status = charge['status'] - else: - 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) + amount = Inventory.calculate_payment_amount(items=data['items']) + amount += Inventory.get_shipping_cost(data['shippingOption']['id']) + try: + payment_intent = stripe.PaymentIntent.modify( + id, + amount=amount + ) - return jsonify({'order': order, 'source': source}) + return jsonify({'paymentIntent': payment_intent}) + except Exception as e: + return jsonify(e), 403 @app.route('/webhook', methods=['POST']) @@ -146,9 +133,8 @@ def webhook_received(): # 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']: + if data_object['object'] == 'payment_intent': payment_intent = data_object - order = stripe.Order.retrieve(payment_intent['metadata']['order']) if event_type == 'payment_intent.succeeded': print('🔔 Webhook received! Payment for PaymentIntent ' + @@ -160,61 +146,40 @@ def webhook_received(): # Monitor `source.chargeable` events. if data_object['object'] == 'source' \ and data_object['status'] == 'chargeable' \ - and 'order' in data_object['metadata']: + and 'paymentIntent' in data_object['metadata']: source = data_object - print(f'Webhook received! The source {source["id"]} is chargeable') + print(f'🔔 Webhook received! The source {source["id"]} is chargeable') - # Find the corresponding Order this Source is for by looking in its metadata. - order = Inventory.retrieve_order(source['metadata']['order']) + # Find the corresponding PaymentIntent this Source is for by looking in its metadata. + payment_intent = stripe.PaymentIntent.retrieve( + source['metadata']['paymentIntent']) - # Verify that this Order actually needs to be paid. - order_status = order['metadata']['status'] - if order_status in ['pending', 'paid', 'failed']: - return jsonify({'error': f'Order already has a status of {order_status}'}), 403 + # Verify that this PaymentIntent actually needs to be paid. + if payment_intent['status'] != 'requires_payment_method': + return jsonify({'error': f'PaymentIntent already has a status of {payment_intent["status"]}'}), 403 - # Create a Charge to pay the Order using the Source we just received. - try: - charge = stripe.Charge.create(source=source['id'], amount=order['amount'], currency=order['currency'], - receipt_email=order['email'], idempotency_key=order['id']) - - if charge and charge['status'] == 'succeeded': - status = 'paid' - elif charge: - status = charge['status'] - else: - status = 'failed' - - except stripe.error.CardError: - # This is where you handle declines and errors. - # 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) - - # Monitor `charge.succeeded` events. - if data_object['object'] == 'charge' \ - and data_object['status'] == 'succeeded' \ - and 'order' in data_object['source']['metadata']: - 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']) - - # Monitor `source.failed`, `source.canceled`, and `charge.failed` events. - if data_object['object'] in ['source', 'charge'] and data_object['status'] in ['failed', 'canceled']: - source = data_object['source'] if data_object['source'] else data_object - print(f'Webhook received! Failure for {data_object["id"]}.`') - - if source['metadata']['order']: - Inventory.update_order(properties={'metadata': {'status': 'failed'}}, - order_id=source['metadata']['order']['id']) + # Confirm the PaymentIntent with the chargeable source. + payment_intent.confirm(source=source['id']) + + # Monitor `source.failed` and `source.canceled` events. + if data_object['object'] == 'source' and data_object['status'] in ['failed', 'canceled']: + # Cancel the PaymentIntent. + source = data_object + intent = stripe.PaymentIntent.retrieve( + source['metadata']['paymentIntent']) + intent.cancel() return jsonify({'status': 'success'}) +@app.route('/payment_intents//status', methods=['GET']) +def retrieve_payment_intent_status(id): + payment_intent = stripe.PaymentIntent.retrieve(id) + return jsonify({'paymentIntent': {'status': payment_intent["status"]}}) + + if __name__ == '__main__': load_dotenv(find_dotenv()) stripe.api_key = os.getenv('STRIPE_SECRET_KEY') - stripe.api_version = '2018-02-06' + stripe.api_version = '2019-02-11' app.run() diff --git a/server/python/inventory.py b/server/python/inventory.py index fab43fa0..eb2198ab 100644 --- a/server/python/inventory.py +++ b/server/python/inventory.py @@ -2,61 +2,49 @@ inventory.py Stripe Payments Demo. Created by Adrienne Dreyfus (@adrind). -Simple library to store and interact with orders and products. -These methods are using the Stripe Orders API, but we tried to abstract them -from the main code if you'd like to use your own order management system instead. +Simple library to store and interact with products and SKUs. +These methods are using the Stripe Product API, but we tried to abstract them +from the main code if you'd like to use your own product and order management system instead. """ import stripe import os from functools import reduce -from stripe_types import Order, Product +from stripe_types import Product from dotenv import load_dotenv, find_dotenv load_dotenv(find_dotenv()) stripe.api_key = os.getenv('STRIPE_SECRET_KEY') -stripe.api_version = '2018-02-06' +stripe.api_version = '2019-02-11' class Inventory: @staticmethod - 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: - return stripe.Order.retrieve(order_id) + def calculate_payment_amount(items: list) -> int: + product_list = stripe.Product.list( + limit=3, stripe_version='2018-02-28') + product_list_data = product_list['data'] + total = 0 + for item in items: + sku_id = item['parent'] + product = next( + filter(lambda p: p['skus']['data'][0]['id'] == sku_id, product_list_data)) + total += (product['skus']['data'][0]['price'] * item['quantity']) + return total @staticmethod - def update_order(properties: dict, order: Order = None, order_id: str = None) -> Order: - if not order: - if not order_id: - print('Error when fetching order -- no id or object given') - order = Inventory.retrieve_order(order_id) - - order.update(properties) - return order + def get_shipping_cost(id) -> int: + shipping_cost = {'free': 0, 'express': 500} + return shipping_cost[id] @staticmethod def list_products() -> [Product]: - return stripe.Product.list(limit=3) + return stripe.Product.list(limit=3, stripe_version='2018-02-28') @staticmethod def retrieve_product(product_id) -> Product: - return stripe.Product.retrieve(product_id) + return stripe.Product.retrieve(product_id, stripe_version='2018-02-28') @staticmethod def products_exist(product_list: [Product]) -> bool: diff --git a/server/python/setup.py b/server/python/setup.py index 83bab634..3ec716b3 100644 --- a/server/python/setup.py +++ b/server/python/setup.py @@ -3,7 +3,7 @@ Stripe Payments Demo. Created by Adrienne Dreyfus (@adrind). This is a one-time setup script for your server. It creates a set of fixtures, -namely products and SKUs, that can then used to create orders when completing the +namely products and SKUs, that can then used to caluclate payment amounts when completing the checkout flow in the web interface. """ @@ -14,13 +14,14 @@ load_dotenv(find_dotenv()) stripe.api_key = os.getenv('STRIPE_SECRET_KEY') -stripe.api_version = '2018-02-06' +stripe.api_version = '2019-02-11' def create_data(): try: products = [{'id': 'increment', 'type': 'good', 'name': 'Increment Magazine', 'attributes': ['issue']}, - {'id': 'pins', 'type': 'good', 'name': 'Stripe Pins', 'attributes': ['set']}, + {'id': 'pins', 'type': 'good', + 'name': 'Stripe Pins', 'attributes': ['set']}, {'id': 'shirt', 'type': 'good', 'name': 'Stripe Shirt', 'attributes': ['size', 'gender']}] for product in products: diff --git a/server/python/stripe_types.py b/server/python/stripe_types.py index 3a243c67..689f2028 100644 --- a/server/python/stripe_types.py +++ b/server/python/stripe_types.py @@ -2,9 +2,7 @@ # https://stripe.com/docs/api#sources Source = dict -# https://stripe.com/docs/api#orders -Order = dict # https://stripe.com/docs/api#service_products Product = dict -#https://stripe.com/docs/api#charges +# https://stripe.com/docs/api#charges Charge = dict