Building a serverless donate form

18 Sep 2019

Learn how to put a donation form on a website - using Netlify & Stripe - fully SCA compliant and no servers to manage!

We will:

tl;dr jump straight to the code here: monty5811/donate-form.

Donation Flow Chart

Step 1: Setup

First of all we need a form where the user can choose how much to donate:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Serverless Donate Form</title>
    <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
    <style>
        html {
            font-family: 'Lucida Grande', Verdana, sans-serif;
        }
    </style>
</head>

<body class="h-screen flex flex-col justify-center">
    <form class="antialiased max-w-xs mx-auto" id="payment-form">
        <input class="block w-64 py-2 px-3 mx-auto mb-4 border-gray-300 border rounded-md" type="number" min="0" placeholder="$50" id="giving-amount" />
        <button class="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-full mx-auto block" id="giving-button">
        Donate
      </button>
    </form>
</body>

</html>

Which looks something like this (we’ve used Tailwind for styling): Donate Form

Step 2: Adding Stripe to the form

Now we need some javascript to handle the interaction with Stripe. We do a few different things to hook our form up to Stripe:

  1. Listen for the user submitting the form
  2. Update the form to a “waiting” state
  3. Get a Stripe Checkout Session ID from our lambda function
  4. Redirect to Stripe’s hosted checkout with the Session ID
  5. Handle any errors

Required changes:

    <!-- rest of content as above -->
    <!-- add jquery & stripe -->
    <script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
    <script src="https://js.stripe.com/v3/"></script>
    <script type="text/javascript">
        var errorText = "Failed. You have not been charged.";

        // look out for submit events on the form
        document.addEventListener("DOMContentLoaded", function(event) {
            var submitButton = document.getElementById("giving-button");

            var stripe = Stripe("<YOUR_STRIPE_PUBLISHABLE_KEY_HERE>");

            var form = document.getElementById("payment-form");
            form.addEventListener("submit", function(event) {
                event.preventDefault();
                const buttonText = submitButton.innerText;
                submitButton.innerText = "Working...";

                var data = {
                    amount: document.getElementById("giving-amount").valueAsNumber * 100,
                };

                // create a stripe session by talking to our netlify function
                $.ajax({
                    type: "POST",
                    url: "/.netlify/functions/get_checkout_session/",
                    data: JSON.stringify(data),
                    success: function(data) {
                        // we got a response from our netlify function:
                        switch (data.status) {
                            case "session-created":
                                // it worked - send the user to checkout:
                                stripe
                                    .redirectToCheckout({
                                        sessionId: data.sessionId
                                    })
                                    .then(function(result) {
                                        submitButton.innerText = result.error.message;
                                    });
                                break;
                            default:
                                submitButton.innerText = errorText;
                        }
                    },
                    dataType: "json"
                });
            });
        });
    </script>
</body>

</html>

Step 3: Add our lambda function

Now we have a form that will take the donation amount & redirect to Stripe’s hosted checkout. However, in order to use a custom “price” with Stripe Checkout we need a server-side component.1

Setting up a whole server seems like overkill for this - a serverless function is ideal.

The serverless function simply takes the amount and gets a Session ID from Stripe. This Session ID is then sent back to the browser where the user is redirected to complete their donation.

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); // get from ENV

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

exports.handler = function(event, context, callback) {
  // some error checking:
  if (event.httpMethod !== "POST" || !event.body) {
    callback(null, {
      statusCode: 400,
      headers,
      body: JSON.stringify({ status: "bad-payload" })
    });
  }
  // Parse the body contents into an object.
  const data = JSON.parse(event.body);

  // Make sure we have all required data. Otherwise, escape.
  if (!data.amount) {
    console.error("Required information is missing.");

    callback(null, {
      statusCode: 400,
      headers,
      body: JSON.stringify({ status: "missing-information" })
    });

    return;
  }
  // actually create the session with Stripe
  // we need to provide a couple of redirect urls:
  stripe.checkout.sessions.create(
    {
      success_url: "https://donate-form-example.netlify.com/success",
      cancel_url: "https://donate-form-example.netlify.com/cancel",
      payment_method_types: ["card"],
      billing_address_collection: "required",
      payment_method_types: ["card"],
      submit_type: "donate",
      line_items: [
        {
          name: "Donation!",
          amount: data.amount,
          currency: "usd",
          quantity: 1
        }
      ]
    },
    function(err, session) {
      // asynchronously called
      if (err !== null) {
        console.log(err);
        callback(null, {
          statusCode: 200,
          headers,
          body: JSON.stringify({ status: "session-create-failed" })
        });
      }
      // woohoo! it worked, send the session id back to the browser:
      callback(null, {
        statusCode: 200,
        headers,
        body: JSON.stringify({
          status: "session-created",
          sessionId: session.id
        })
      });
    }
  );
};

You can see how this is hooked up to Netlify in the full repo: monty5811/donate-form.

Conclusions

That’s it! We have built a donation form where a user can choose how much they would like to donate and we have done it without ever having to worry about running our own server.

You can just as easily do this for a non-static site - you just need to replace the serverless function with a route on your site that will create a Stripe Session & return the id to the frontend.

  1. If you have fixed prices, or fixed donation amounts, then you don’t need any server side components. You can do everything client side. See the Stripe docs for more info