Search code examples
reactjsaws-lambdastripe-paymentsgatsbynetlify

How do I write a Lambda function for my Stripe integration?


Background: I am migrating our sales pages to a serverless CDN using Gatsby -> Netlify, and am trying to implement a Stripe customized payment flow because we want to customize our checkout form. I implemented the Stripe Checkout on one of our landing pages here, but it's not what we want. Landing Page

Stripe's documentation is very straightforward, but the documentation assumes one is running a server.

The below code is the server implementation snippet from their documentation.

const express = require("express");
const app = express();
// This is your real test secret API key.
const stripe = require("stripe")("sk_test_51J085aDDSnMNt7v1ZO3n5fxobP6hhEhf1uC2SDmkHGIX8fCnxFiSMrxITKu06ypYUHYAMHVZ1lhc5Fqm7UoVa6dx00XvV5PZzG");

app.use(express.static("."));
app.use(express.json());

const calculateOrderAmount = items => {
  // Replace this constant with a calculation of the order's amount
  // Calculate the order total on the server to prevent
  // people from directly manipulating the amount on the client
  return 1400;
};

app.post("/create-payment-intent", async (req, res) => {
  const { items } = req.body;
  // Create a PaymentIntent with the order amount and currency
  const paymentIntent = await stripe.paymentIntents.create({
    amount: calculateOrderAmount(items),
    currency: "usd"
  });

  res.send({
    clientSecret: paymentIntent.client_secret
  });
});

app.listen(4242, () => console.log('Node server listening on port 4242!'));

Here's Stripe's payment flow.

Stripe's Payment Processing Flow

Challenge: Since I am moving our content to Netlify, which is a serverless CDN, I need to use Lambda functions to interact with the Stripe API instead of the express server implementation in their example.

Netlify describes using serverless functions on their platform here.

I was able to run this silly little thing. But I know how request that output in my React app if I wanted to...

exports.handler = async function(event, context, callback) {
    const { STRIPE_PUBLISHABLE_KEY } = process.env;
    console.log("I am the Hello World Lambda function.")
    return {
        statusCode: 200,
        body: JSON.stringify({message: STRIPE_PUBLISHABLE_KEY})
    };
}

I know I'm displaying my level of skill here, but as my Dad would say: better shame than pain.

Question:/Ask Can someone help me understand how to think about this?

I don't know if this would be a help, but here's my git repository.

Any help would be incredibly appreciated. And if any of you have a few spare cycles and would consider mentoring me on this over a zoom meeting. I will absolutely pay you for your time.

Thanks much!

John


Solution

  • I can definitely help you get started with Serverless / Stripe. There are multiple APIs and libraries that are quite confusing at first, so this will hopefully be useful for others doing the same.

    The Stripe API has a number of ways of doing similar things, but this is a basic flow you can use for accepting a credit card payment securely on a static site using Netlify Lambdas (or any similar solution).

    I'll assume that you're using Stripe's Elements credit card form for react.

    The basic principle is that we need to do actions which use the Stripe secret on the "server" side (i.e. in a lambda) and the rest we can do on the client.

    import React from 'react';
    import ReactDOM from 'react-dom';
    import {loadStripe} from '@stripe/stripe-js';
    import {
      CardElement,
      Elements,
      useStripe,
      useElements,
    } from '@stripe/react-stripe-js';
    
    const CheckoutForm = () => {
      const stripe = useStripe();
      const elements = useElements();
    
      // we will edit this
      const handleSubmit = async (event) => {
        event.preventDefault();
        const {error, paymentMethod} = await stripe.createPaymentMethod({
          type: 'card',
          card: elements.getElement(CardElement),
        });
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <CardElement />
          <button type="submit" disabled={!stripe}>
            Pay
          </button>
        </form>
      );
    };
    
    const stripePromise = loadStripe('pk_test_abc123');
    
    const App = () => (
      <Elements stripe={stripePromise}>
        <CheckoutForm />
      </Elements>
    );
    
    ReactDOM.render(<App />, document.body);

    Lambda

    Change the body of your lambda as follows:

      const stripe = require("stripe")(process.env.STRIPE_SECRET);
      const { amount, currency = "gbp" } = JSON.parse(event.body);
    
      try {
        const paymentIntent = await stripe.paymentIntents.create({
          amount,
          currency,
        });
        return {
          statusCode: 200, // http status code
          body: JSON.stringify({
            paymentIntent
          }),
        };
      } catch (e) {
        // handle errors
      }
    

    Here we initialise the stripe library with your stripe secret key, we create a minimal paymentIntent using just amount and currency, and then we return it to the client.

    Frontend

    Assuming you've got Stripe and Elements loaded as in the linked example and have a basic credit card form, you'll just need to edit the submit handler.

    First call your paymentIntents lambda:

    // handleSubmit()
    const intent = await fetch("/.netlify/functions/payment-intent", {
      method: "POST",
      body: JSON.stringify({
          amount: 500,
        }),
      });
    const { paymentIntent } = await intent.json();
    

    Now you can use the paymentIntent to confirm the card details the user has entered. (The paymentIntent contains a client_secret which is required to confirm a payment.)

    // if you've followed the example, this is already done
    import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
    const stripe = useStripe();
    const elements = useElements();
    
    // now back to handleSubmit()
    await stripe.confirmCardPayment(paymentIntent.client_secret, {
      payment_method: {
        card: elements.getElement(CardElement),
        billing_details: {
          email: profile.email,
        },
      },
    });
    

    Here we use the javascript API's confirmCardPayment method to confirm the payment using the client_secret from the paymentIntent we just set up. The credit card details are taken care of by the elements.getElement method. We also provide the user's email so their receipt can be sent to them.

    Next steps

    This is a very basic serverless implementation of accepting payments via stripe. You can look at the different APIs to set up customers and use their IDs for payments, set up subscriptions, and much more.

    Naturally you will also want to handle the many and various errors that I've ignored.

    Versions used are latest versions of all stripe libraries listed as at 2021-06-12