Skip to content

Instantly share code, notes, and snippets.

@jorgeberrizbeitia
Created November 16, 2021 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jorgeberrizbeitia/9f4df47f50bf1c94206381ec7052c571 to your computer and use it in GitHub Desktop.
Save jorgeberrizbeitia/9f4df47f50bf1c94206381ec7052c571 to your computer and use it in GitHub Desktop.
ADAPTIVE - Payments with stripe.js

Stripe installation steps

Initial Setup

  • Create an account. Stripe is free to use and only charge you a percentage of real transaccions done. For testing and implementing on your projects you don't need to pay anything.

  • There are several ways to implement a payment feature, however for this case we will follow the most popular implementation which is the accepting custom payment setup.

  • Go to the stripe documentations and click on Prebuild checkout page, select custom payment flow, then in the top select platform Web, frontend React and Backend Node. Again, this might be different depending on what you are using and want to implement so feel free to use different settings.

  • This documentation page holds by itself all the information needed to setup your payment features, however we will add them in this gist as well.

Backend/Server Setup

  • First of all, install the stripe package in the backend

npm install --save stripe

  • Create a payment intent route. You can copy from the documentation page or the code below. If copying from the documentation remember to change app to router if needed. Also note express needs to be declared in a separate line and not simply used to create the router. The stripe key shown is a stripe testing key
const express = require("express")
const router = express.Router();

const { resolve } = require("path");

// TODO inside the ("") you need to add your real key or the test key that stripe will give you in their documentation.
const stripe = require("stripe")("");

const calculateOrderAmount = items => {
    // ! IMPORTANT Follow below Steps, Look for the real price in the DB and return it
    // 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
    // ! If needed, we can also create our own Purchase Order in the DB, just set the status to pending, as it has not yet being paid. 
    return 1400;
  };

router.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: "eur",
    payment_method_types: [
      "card",
    ],
  });

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

module.exports = router;
  • Remember to require the payment route you just created in index.js
const paymentRoutes = require("./payment.routes");
router.use("/payment", paymentRoutes);

Frontend/Client Setup

  • Install @stripe/react-stripe and @stripe/stripe-js in the frontend

npm install --save @stripe/react-stripe-js @stripe/stripe-js

  • Add a CheckoutForm component, copy the code provided by the stripe documentation (also the code below). There is nothing to change in this component. It will send the payment information from the User to Stripe.
import React, { useEffect, useState } from "react";
import {
  PaymentElement,
  useStripe,
  useElements
} from "@stripe/react-stripe-js";

export default function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const [message, setMessage] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!stripe) {
      return;
    }

    const clientSecret = new URLSearchParams(window.location.search).get(
      "payment_intent_client_secret"
    );

    if (!clientSecret) {
      return;
    }

    stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
      switch (paymentIntent.status) {
        case "succeeded":
          setMessage("Payment succeeded!");
          break;
        case "processing":
          setMessage("Your payment is processing.");
          break;
        case "requires_payment_method":
          setMessage("Your payment was not successful, please try again.");
          break;
        default:
          setMessage("Something went wrong.");
          break;
      }
    });
  }, [stripe]);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js has not yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setIsLoading(true);

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        // ! Make sure to change this to your payment completion page
        // ! We can use this URL to contact the Server so it updates the status of the Purchase Order to be paid.
        return_url: "http://localhost:3000",
      },
    });

    // This point will only be reached if there is an immediate error when
    // confirming the payment. Otherwise, your customer will be redirected to
    // your `return_url`. For some payment methods like iDEAL, your customer will
    // be redirected to an intermediate site first to authorize the payment, then
    // redirected to the `return_url`.
    if (error.type === "card_error" || error.type === "validation_error") {
      setMessage(error.message);
    } else {
      setMessage("An unexpected error occured.");
    }

    setIsLoading(false);
  };

  return (
    <form id="payment-form" onSubmit={handleSubmit}>
      <PaymentElement id="payment-element" />
      <button disabled={isLoading || !stripe || !elements} id="submit">
        <span id="button-text">
          {isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}
        </span>
      </button>
      {/* Show any error or success messages */}
      {message && <div id="payment-message">{message}</div>}
    </form>
  );
}
  • Now that you have created the CheckoutForm component, let's create Payment.jsx, this code also comes from stripe in the form of App.js. This component will create the payment intent when loaded, as well as show the CheckoutForm to the user. This component should only be invoked when the user is ready to pay for the items.

  • 3 changes are needed in this component:

    • Add your publishable stripe key on loadStripe("")
    • Change the url of the .fetch to match your backend endpoint
    • Add the items to be bought in the body of the request
import React, { useState, useEffect } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "./CheckoutForm";
import "./Payment.css";

// Make sure to call loadStripe outside of a component’s render to avoid
// recreating the Stripe object on every render.
// ! Make sure you add your test or real publishable API key as an argument to loadStripe()
const stripePromise = loadStripe("");

export default function Payment() {
  const [clientSecret, setClientSecret] = useState("");

  useEffect(() => {
    // Create PaymentIntent as soon as the page loads
    // ! Remember to replace this URL with your correct backend Endpoint
    fetch("/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      // ! Also, inside body: make sure you pass the reals items to be bought. 
      // ! If it is several, as an array, if one, just added it inside the array. 
      // ! These items might come from previous component, passed as props
      body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
    })
      .then((res) => res.json())
      .then((data) => {
        console.log("payment intent sent correctly")
        setClientSecret(data.clientSecret)
      });
  }, []);

  const appearance = {
    theme: 'stripe',
  };
  const options = {
    clientSecret,
    appearance,
  };

  return (
    <div className="App">
      {clientSecret && (
        <Elements options={options} stripe={stripePromise}>
          <CheckoutForm />
        </Elements>
      )}
    </div>
  );
}
  • Add styles provided in the documentation (same code as below) to your Payment.css
#root {
  display: flex;
  align-items: center;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  display: flex;
  justify-content: center;
  align-content: center;
  height: 100vh;
  width: 100vw;
}

form {
  width: 30vw;
  min-width: 500px;
  align-self: center;
  box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
    0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
  border-radius: 7px;
  padding: 40px;
}


#payment-message {
  color: rgb(105, 115, 134);
  font-size: 16px;
  line-height: 20px;
  padding-top: 12px;
  text-align: center;
}

#payment-element {
  margin-bottom: 24px;
}

/* Buttons and links */
button {
  background: #5469d4;
  font-family: Arial, sans-serif;
  color: #ffffff;
  border-radius: 4px;
  border: 0;
  padding: 12px 16px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  display: block;
  transition: all 0.2s ease;
  box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
  width: 100%;
}

button:hover {
  filter: contrast(115%);
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
  border-radius: 50%;
}

.spinner {
  color: #ffffff;
  font-size: 22px;
  text-indent: -99999px;
  margin: 0px auto;
  position: relative;
  width: 20px;
  height: 20px;
  box-shadow: inset 0 0 0 2px;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}

.spinner:before,
.spinner:after {
  position: absolute;
  content: '';
}

.spinner:before {
  width: 10.4px;
  height: 20.4px;
  background: #5469d4;
  border-radius: 20.4px 0 0 20.4px;
  top: -0.2px;
  left: -0.2px;
  -webkit-transform-origin: 10.4px 10.2px;
  transform-origin: 10.4px 10.2px;
  -webkit-animation: loading 2s infinite ease 1.5s;
  animation: loading 2s infinite ease 1.5s;
}

.spinner:after {
  width: 10.4px;
  height: 10.2px;
  background: #5469d4;
  border-radius: 0 10.2px 10.2px 0;
  top: -0.1px;
  left: 10.2px;
  -webkit-transform-origin: 0px 10.2px;
  transform-origin: 0px 10.2px;
  -webkit-animation: loading 2s infinite ease;
  animation: loading 2s infinite ease;
}

@keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@media only screen and (max-width: 600px) {
  form {
    width: 80vw;
    min-width: initial;
  }
}

Testing!

Once you are done, run both your server and client and use the test cards provided by stripe to test your payment feature. You can input any date (future) and code. You should be able to see the payments in your stripe dashboard (website)

Payment succeeds: 4242 4242 4242 4242
Payment requires authentication: 4000 0025 0000 3155
Payment is declined: 4000 0000 0000 9995

Final adjustments for your app

After you are done with the setup you might want to adapt it to your app by following the following tips:

  • First, you will need to change your stripe secret in the backed and stripe API key in the frontend (this key is publishable as per stripe documentation). Both should come from .env

  • Right now the payment will be a default 14.00 EUR, to adapt this to your code the first thing will be making sure the user is passing the correct item to be bough from your component to the Payment component for the payment Intent inside the body of the request. Also make sure the URL is correctly pointing to the backend endpoint.

  • In the server side, on your payment route there is a calculateOrderAmount function. This currently returns 1400 (14.00 EUR) by default and that is the amount that will be charged from the credit card. You can change this and adapt it to the correct amount of the item. NOTE: Always set the price of items and stripe connection from your DB and never use the one coming from the frontend, as in the frontend this information is vulnerable and can be modified.

  • In the currency of the post request, you can change the currency of the payment.

  • Remember you can test it as much as you like with the stripe test cards provided!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment