Accepting Payments with Stripe

As a major player in the online payments market, Stripe offers buyers worldwide a unique, hassle-free paying experience. Support for more than 150 currencies, various payment methods and models, and modern security levels speak a clear language. Sellers benefit from a powerful user interface – the dashboard –, web developers from versatile APIs and interfaces.

In this tutorial, we are going to add a StripeCheckoutWidget to the Scrivito Example App for letting visitors pay for goods or services using their credit or debit card. For having Stripe actually charge the customer, we’ll pass the card and product data to a Lambda function in which we will be talking with Stripe to have the amount in question transferred to our account.

Let us first spend a short moment on the general procedure when handling transactions online. All processes involving sensitive personal data, be it credit card numbers or email addresses, require a backend to hide this data from the client side and hence make it inaccessible to villains. Of course, in the serverless age, any secure computing service able to make outbound requests will do as a backend.

As we are focusing on the general working principle of online payment handling with Stripe, our StripeCheckoutWidget is limited to one-time payments and doesn’t include cart functionality, invoice generation and the like. However, it should be best suited as a starting point and will cover small e-commerce use cases. There are numerous React-based shopping carts out there, and using Stripe’s dashboard for concluding purchases in such a setting is not too bad an idea.

Preparation

If you haven’t already set up your local Example App, just follow our Getting Started with Scrivito guide. For the backend part, an AWS Lambda function will be used. To spare you the effort of creating an AWS account and defining an API gateway for the Lambda, we’ll use Netlify Functions in combination with Netlify Lambda CLI, a great way to develop and test your remote functionality locally.

Of course, you will need an account at Stripe – it’s free. They’ll provide you with a pair of API keys (public and private) for testing purposes. By the way, in test mode, Stripe lets you use their APIs over HTTP so you don’t have to set up SSL for localhost.

Setting up Netlify Lambda

Before you can set up Netlify Lambda for your local Example App, make sure that you’ve claimed your Scrivito site on Netlify. This can be done by clicking the corresponding button on the “Deployment” tab in your Scrivito dashboard.

Then, in your Example App’s project directory, create the source directory for the Lambda function and install the Netlify Lambda command line interface:

Terminal
Copy
mkdir src/lambda
npm install netlify-lambda -D

To make Netlify aware of the Lambda, add the functions key to the “netlify.toml” file so that it reads:

netlify.toml
Copy
[build]
  command = "npm run build"
  publish = "build"
  functions = "lambda"

Next, in your project’s “package.json” file, add commands for building the Lambda and starting the local server, and adapt the overall build command so that it covers the Lambda:

package.json
Copy
  "scripts": {
    "build": "webpack -p && npm run lambda-build",
    "lambda-serve": "netlify-lambda serve src/lambda",
    "lambda-build": "netlify-lambda build src/lambda",
    …
  }

The local Lambda server will listen on port 9000; accessing it needs to be permitted in the CSP. So, in your project’s “webpack.config.js” file, add a corresponding directive in the function that builds the CSP header:

webpack.config.js
Copy
function devServerCspHeader() {
  …
  directives["default-src"].push("localhost:9000");
  …
}

Finally, add your Stripe keys as well as the Lambda’s endpoint to your environment:

.env
Copy
STRIPE_PUBLISHABLE_KEY="pk_test_…"
STRIPE_SECRET_KEY="sk_test_…"
LAMBDA_ENDPOINT="http://localhost:9000/purchase"

Keep in mind that, before deploying, your stripe keys as well as the Lambda endpoint need to be made known in your site settings on Netlify.

Creating the StripeCheckoutWidget

As we are offering goods or services, the first thing to do is to specify what we are selling. We will let the editor who adds this widget to a page provide the product data via some attributes in the widget’s class definition.

Defining the widget class

src/Widgets/StripeCheckoutWidget/StripeCheckoutWidgetClass.js
Copy
import * as Scrivito from "scrivito";

const StripeCheckoutWidget = Scrivito.provideWidgetClass(
  "StripeCheckoutWidget",
  {
    attributes: {
      name: "string",
      price: "float",
      description: "string",
      details: "widgetlist",
    },
  }
);

export default StripeCheckoutWidget;

As you can see, our product instances will have a name, a price, and a description. These are the facts we are going to hand over to Stripe in the checkout process. Additionally, for allowing editors to refine the product display, a widgetlist attribute, details, is made available so adding images, feature descriptions, etc., should be easy enough.

Defining the widget’s editing configuration

A widget’s editing configuration specifies what editors can change using the properties view of such a widget’s instance. Additionally, the configuration lets you initialize the attributes of new widgets. In our case, we’d like to make the product price and description editable, and also add some placeholder content to it:

src/Widgets/StripeCheckoutWidget/StripeCheckoutWidgetEditingConfig.js
Copy
import * as Scrivito from "scrivito";
import ImageWidget from "../ImageWidget/ImageWidgetClass";

Scrivito.provideEditingConfig("StripeCheckoutWidget", {
  title: "StripeCheckout",
  attributes: {
    price: {
      title: "Product price",
      description: "Price in USD",
    },
    description: {
      title: "Product description",
      description: "Benefits, use cases, etc.",
    },
  },
  initialContent: {
    name: "Product name",
    price: 99.95,
    description: "Get it now!",
    details: [new ImageWidget()],
  },
  properties: ["price", "description"],
});

Anticipating what a new StripeCheckoutWidget would look like when rendered by its component (which we don’t have yet), we would want it to include the preset content, arranged above and below the image widget in the middle:

Providing the widget component

Now, let’s make it happen and render our widget! We will be using a couple of packages, first and foremost react-stripe-checkout, the component that renders an overlay for collecting the payment details. In your project directory, execute:

Terminal
Copy
npm install react-stripe-checkout axios react-toastify

We will use axios later to post the visitor’s payment details to our Lambda backend. In case you haven’t heard of react-toastify yet, it’s a versatile tool for displaying splash messages in your app. Here, it serves to notify us about the result of the checkout process.

Essentially, our StripeCheckoutWidget component produces what you’ve seen above: it presents the details of the product we are offering. The StripeCheckout component we are using in this widget represents the interface enabling the visitor to purchase the product.

src/Widgets/StripeCheckoutWidget/StripeCheckoutWidgetComponent.js
Copy
import * as React from "react";
import * as Scrivito from "scrivito";
import StripeCheckout from "react-stripe-checkout";
import axios from "axios";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

toast.configure();

class StripeCheckoutWidgetComponent extends React.Component {
  constructor(props) {
    super(props);

    this.handleToken = this.handleToken.bind(this);
  }

  async handleToken(token, addresses) {
    toast("Billing details accepted!", { type: "success" });
  }

  render() {
    const widget = this.props.widget;
    return (
      <div className="container">
        <Scrivito.ContentTag tag="h2" content={widget} attribute="name" />
        <Scrivito.ContentTag content={widget} attribute="details" />
        <div>
          <p className="strong">
            Only US$ {widget.get("price")} –{" "}
            <Scrivito.ContentTag
              tag="span"
              content={widget}
              attribute="description"
            />
          </p>
        </div>
        <StripeCheckout
          stripeKey="pk_test_abcdeFGHIJ12345klmno67890"
          amount={widget.get("price") * 100}
          currency="USD"
          token={this.handleToken}
          name={widget.get("name")}
          billingAddress
          shippingAddress
        >
          <button type="button" className="btn btn-outline-primary">
            Buy now
          </button>
        </StripeCheckout>
      </div>
    );
  }
}

Scrivito.provideComponent(
  "StripeCheckoutWidget",
  StripeCheckoutWidgetComponent
);

After rendering the widget’s attributes, the StripeCheckoutWidgetComponent renders the StripeCheckout component, passing to it the relevant data as props: stripeKey, amount, currency, and token. stripeKey is our publishable test key, amount the total to charge (Stripe wants it in cents), the currency to use, and a callback for the token. StripeCheckout executes this callback and passes the token to it after having validated the payment details. We can then use this token in our Lambda backend to trigger the actual payment. Our above callback, handleToken, simply displays a notification; we’ll elaborate on that in a minute.

See the react-stripe-checkout documentation for other props available for configuring the checkout procedure.

Above, we’ve added a button element as a child to the StripeCheckout component so that we can style the trigger to our liking. As a default, the component renders its own “Pay With Card” button.

Providing the token handling callback

After a customer has entered their payment details and finally clicked the “Pay” button, the handleToken callback is executed. StripeCheckout passes two objects to it, token and addresses, we can now use as desired and forward to our backend. We are not using addresses here because the token object already includes the billing address. Instead, we are passing in the widget’s product price and name; the latter enables us to identify the product in our Stripe dashboard.

src/Widgets/StripeCheckoutWidget/StripeCheckoutWidgetComponent.js
Copy
  async handleToken(token, addresses) {
    const widget = this.props.widget;
    const product = {
      name: widget.get("name"),
      price: widget.get("price"),
    };
    const response = await axios.post("http://localhost:9000/purchase", {
      token,
      product,
    });
    const status = response.data;
    if (status === "success") {
      toast("Thank you for your purchase! Check email for details!", {
        type: "success",
      });
    } else {
      toast("Something went wrong.", { type: "error" });
    }
  }

We are posting the data to our local Lambda backend, which can then request Stripe to execute the transaction. In the above callback, we are displaying a success or failure notification, but you should, of course, handle these statuses in a more explicit and customer-friendly way.

Providing the Lambda backend

Our Node.js-based Lambda function requires dotenv (already included in the Example App), the uuid package and, of course, stripe:

Terminal
Copy
npm install uuid
npm install stripe --save

Here’s the Lambda; we named the file “purchase.js”:

src/lambda/purchase.js
Copy
require("dotenv").config();
const uuid = require("uuid/v4");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

const headers = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "Content-Type",
};

exports.handler = async function(event) {
  let error;
  let message;
  try {
    const { token, product } = JSON.parse(event.body);
    const customer = await stripe.customers.create({
      email: token.email,
      source: token.id,
    });

    const idempotency_key = uuid();
    const charge = await stripe.charges.create(
      {
        amount: product.price * 100,
        currency: "USD",
        customer: customer.id,
        receipt_email: token.email,
        description: `Purchased the ${product.name}`,
        shipping: {
          name: token.card.name,
          address: {
            line1: token.card.address_line1,
            line2: token.card.address_line2,
            city: token.card.address_city,
            country: token.card.address_country,
            postal_code: token.card.address_zip,
          },
        },
      },
      {
        idempotency_key,
      }
    );
    message = "success";
  } catch (error) {
    console.error("Error:", error);
    message = "Error: " + error;
  }
  return {
    statusCode: 200,
    headers: headers,
    body: JSON.stringify(message),
  };
};

Our Lambda function first creates a Stripe customer object using parts of the incoming token. Then, it attempts to create a charge for this customer, based, again, on the token but also on the product. For details about other items that can be provided with charges, see the documentation.

The idempotency key that needs to be passed in ensures that the charge is not created several times in case you need to repeat the request due to a network error. See the documentation on error handling for further information.

Try it out!

Now that everything is in place, start your Lambda server and your Example App (in separate terminal windows):

Terminal
Copy
npm run lambda-serve
# New terminal window
npm start

Congratulations! You can now start accepting payments via Stripe on your website!

Note that we’ve used netlify-lambda here to serve our Lambda. Netlify recommends using Netlify Dev instead, however, at the time of writing, this development server is still beta.

Final thoughts

You may want to make sure in your Lambda function that incoming requests are POST requests (event.httpMethod === "POST") and originate from your domain.

As sometimes, there’s potential for improvements. For example, you could make the currency (and other pieces of payment-related data) a property of the widget so that editors can specify it, and include it in the product data when calling the Lambda function.

After deployment and when running your site productively, don’t forget to use your real-life Stripe keys!