Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

Implementing 3D Secure in Stripe

16 min read 4675

We live in a world where we interact with a lot of online services and make payments to these services through online payment gateways. And we, as developers, have the responsibility to integrate these payment gateways in such a way that it’s secure for both the user and the party collecting the payment.

In this article we will cover how to implement 3D Secure protection on accepting payments online using Stripe.

What is 3D Secure?

3D Secure is a way for Stripe to authenticate a user before actually processing the payment. When a user enters his card details, he is prompted with a popup or a redirect to authenticate the payment.

It’s usually verifying identity via OTP, but it might depend upon the bank issuing the card. In some countries 3D Secure is not necessary, but in countries like India, 3D Secure is required.

You can set your radar rules in your Stripe account to require 3D Secure authentication, but it’s of no use if you don’t have the code in your payment form to make a 3D Secure popup work.

In this article we will create a simple donation web application made using NodeJS, React, and of course, Stripe. We will cover the following topics:

  • Setting up Stripe and getting the API keys
  • Setting up a NodeJS back end and React front end
  • Creating a checkout form in the front end
  • Processing payments the usual way
  • Using 3D Secure as a fallback if authentication is required
  • Confirming the payment
  • Adding recurring payments (subscriptions)
  • Testing our integration

What will you require?

  • A code editor – I prefer VSCode, but you can use any code editor of your choice
  • NodeJS installed
  • A Stripe account
  • Basic knowledge of command line
  • Basic knowledge of ReactJS and NodeJS

Let’s get started!

First, we will work on the back end. I prefer an “API first approach” which means you first create an API and then work on the rest of the front end.

We will create our back end using NodeJS, Express, and a Stripe package to fetch the payment related content.

Initiating the back end

Let’s create our back end. To do that, open the terminal/command prompt and type the following command to initiate a NodeJS project in your desired folder:

npm init -y

Running this command will generate a package.json file in the folder.

We made a custom demo for .
No really. Click here to check it out.

Now open VSCode in the folder using the following command so that we can start editing:

code .

Now that VSCode is opened, you can use the integrated terminal, which will make your life easier. Just hit Ctrl + J on Windows or Command + J on Mac to open the terminal in VSCode.

Let’s install few packages that will help us with the project further. Type the following command in the terminal and we will see what these packages do:

npm install express cors stripe dotenv

These are the packages being installed:

  • Express is used to easily create HTTP servers
  • CORS helps us get rid of cross origin errors in our client applications
  • Stripe is the actual connection to Stripe. We can fetch payment details and create payments using this package
  • Dotenv helps us enable environment variables to store sensitive data

Adding a Stripe secret key to environment variables

Before moving further with this payment system, let’s set up the Stripe secret key in the environment variable.

All secret API keys and credentials must be stored in environment variables so that the data doesn’t get stolen if the actual code is stolen.

To get your Stripe secret key, open your Stripe dashboard and you will see a side menu similar to the picture below:

Screenshot of Stripe dashboard

Now, click on Developers, and then API Keys. There you should see your Stripe publishable and secret key.

For now, we need the secret key. Please note that you should not share your secret key with anyone. Sharing your secret key will give others access to your Stripe account.

On the other hand, the publishable key is the one we use in the front end and it doesn’t matter if anyone gains access to it, because it’s meant to be public.

Now, copy your Stripe secret key and go to VSCode, create a new file named .env, and paste the key in the following format:

STRIPE_SECRET_KEY=(secret key here)

The .env file is used to store environment variables. The dotenv package will search for this file to load the environment variables. Now that the .env file is done, we don’t need to touch environment variables again in this tutorial.

Installing Nodemon

While following the tutorial, you might need to restart the server several times. To avoid that, we can install a global package called nodemon which will automatically restart our server whenever we save a file. You can read more about Nodemon here.

Type the following command in the terminal:

npm install -g nodemon

Use sudo if required because Nodemon is supposed to be installed globally, so it will require root permissions.

Setting up an Express server

Let’s create the file that will run our server. We can name it index.js because it’s specified as main by default in the package.json file. You can change the name if you want, but we will stick with index.js in this tutorial.

Let’s start off by creating an Express server and a simple route:

const express = require("express");
const app = express();
const PORT = process.env.PORT || 5000;
const cors = require("cors");

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/", (req, res) => res.json({ status: 200, message: "API Works" }));

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

This creates a simple Express server with one home route, which simply returns a JSON saying the API works.

Here, we set the port to process.env.PORT || 5000 because if you decide to deploy this server to a service like Heroku, they will host it on their own ports which are stored in their environment variables, so we let them decide the port. If process.env.PORT is undefined, the app is running locally and port 5000 will be used.

We use the cors package as an Express middleware so that client applications can interact with our server properly without any cross-origin errors. You can configure the cors package according to your needs, but for this tutorial, we will just allow any traffic.

In the middleware section, we also allow JSON and url-encoded data through a request body and Express will parse it for us automatically.

Now if you go to Postman or any other HTTP client and perform a GET request on http://localhost:5000, you will get the following JSON response:

{
  "status": 200,
  "message": "API Works"
}

If you see this message, your Express server is set up properly. Now let’s move on to the next step.

Setting up dotenv

Now let’s configure the dotenv package so that it can properly recognize our environment variables from .env file. Write the following code at the top:

require("dotenv").config();

Initializing Stripe

Now let’s set up our connection to Stripe. Previously in the tutorial, we had installed a package named stripe that will help us communicate with Stripe. But first, we need to provide it our Stripe secret key so that it can interact with our Stripe account.

Include this snippet on top of the file we created previously:

const Stripe = require("stripe");
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);

Earlier we dealt with environment variables, and here’s where we use the STRIPE_SECRET_KEY we stored. Now Stripe recognizes your account and we can interact with Stripe further.

The entire code till should now display the following:

require("dotenv").config();
const express = require("express");
const app = express();
const PORT = process.env.PORT || 5000;
const cors = require("cors");
const Stripe = require("stripe");
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/", (req, res) => res.json({ status: 200, message: "API Works" }));

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Collecting data for payment

Let’s think of what data we need to collect from the user to initiate the payment. We will keep things simple for the sake of this tutorial:

  • Email address
  • Payment amount
  • paymentMethod, an ID generated by Stripe on the front end that will represent a specific card
  • Subscription, either onetime or monthly. We will set up recurring payments if the subscription is set to monthly

As we are “creating” a payment, we will use a POST request. Another reason to use POST request is that the data we send to the server is not shown in the URL itself, unlike a GET request. Plus, GET requests are directly accessible through the browser and that’s not something that we want.

So let’s create a POST request listener and collect data:

app.post("/donate", async (req, res) => {
  try {
    let { email, amount, paymentMethod, subscription } = req.body;
    if (!email || !amount || !paymentMethod || !subscription) 
      return res.status(400).json({ status: 400, message: "All fields are required!" });
    amount = parseInt(amount);

    if (subscription === "onetime") {
      // One time payment code here
    }

    if (subscription === "monthly") {
      // Recurring payment code here
    }

    res.status(400).json({ status: 400, message: "Invalid type" });
  } catch(err) {
    console.error(err);
    res.status(500).json({ status: 200, message: "Internal server error" });
  }
});

In the above code we are doing the following:

  • Setting up a POST listener on the /donate route, of course
  • Collecting the email, amount, and paymentMethod from the user
  • Validating the fields, so that if any of the fields are missing, an error message will be sent
  • Sometimes the client application might send amount as a string, in which case we are converting the amount to an integer value using the parseInt() function

First, we will deal with one-time payments.

Attempting simple HTTP Payment

We use 3D secure only if it’s required, or as per our radar rules in the Stripe dashboard. We must attempt an HTTP payment before we move on to 3D Secure because some cards do not support 3D Secure.

Now it’s time to contact Stripe:

const paymentIntent = await stripe.paymentIntents.create({
  amount: Math.round(amount * 100),
  currency: "INR",
  receipt_email: email,
  description: "Payment for donation",
  payment_method: paymentMethod,
  confirm: true
});

This initiates the payment right away. The confirm field tells Stripe to confirm the payment as soon as it receives it. If you do not specify confirm, it won’t charge the user, and you would need to confirm the order manually before making another request to Stripe.

In the amount field, you specify the secondary currency unit (e.g., USD is cents and INR is paisa). Math.round() is used here to remove any decimals, because Stripe doesn’t like decimal numbers.

Specify the currency according to your Stripe account location. For me it’s, India so I use INR in currency.

Once the payment is completed, the receipt will be sent to email specified. In this case, we mention the email we collected from the user.

Now let’s check if this simple HTTP payment was successful. To do that we can check the status property of paymentIntent:

if (paymentIntent.status === "succeeded") {
  // Payment successful!
  return res.json({
    status: 200,
    message: "Payment Successful!",
    id: paymentIntent.id
  });
}
>

That’s all for a simple HTTP payment. Here, paymentIntent.id can be used as a payment ID. And we use return to stop further execution immediately so that there are no unexpected errors.

However, if the status is not succeeded but requires_action, it means 3D Secure is required. So here’s how we will deal with 3D Secure:

  • We will get a client_secret for the payment intent
  • We will send the client_secret to the front end
  • The front end will use the client_secret to authenticate using 3D secure
  • We will make a route in the back end to check the payment status again

Getting client_secret and sending it to the front end

Let’s check if the payment intent we created requires 3D secure, and then send the client secret:

if (paymentIntent.status === "requires_action") {
  return res.json({ 
    status: 200,
    message: "3D secure required",
    actionRequired: true,
    clientSecret: paymentIntent.client_secret
  });
}

This way, we send the client secret to the front end. We will deal with the front end later in this article once we are done with the back end portion.

And finally, if the status is neither succeeded nor requires_action, we will inform the user that the payment has failed. We used return in previous cases so we don’t need to use else:

return res.status(400).json({
  status: 400,
  message: "Payment failed!"
});

Handling recurring payments

We do not use payment intents directly in recurring payments. The process to create a recurring payment is a bit different:

  • First we create a price, which will be our donation amount
  • Next we create a Stripe customer with the user’s email
  • Then we create a subscription and charge the customer with the price. Stripe will send an email to the customer each month for the payment if authentication is required
  • Finally, we make the user pay the first invoice on our website itself

Previously we created an if statement for the monthly subscription type. All the recurring payment code goes in there.

Creating a price

Let’s move on to the first step, creating a price:

const price = await stripe.prices.create({
  unit_amount: Math.round(amount * 100),
  recurring: { interval: "month" },
  currency: "INR",
  product_data: {
    name: "Recurring donation"
  }
});

Here the unit_amount is the actual amount – we already discussed how this is sent to Stripe.

We also provide recurring with an interval. In this case we set it to month. The product_data object contains some information about the product itself. In this case it’s just a donation, so we just specify it.

Creating a customer

Now, let’s create the customer:

const customer = await stripe.customers.create({
  email, 
  description: "Donation customer",
  payment_method: paymentMethod,
  invoice_settings: {
    default_payment_method: paymentMethod
  }
});

Here we specify the paymentMethod so that we can charge the customer right away when needed without any complications.

Creating a subscription

This is where the customer is actually charged. When initiating a subscription, an invoice is generated which can be paid by the user, but we will make the user pay the invoice right away to start the subscription.

We can get the paymentIntent from the subscription and then we can do checks as we did before:

const subscribe = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: price.id }],
  expand: ["latest_invoice.payment_intent"]
});

We are passing in the ID of customer and price, linking everything together. Also, to get access to the paymentIntent of the latest invoice, we use the expand property.

As we try to create a subscription, Stripe already attempts an HTTP-based payment. Now we need to take care of 3D secure payments the same way we did before:

if (
  subscribe.latest_invoice.payment_intent.status === "requires_action"
) {
  // proceed to 3ds
  return res.status(200).json({
    status: 200,
    message: "3D Secure required",
    actionRequired: true,
    clientSecret: subscribe.latest_invoice.payment_intent.client_secret,
    id: subscribe.latest_invoice.payment_intent.id,
  });
}
if (subscribe.latest_invoice.payment_intent.status === "succeeded") {
  return res.json({
    status: 200,
    message: "Payment successful!",
  });
}
return res.status(400).json({ status: 400, message: "Payment failed!" });

It’s the same method we used for the one-time payments. We are done with the payment route in the backend.

There’s one more route to cover – the checking route. After authenticating on the front end, we need a route to check and verify the status with the back end:

app.get("/check/:id", async (req, res) => {
  try {
    const id = req.params.id;
    const paymentIntent = await stripe.paymentIntents.retrieve(id);
    if (paymentIntent?.status === "succeeded") {
      return res.json({
        status: 200,
        message: "Payment successful!",
        id,
      });
    }
    res
      .status(400)
      .json({
        status: 200,
        message: "Payment failed! Please try again later.",
      });
  } catch (err) {
    console.error(err);
    res.status(500).json({ status: 500, message: "Internal server error" });
  }
});

This time we use a GET request and check if the payment is actually complete. This can be done if you don’t want to use a webhook and want to provide a virtual service to the user right away.

This would be the place for your app to know that a payment is successful and the user is good to go. But in this case, this is a donation website and we don’t need to do anything special here.

Complete index.js code

Your index.js file should now look like this:

require("dotenv").config();
const express = require("express");
const app = express();
const PORT = process.env.PORT || 5000;
const cors = require("cors");
const Stripe = require("stripe");
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/", (req, res) => res.json({ status: 200, message: "API Works" }));

app.post("/donate", async (req, res) => {
  try {
    let { email, amount, paymentMethod, subscription } = req.body;
    if (!email || !amount || !paymentMethod || !subscription) 
      return res.status(400).json({ status: 400, message: "All fields are required!" });
    amount = parseInt(amount);

    if (subscription === "onetime") {
      // One time payment code here
      const paymentIntent = await stripe.paymentIntents.create({
        amount: Math.round(amount * 100),
        currency: "INR",
        receipt_email: email,
        description: "Payment for donation",
        payment_method: paymentMethod,
        confirm: true
      });
      if (paymentIntent.status === "succeeded") {
        // Payment successful!
        return res.json({
          status: 200,
          message: "Payment Successful!",
          id: paymentIntent.id
        });
      }
      if (paymentIntent.status === "requires_action") {
        return res.json({ 
          status: 200,
          message: "3D secure required",
          actionRequired: true,
          clientSecret: paymentIntent.client_secret
        });
      }
      return res.status(400).json({
        status: 400,
        message: "Payment failed!"
      });
    }

    if (subscription === "monthly") {
      // Recurring payment code here
      const price = await stripe.prices.create({
        unit_amount: Math.round(amount * 100),
        recurring: { interval: "month" },
        currency: "INR",
        product_data: {
          name: "Recurring donation"
        }
      });

      const customer = await stripe.customers.create({
        email, 
        description: "Donation customer",
        payment_method: paymentMethod,
        invoice_settings: {
          default_payment_method: paymentMethod
        }
      });

      const subscribe = await stripe.subscriptions.create({
        customer: customer.id,
        items: [{ price: price.id }],
        expand: ["latest_invoice.payment_intent"]
      });

      if (
        subscribe.latest_invoice.payment_intent.status === "requires_action"
      ) {
        // proceed to 3ds
        return res.status(200).json({
          status: 200,
          message: "3D Secure required",
          actionRequired: true,
          clientSecret: subscribe.latest_invoice.payment_intent.client_secret,
          id: subscribe.latest_invoice.payment_intent.id,
        });
      }
      if (subscribe.latest_invoice.payment_intent.status === "succeeded") {
        return res.json({
          status: 200,
          message: "Payment successful!",
        });
      }
      return res.status(400).json({ status: 400, message: "Payment failed!" });
    }

    res.status(400).json({ status: 400, message: "Invalid type" });
  } catch(err) {
    console.error(err);
    res.status(500).json({ status: 200, message: "Internal server error" });
  }
});

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Creating the front end

Now let’s move on to the front end to learn how to trigger 3D Secure authentication and how to initiate payments.

We won’t do any fancy styling in the front end. Let’s keep it simple and focus on the payment side of things.

We will use React in the front end. Create a new folder called frontend, open terminal in that folder, and type the following command:

npx create-react-app .

The . specifies that we are creating a React app in the current folder itself.

Now lets install some packages that we will need while making this app:

npm install axios @stripe/react-stripe-js @stripe/stripe-js
  • axios is a library to make HTTP requests easily without messing with the fetch API
  • Both of the Stripe packages are useful for creating Stripe Elements and communicating with Stripe

Now open VSCode in the React app using the following command:

code .

Once in the integrated terminal, type the following command to start up the React App:

npm start

A new browser tab should open and you should see the following screen:

Screenshot of blank React app

If you see this screen, you’ve successfully initiated a React application. Now let’s do some cleanup.

Delete the following files in src which we don’t need:

  • App.test.js
  • setupTests.js
  • logo.svg

Once you delete these files, you’ll see an error pop up. That’s because we broke some things.

Go to App.js and remove the logo import on the top and the content under the first div. Remove everything in App.css.

Your App.js should look something like this:

import "./App.css";
function App() {
  return <div className="app"></div>;
}
export default App;

Next, let’s create a new component named Checkout. Create two files in src: Checkout.js and Checkout.css.

Since we are not focusing on styling in this tutorial, I am providing the contents of a CSS file but we will not go through what’s actually happening here in Checkout.css:

.checkout {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  width: 100%;
}
.checkout__container {
  background-color: #f5f5f5;
  padding: 20px;
  width: 25%;
  display: flex;
  flex-direction: column;
}
.checkout__textBox {
  padding: 10px;
  font-size: 18px;
  margin-bottom: 10px;
}
.checkout__radio {
  margin-bottom: 10px;
}
.checkout__btn {
  margin-top: 10px;
  padding: 10px;
  font-size: 18px;
  border: none;
  background-color: #0984e3;
  color: white;
}

Now, open Checkout.js and create a React functional component:

import React from "react";
function Checkout() {
  return <div className="checkout"></div>;
}
export default Checkout;

Now let’s import this component and use it in App.js:

import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import "./App.css";
import Checkout from "./Checkout";
const stripePromise = loadStripe("(publishable key here)");
function App() {
  return (
    <div className="app">
      <Elements stripe={stripePromise}>
        <Checkout />
      </Elements>
    </div>
  );
}
export default App;

We wrap our Checkout component inside the Elements provided to us by Stripe. This component acts as a wrapper for all the Stripe elements and services we need.

We use the loadStripe() function and pass in the publishable key, and then pass in the stripePromise as stripe in the Elements component as props.

Now let’s go to Checkout.js and make the basic layout of our form:

import { CardElement } from "@stripe/react-stripe-js";
import React, { useState } from "react";
function Checkout() {
  const [email, setEmail] = useState("");
  const [amount, setAmount] = useState("");
  const [subscription, setSubscription] = useState("onetime");
  const handleSubmit = async (e) => {
    try {
      e.preventDefault();
    } catch (error) {
      console.error(error);
      alert("Payment failed!");
    }
  };
  return (
    <div className="checkout">
      <form className="checkout__container" onSubmit={handleSubmit}>
        <input
          type="email"
          value={email}
          className="checkout__textBox"
          onChange={(e) => setEmail(e.target.value)}
          placeholder="E-mail Address"
        />
        <input
          type="number"
          value={amount}
          className="checkout__textBox"
          onChange={(e) => setAmount(e.target.value)}
          placeholder="Amount"
        />
        <div className="checkout__radio">
          <input
            type="radio"
            onChange={(e) => setSubscription("onetime")}
            checked={subscription === "onetime"}
          />
          Onetime
        </div>
        <div className="checkout__radio">
          <input
            type="radio"
            onChange={(e) => setSubscription("monthly")}
            checked={subscription === "monthly"}
          />
          Monthly
        </div>
        <CardElement
          options={{
            style: {
              base: {
                fontSize: "16px",
                color: "#424770",
                "::placeholder": {
                  color: "#aab7c4",
                },
              },
              invalid: {
                color: "#9e2146",
              },
            },
          }}
        />
        <button className="checkout__btn" type="submit">
          Donate
        </button>
      </form>
    </div>
  );
}
export default Checkout;

We created a basic form asking for email and desired amount. The CardElement component is used to show a little element for the user to enter card details.

Now let’s handle the event when the user submits the form:

const handleSubmit = async (e) => {
  try {
    e.preventDefault();
    if (!elements || !stripe) return;
    const cardElement = elements.getElement(CardElement);
    const { error, paymentMethod } = await stripe.createPaymentMethod({
      type: "card",
      card: cardElement,
    });
  } catch (error) {
    console.error(error);
    alert("Payment failed!");
  }
};

First we will check if Stripe and Elements are loaded. If not, then the form will do nothing. How can you process a payment without Stripe being loaded?

Then we get to the cardElement. The reason it’s too easy to find is because there can be only one CardElement in the entire form.

Next, we create a paymentMethod from the details entered in cardElement, which in turn will return an object containing the payment method ID, which we require at the back end.

Now let’s hit our back end and process the payment.

Firstly let’s import axios:

import axios from "axios"

Then, let’s make a request to our back end providing information about the payment:

const res = await axios.post("http://localhost:5000/donate", {
  amount,
  email,
  subscription,
  stripeToken: paymentMethod.id,
});

If there’s an error in the request or the response code points to an error, the code will stop executing and go to the catch block to handle the error.

Now the back end will attempt to perform simple HTTP payment and we will get a response. If we need 3D secure, actionRequired will be true:

if (res.data.actionRequired) {
  // We perform 3D Secure authentication
  const { paymentIntent, error } = await stripe.confirmCardPayment(
    res.data.clientSecret
  );
  if (error) return alert("Error in payment, please try again later");
  if (paymentIntent.status === "succeeded")
    return alert(`Payment successful, payment ID - ${res.data.id}`);
  const res2 = await axios.get(`http://localhost:5000/check/${res.data.id}`);
  alert(`Payment successful, payment ID - ${res.data.id}`);
} else {
  // Simple HTTP Payment was successful
  alert(`Payment successful, payment ID - ${res.data.id}`);
}

Here, we check if actionRequired is true. If it is, we need to trigger a 3D Secure authentication popup. We do that by passing in the clientSecret we get from server to confirmCardPayment() function from stripe.

Then, we get back the paymentIntent and check the payment from our server by sending the payment intent ID to the /check route of our Express server. The route returns a 200 status code if the payment was successful, otherwise our code will go through the catch block as explained before.

So that’s how you trigger 3D Secure. Here’s the complete code of Checkout.js:

import { CardElement } from "@stripe/react-stripe-js";
import React, { useState } from "react";
import axios from "axios";

function Checkout() {
  const [email, setEmail] = useState("");
  const [amount, setAmount] = useState("");
  const [subscription, setSubscription] = useState("onetime");
  const handleSubmit = async (e) => {
    try {
      e.preventDefault();
      if (!elements || !stripe) return;
      const cardElement = elements.getElement(CardElement);
      const { error, paymentMethod } = await stripe.createPaymentMethod({
        type: "card",
        card: cardElement,
      });
      const res = await axios.post("http://localhost:5000/donate", {
        amount,
        email,
        subscription,
        stripeToken: paymentMethod.id,
      });
      if (res.data.actionRequired) {
        // We perform 3D Secure authentication
        const { paymentIntent, error } = await stripe.confirmCardPayment(
          res.data.clientSecret
        );
        if (error) return alert("Error in payment, please try again later");
        if (paymentIntent.status === "succeeded")
          return alert(`Payment successful, payment ID - ${res.data.id}`);
        const res2 = await axios.get(`http://localhost:5000/check/${res.data.id}`);
        alert(`Payment successful, payment ID - ${res.data.id}`);
      } else {
        // Simple HTTP Payment was successful
        alert(`Payment successful, payment ID - ${res.data.id}`);
      }
    } catch (error) {
      console.error(error);
      alert("Payment failed!");
    }
  };

  return (
    <div className="checkout">
      <form className="checkout__container" onSubmit={handleSubmit}>
        <input
          type="email"
          value={email}
          className="checkout__textBox"
          onChange={(e) => setEmail(e.target.value)}
          placeholder="E-mail Address"
        />
        <input
          type="number"
          value={amount}
          className="checkout__textBox"
          onChange={(e) => setAmount(e.target.value)}
          placeholder="Amount"
        />
        <div className="checkout__radio">
          <input
            type="radio"
            onChange={(e) => setSubscription("onetime")}
            checked={subscription === "onetime"}
          />
          Onetime
        </div>
        <div className="checkout__radio">
          <input
            type="radio"
            onChange={(e) => setSubscription("monthly")}
            checked={subscription === "monthly"}
          />
          Monthly
        </div>
        <CardElement
          options={{
            style: {
              base: {
                fontSize: "16px",
                color: "#424770",
                "::placeholder": {
                  color: "#aab7c4",
                },
              },
              invalid: {
                color: "#9e2146",
              },
            },
          }}
        />
        <button className="checkout__btn" type="submit">
          Donate
        </button>
      </form>
    </div>
  );
}
export default Checkout;

To test your Stripe integration, here are some card details provided by Stripe to test. You need to be on test mode to use these cards, and you will not be charged.

A popup will be opened when you enter the card with 3D Secure. In production environments, the user will be sent an SMS to their phone number to authenticate the payment.

You can set your Radar rules to force 3D Secure for supported cards, just be aware that Radar rules are not available for all countries.

What’s Next?

I recommend checking out more from Stripe, like Apple Pay, Google Pay, saved cards, off-session payment, and the multiple other payment methods offered.

You can also check out Stripe Checkout, where you just need to pass in products and the payment will be handled by Stripe.

LogRocket: See the technical and UX reasons for why users don’t complete a step in your ecommerce flow.

LogRocket is like a DVR for web apps and websites, recording literally everything that happens on your ecommerce app. Instead of guessing why users don’t convert, LogRocket proactively surfaces the root cause of issues that are preventing conversion in your funnel, such as JavaScript errors or dead clicks. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Start proactively monitoring your ecommerce apps — .

Atharva Deosthale Web Developer and Designer | JavaScript = ❤ | MERN Stack Developer

One Reply to “Implementing 3D Secure in Stripe”

Leave a Reply