This video is available to students only

Stripe Implementation

We want people to be able to actually purchase products, so we'll have to integrate with a payment processor to handle payments with credit cards. Stripe is an example of a payment processor that can help businesses accept one time payments, recurring payments, and distributing payments.

For this section, we'll go through an example of integrating with Stripe's Checkout product, which provides us with a hosted payment form that supports many different methods of payment, including payments for countries outside the US. Stripe can also handle sending receipts, fraud detection, and a lot of other tricky details around payment processing.

In the process of implementing this integration, we will see how to build our own custom Flask extension, receive webhooks, and robustly mock external APIs in tests.

High Level Implementation.

To implement Stripe Checkout, we will redirect users to a page on stripe.com when they click purchase. Here the user will see a payment form. When a user successfully completes their transaction, Stripe will first ping our server to tell us that the transaction has completed successfully, and then will redirect users back to our site.

Signup for Stripe

Sign up for an account on Stripe.com. You can instantly get test environment credentials.

To get your Test API keys, you can navigate to dashboard.stripe.com/test/apikeys which will provide you with a "publishable key" and a "secret key".

Stripe API Creds
Stripe API Creds

We're going to need the stripe Python library, so add that to your requirements.txt as well and run pip install -r requirements.txt.

We'll also want the Stripe command line interface, which you can install at https://stripe.com/docs/stripe-cli.

Configuring our Stripe extension

In order to actually call the Stripe API, we need to set some credentials. First, let's set some defaults in the application configuration within yumroad/config.py

class BaseConfig:
    ...
    STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', 'sk_test_k1')
    STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', 'sk_test_k1')

In our terminal, we can export the actual values into our environment variables.

export STRIPE_WEBHOOK_KEY='...'
export STRIPE_PUBLISHABLE_KEY='...'

Now our Stripe credentials are accessible through the application config attribute. Instead of directly calling the stripe library from within our controllers, we should wrap all of our Stripe specific logic into a separate module.

In order to pass in the application configuration variables, we can follow a similar pattern to all of the extensions we have been using by creating our own custom library following the pattern that Flask extensions use to get application configuration information. By having a single instance that is already configured during the create_app function, we will have a object that we can access from anywhere in our app without worrying about configuration.

Our first step is to create our library in a file as yumroad/payments.py.

import stripe

class Checkout
    def init_app(self, app):
        # Store application config credentials & perform setup here
        stripe.api_key = app.config.get('STRIPE_SECRET_KEY')
        self.publishable_key = app.config.get('STRIPE_PUBLISHABLE_KEY')

    def do_action(self, args):
        # Perform Stripe specific actions
        pass

In yumroad/extensions.py, we'll instantiate the object.

from yumroad.payments import Checkout

checkout = Checkout()

And finally, within yumroad/__init__.py, we will call create_app

from yumroad.extensions import (..., checkout)

def create_app(environment_name='dev'):
    ...
    checkout.init_app(app)

If we were making this a Flask Extension used by other applications, we'd also want to support initialization by passing app during instantiation as that is used by applications that don't use the application factory pattern (create_app) of setting up a Flask app.

class Checkout:
    def __init__(self, app=None):
        if app:
            self.init_app(app)
    ...
# Usage: checkout = Checkout(app)

Redirecting

In order to redirect users to a checkout page, we first need to create a Session on Stripe with the details of a product.

In order to create a session, we need to pass a few things into Stripe.

stripe.checkout.Session.create(
    payment_method_types=['card'],
    client_reference_id="the product id",
    line_items=[{
        'name': "Product Name",
        'description': "A description",
        'amount': 1000, # in cents
        'currency': 'usd',
        'images': ["https://example.com/image.png"],
        'quantity': 1,
    }],
    mode='payment',
    success_url="page to send users after success",
    cancel_url="page to send users if they cancel",
)

Most of the attributes we can get directly as the attributes of the Product model, but we don't have a specific URL to redirect users to after checkout, so we'll make one that will accept the ID of the Stripe session and whether or not the payment was successful as URL parameters.

In the products blueprint (blueprints/products.py), add a new post_checkout route.

from flask import ..., flash

@product_bp.route('/product/<product_id>/post_checkout')
def post_checkout(product_id):
    product = Product.query.get_or_404(product_id)
    purchase_state = request.args.get('status')
    post_purchase_session_id = request.args.get('session_id')
    if purchase_state == 'success' and post_purchase_session_id:
        flash("Thanks for purchasing {}. You will receive an email shortly".format(product.name), 'success')
    elif purchase_state == 'cancel' and post_purchase_session_id:
        flash("There was an error while attempting to purchase this product. Try again", 'danger')
    return redirect(url_for('.details', product_id=product_id))

We can't rely on the success argument here to fulfill our order as anyone could potentially load this page. Instead we'll get a webhook before Stripe redirects our users back. That allows us to safely show a success message.

The success URL that we pass to Stripe can now look something like http://oursite.com/product/1/post_checkout?status=success.

We don't know what the ID of the session will be before we create it and since we want the success_url to have access to the session ID, we need to tell Stripe to include it. If the string {CHECKOUT_SESSION_ID} appears in the redirect URL that we pass into Stripe, their API will replace it with the real session ID before redirecting our users.

To generate that URL, you might think that something like this call to url_for would work (using _external=True to generate the full URL including the hostname).

url_for('product.post_checkout', product_id=product.id,
                              session_id='{CHECKOUT_SESSION_ID}',
                              status='success',
                              _external=True)

However, url_for escapes the value of { and } and produces http://localhost:5000/product/1/post_checkout?session_id=%7BCHECKOUT_SESSION_ID%7D&status=success

That's not quite what we need, so to unescape the URL, we'll import a function from the built in urllib.

from urllib.parse import unquote
unquote(url_for('product.details', product_id=product.id,
                session_id='{CHECKOUT_SESSION_ID}',
                status='success',
                _external=True))

This will produce a redirect URL of http://localhost:5000/product/1/post_checkout?session_id={CHECKOUT_SESSION_ID}&status=success.

Now that we have our redirect URLs, create a method called create_session within payments.py where we'll pass in a Product.

from urllib.parse import unquote
from flask import url_for
import stripe

class Checkout:
    ...
    def create_session(self, product):
        if not product.price_cents:
            return
        success_url = unquote(url_for('product.details', product_id=product.id,
                              session_id='{CHECKOUT_SESSION_ID}',
                              status='success',
                              _external=True))
        failure_url = unquote(url_for('product.details', product_id=product.id,
                              session_id='{CHECKOUT_SESSION_ID}',
                              status='cancel',
                              _external=True))
        session = stripe.checkout.Session.create(
                    payment_method_types=['card'],
                    client_reference_id=product.id,
                    line_items=[{
                        'name': product.name,
                        'description': product.description,
                        'amount': product.price_cents,
                        'currency': 'usd',
                        'images': [product.primary_image_url],
                        'quantity': 1,
                    }],
                    mode='payment',
                    success_url=success_url,
                    cancel_url=failure_url,
                    )
        return session

Start a new discussion. All notification go to the author.