Got an e-commerce site powered by Spring Boot? Need to accept payments via credit card? If so, then you'll be happy to learn that the Stripe API makes it easy to collect cash online!

In this guide, I'll walk you through how to implement a Stripe payment solution within your Spring Boot application.

As always, you're free to go straight to the code on GitHub. However, if you'd like to learn about the whys and wherefores of what's going on in that code, feel free to read on.

The Use Case

Your boss Smithers walks into your office. He likes what you've done with the e-commerce site, but he's unhappy that the application doesn't accept credit card payments.

He says that the big bosses on the board no longer want to bill people for their orders. Instead, they want shoppers to pay online before the company ships any products.

Also, Smithers says that he's heard some good things about that Stripe payment gateway. Maybe you should look at that.

He leaves your office singing "We're in the money."

Get a Stripe Account

Before you can write a lick of code here, you're going to need to get yourself set up with a Stripe account. Don't worry, it's free.

And not only is it free, the good folks at Stripe will set you up with a playpen so you can test credit card payments without using a real credit card number. 

Once you get your Stripe account set up, you'll receive two important keys:

  • A publishable key
  • A secret key

Those names are pretty descriptive. You can share the publishable key with others. But don't you dare share that secret key. Keep that all to yourself and developers you trust.

You'll need both those keys to proceed with the coding.

The E-Commerce Models

Speaking of coding, let's start by looking at the e-commrece models. Those are the models that represent customer orders.

First, the Product model:

public class Product {

    private String name;
    private Money price;
    private BigDecimal priceValue;
    
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Money getPrice() {
        return Money.of(priceValue, CurrencyUtil.USD);
    }
    public BigDecimal getPriceValue() {
        return priceValue;
    }
    public void setPriceValue(BigDecimal priceValue) {
        this.priceValue = priceValue;
    }
}

 

It doesn't take a genius to figure out that model represents a single product that a customer can order.

You might be surprised, though, to see that the code is using both a price and a priceValue field. 

The price field returns a Money object. That's part of the JSR 354 specification that you might find useful if you're doing a lot of work with currency (especially if you're doing work with currency from different countries).

The priceValue field, on the other hand, returns a BigDecimal object that the application can print on the screen.

Next, let's take a look at the LineItem class:

public class LineItem {

    private Product product;
    private int quantity;
    
    public Product getProduct() {
        return product;
    }
    public void setProduct(Product product) {
        this.product = product;
    }
    public int getQuantity() {
        return quantity;
    }
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

 

As the name implies, the LineItem object represents a line item on an order. All it does is associate a specific product with a quantity. So if the customer orders two Big Monster T-Shirts, the product in the line item will be Big Monster T-Shirt and the quantity will be 2.

And finally:

public class Order {

    private List<LineItem> lineItems;

    public List<LineItem> getLineItems() {
        return lineItems;
    }

    public void setLineItems(List<LineItem> lineItems) {
        this.lineItems = lineItems;
    }
}

 

An order is nothing more than a collection of line items. And that's all you see in the Order class.

The Payment Model

Next, let's take a look at the payment model. This is what you'll use when interacting with the Stripe API.

public class CreatePaymentResponse {
    private String clientSecret;
    
    public CreatePaymentResponse(String clientSecret) {
      this.clientSecret = clientSecret;
    }

    public String getClientSecret() {
        return clientSecret;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }
}

 

Oh, look. Another easy one.

All that model holds is the client secret. That's YOUR secret, by the way.

The Services

The application includes a couple of services that you'll need to understand as well. 

First, take a look at InventoryService:

@Service
public class InventoryService {

    public List<Product> getInventory() {
        List<Product> inventory = new ArrayList<Product>();

        Product bigMonsterShirt = new Product();
        bigMonsterShirt.setName("Big Monster T-Shirt");
        bigMonsterShirt.setPriceValue(new BigDecimal("22.99"));
        
        Product superSquirtGun = new Product();
        superSquirtGun.setName("Super Squirt Gun");
        superSquirtGun.setPriceValue(new BigDecimal("33.75"));
        
        inventory.add(bigMonsterShirt);
        inventory.add(superSquirtGun);
        
        return inventory;       
    }
}

 

As you can see that service includes one and only one public method that returns your entire inventory. 

To keep things simple here, your entire inventory is hard-coded and it only includes two (2) items. Those are the items that your customer can order.

Next, have a look at the OrderFormService:

@Service
public class OrderFormService {

    @Autowired
    private InventoryService inventoryService;
    
    
    public Order getEmptyOrder() {
        Order order = new Order();
        
        List<LineItem> lineItems = new ArrayList<LineItem>();
        
        List<Product> products = inventoryService.getInventory();
        products.forEach(product -> {
            LineItem lineItem = new LineItem();
            lineItem.setProduct(product);
            lineItem.setQuantity(0);
            
            lineItems.add(lineItem);
        });
        
        order.setLineItems(lineItems);
        
        return order;
    }
}

 

Well, now things are getting a little more interesting, aren't they?

The bottom line for this service: it instantiates an empty order form. Then, it's up to the customer to decide how many of each product he or she wants to buy.

As you can see, the code uses an Autowired InventoryService object (that you just looked at) to get the entire inventory of two (2) products. Then, for each product, it creates a new line item and sets the quantity ordered to 0 because you don't want to assume that the customer will order anything.

A Couple of Utilities

Next, check out a couple of the utility methods. First up is OrderUtil:

public class OrderUtil {

    public static Long calculateOrderAmountInCents(Order order) {
        BigDecimal amount = new BigDecimal("0");

        if (order != null && order.getLineItems() != null) {
            for (LineItem lineItem : order.getLineItems()) {
                if (lineItem.getQuantity() > 0) {
                    Product product = lineItem.getProduct();
                    Money productPrice = product.getPrice();
                    Money totalCost = productPrice.multiply(lineItem.getQuantity());
                    amount = amount.add(totalCost.getNumberStripped());
                }
            };
        }
        
        amount = amount.multiply(new BigDecimal("100"));
        
        return amount.longValue();
    }
}

 

The whole point of that single static method is to calculate the total order value. It does that by adding up the value of each line item.

You might wonder why the code is multiplying the final value by 100 towards the end. There's a very good reason for that.

The Stripe API takes the amount charged on the credit card in cents.

So if you have a dollar and cents amount, you need to multiply it by 100 to get the number of cents.

And here's CurrencyUtil:

public class CurrencyUtil {

    public static final CurrencyUnit USD = Monetary.getCurrency("USD");
}

 

All that does is identify the currency you're using as U.S. dollars. You'll want something more sophisticated if you're working with an international audience.

The Properties File

You're going to need to put the secret key value in your application.properties file. The entry looks like this:

stripe.secret.key=[API KEY HERE]

Just substitute your own secret key for the "[API KEY HERE]" text.

Keep in mind: you should use the test key for this demo. 

When you created your Stripe account, you should have received two sets of keys: one for testing and one for production. Use the testing keys throughout this guide.

Once you're convinced that everything is ready to go, you can switch to the production keys.

The Controller

Continuing with the "Keep It Simple" theme, this project includes only one controller. It looks like this:

@Controller
public class OrderFormController {

    @Autowired
    private OrderFormService orderFormService;

    @Value("${stripe.secret.key}") 
    private String stripeSecretKey;
 
    
    @GetMapping("/orderForm")
    public String orderForm(Model model) {
        Order order = orderFormService.getEmptyOrder();
        model.addAttribute("order", order);
        
        return "orderForm";
    }
    
    
    @PostMapping("/orderForm")
    public String processOrder(Order order, Model model) {
        Stripe.apiKey = stripeSecretKey;

        Long totalAmount = OrderUtil.calculateOrderAmountInCents(order);
        model.addAttribute("totalAmount", Money.of(totalAmount, CurrencyUtil.USD).divide(100).getNumberStripped());
        
        try {
            PaymentIntentCreateParams createParams = new PaymentIntentCreateParams.Builder()
                    .setCurrency("usd")
                    .setAmount(totalAmount)
                    .build();
            
            PaymentIntent intent = PaymentIntent.create(createParams);
            CreatePaymentResponse paymentResponse = new CreatePaymentResponse(intent.getClientSecret());
            
            model.addAttribute("paymentResponse", paymentResponse);
        } catch (StripeException se) {
            se.printStackTrace();
        }
        
        return "payment";
    }
}

 

As you can see, the code Autowires the OrderFormService singleton that you saw earlier. It also grabs the value of the API secret key that you just put in the application.properties file.

The first public method handles a GET request. That's when a user loads a page with an empty order form so he or she can decide how many of each item to order.

The second public method handles a POST request. That's when the user is finished with the order and proceeds to check out.

In this section, I'll just cover the first method. The start of the code instantiates the empty Order object and places it in the model with the name "order."

The method returns the String "orderForm." That references a Thymeleaf template that displays the order form in HTML.

Let's look at that next.

The Order Form

Here's the order form in all its HTML glory:

<!DOCTYPE html>
<html lang="en-US" xmlns:th="http://www.thymeleaf.org">
<head>
    <object th:include="fragments/head :: head" th:remove="tag"></object>
    <title>Order Form</title>
</head>

<body>
    <h1>Order Form</h1>
    <form id="orderForm" th:action="@{/orderForm}" th:object="${order}" method="POST">
        <div class="line-item" th:each="lineItem, lineItemStatus : *{lineItems}">
            <input type="hidden" th:field="*{lineItems[__${lineItemStatus.index}__].product.name}"/>
            <input type="hidden" th:field="*{lineItems[__${lineItemStatus.index}__].product.priceValue}"/>
            <div class="product-name" th:text="${lineItem.product.name} + ' @ $' + ${lineItem.product.priceValue}"></div>
            <div class="product-quantity"><input class="quantity" type="number" min="0" max="5" value="0" th:field="*{lineItems[__${lineItemStatus.index}__].quantity}"/></div>
        </div>
        <div>
            <button type="submit">Check Out</button>
        </div>
    </form>
</body>
</html>

 

Focus on the <form> element and its child elements.

For starters, note that the form is associated with the Order object that got put in the model in the previous step.  For more info about how that works, check out my guide to Thymeleaf validation.

Also, pay attention to the th:action attribute. That identifies the URL the user will POST the form to when clicking the Check Out button.

The URL is the same as the URL that brought the user to this page. However, since the user is POSTing to that URL (a form submission) instead of GETting it (a simple HTML page load), the second method in the controller from the previous section will handle the processing.

That's why you see the @GetMapping annotation for the first method  in the controller and the @PostMapping annotation for the second method. The first one handles a standard page display while the second one handles a form submission.

Okay, so what's this all about:

<div class="line-item" th:each="lineItem, lineItemStatus : *{lineItems}">

That's telling the template engine to iterate over the line items.

Recall that there are exactly two (2) line items on the order form: one for each product. The form defaults to a quantity of 0 for each product. The user decides how many of each product to order.

The unusual notation for the iteration (the asterisk in from of {lineItems}" binds the iteration to the underlying form object.

Within the iteration each line item is identified with the unimaginative name of lineItem

The lineItemStatus reference you see above maintains info about each iteration. For example, you can use that reference to grab the current index. That index will tell you whether the iteration is on its first loop, second loop, third loop, and so on.

Now take a look at the next two lines:

<input type="hidden" th:field="*{lineItems[__${lineItemStatus.index}__].product.name}"/>
<input type="hidden" th:field="*{lineItems[__${lineItemStatus.index}__].product.priceValue}"/>

There's some fairly bizarre syntax in those lines.

What's it all about? That code binds the product name and price to the underlying form. So when the user submits the form, the application will have those values for each line item.

And this is why you're using both price and priceValue in the Product class.  HTML isn't quite sophisticated enough to handle a Money object. It really needs something more basic.

So priceValue is stored as a BigDecimal and later converted to Money.  

Now, as far as the weird *{lineItems[__${lineItemStatus.index}__] notation, that's doing nothing more than getting the current LineItem object in the iteration. Then, it's grabbing a property from the object (either the product name or the price value).

Note that lineItemStatus.index tells the application where it's at in the current iteration. It's a position in the array of LineItem objects. 

And remember that asterisk out in front binds the field to the form. That's very important.

Now look at this:

<div class="product-name" th:text="${lineItem.product.name} + ' @ $' + ${lineItem.product.priceValue}"></div>

There's no input in that line of code. That just shows the user the name of the product on that line item as well as its price.

And then this:

<div class="product-quantity"><input class="quantity" type="number" min="0" max="5" value="0" th:field="*{lineItems[__${lineItemStatus.index}__].quantity}"/></div>

There is input in that line of code. That's where the user selects the quantity of each product that he or she would like to purchase. The user can select between 0 and 5 as a quantity for each product.

Take a look at the familiar syntax towards the end and you'll see that it's binding the quantity that the user selects to the quantity field of the LineItem object.

And finally:

<div>
    <button type="submit">Check Out</button>
</div>

That's just a button that submits the form when the user clicks it.

The Other Method

Now it's time to revist the controller and take a look at the second method. That's the one that handles a POST request.

    @PostMapping("/orderForm")
    public String processOrder(Order order, Model model) {
        Stripe.apiKey = stripeSecretKey;

        Long totalAmount = OrderUtil.calculateOrderAmountInCents(order);
        model.addAttribute("totalAmount", Money.of(totalAmount, CurrencyUtil.USD).divide(100).getNumberStripped());
        
        try {
            PaymentIntentCreateParams createParams = new PaymentIntentCreateParams.Builder()
                    .setCurrency("usd")
                    .setAmount(totalAmount)
                    .build();
            
            PaymentIntent intent = PaymentIntent.create(createParams);
            CreatePaymentResponse paymentResponse = new CreatePaymentResponse(intent.getClientSecret());
            
            model.addAttribute("paymentResponse", paymentResponse);
        } catch (StripeException se) {
            se.printStackTrace();
        }
        
        return "payment";
    }

 

As you can see, the method accepts the Order object as a parameter. That's the object that gets passed in via the form submission.

The first line sets the Stripe secret key. The code gets that key from the line in the application.properties file that you saw earlier. Take a look at the @Value setting at the top of the controller class.

The next line gets the total amount that you'll charge to the person's credit card. Remember that value is in cents.

However, when you display the total amount to the user, you want to show the value in dollars and cents because it would be pretty user-hostile to show it only in cents.

That's why this line exists:

model.addAttribute("totalAmount", Money.of(totalAmount, CurrencyUtil.USD).divide(100).getNumberStripped());

That's going to put the charged amount as a BigDecimal object in the model. It also divides the number of cents by 100 to get the dollars-and-cents number.

Next, take a look at the code in the try block. Everything in there is pretty much boilerplate code you can get from the Stripe website.

The first "line" (I know,, it's actually multiple lines) creates PaymentIntent parameters. The second line instantiates the PaymentIntent object using those parameters.

What's a PaymentIntent? Exactly what it sounds like. It's an intent to charge someone's credit card to pay for goods or services.

More than that, though, the PaymentIntent object tracks the payment from its creation all the way through the checkout process.

Oh, by the way: the line that creates the PaymentIntent object isn't just a simple bit of Java code. It actually makes a network call to Stripe.

That network call returns a lot of data, but for the purposes of this guide all you need to know is this: it returns a client secret specific to this transaction.

According to the Stripe API docs, the client secret "should not be stored, logged, embedded in URLs, or exposed to anyone other than the customer."

Next, the code uses the client secret (a String) from the PaymentIntent object to instantiate the CreatePaymentResponse object that you already looked in the Models section.

At the end of the try block, the code adds the CreatePaymentResponse object to the model so it can be handled on the client side.

Finally, the method returns the String "payment." Once again, that's a reference to a Thymeleaf template that renders HTML.

The payment page here is nothing more than a rudimentary shopping cart. Users will see the total value of their order and have the option to enter credit card info.

Let's look at that next.

The Payment Page

Here's the code for the payment page or checkout page or shopping cart:

<!DOCTYPE html>
<html lang="en-US" xmlns:th="http://www.thymeleaf.org">
<head>
    <object th:include="fragments/head :: head" th:remove="tag"></object>
    <title>Enter Paymennt Info</title>
    
    <script th:inline='javascript'>
        //get this from the controller
        var secretKey = /*[[${paymentResponse.clientSecret}]]*/ 'Secret Key';       
    </script>
    
    <script src="https://js.stripe.com/v3/"></script>
    <script src="/js/client.js" defer></script>
</head>

<body>
    <h1>Enter Payment Info</h1>
    <div class="total-amount-section">
        Total Order Value: $<span th:text="${totalAmount}"></span>
    </div>
    <form id="payment-form">
      <div id="card-element"><!--Stripe.js injects the Card Element--></div>
      <button id="submit">
        <div class="spinner hidden" id="spinner"></div>
        <span id="button-text">Pay</span>
      </button>
      <p id="card-error" role="alert"></p>
      <p class="result-message hidden">
        Payment succeeded, see the result in your
        <a href="" target="_blank">Stripe dashboard.</a> Refresh the page to pay again.
      </p>
    </form>
</body>
</html>

 

Let's break that down a bit.

First, take a look at the JavaScript towards the top:

<script th:inline='javascript'>
     //get this from the controller
     var secretKey = /*[[${paymentResponse.clientSecret}]]*/ 'Secret Key';       
</script>

That's Thymeleaf's fairly bizarre way of embedding a model object into JavaScript code. In this case, the code is grabbing the secret key from the CreatePaymentResponse object and setting it to the variable secretKey in JavaScript.

Now, you might be asking yourself the following question: "Wait. Why are we storing the secret key on the client side? I thought it was supposed to be secret! Everyone can see that key by viewing the source!"

Well, yeah. But remember, this isn't your secret key.

Instead, it's a secret key that Stripe generated specifically for this transaction. So you're in the clear.

Next, take a look at these imports:

<script src="https://js.stripe.com/v3/"></script>
<script src="/js/client.js" defer></script>

The first import is Stripe's API code. You'll need that if you want to use Stripe for payment processing.

The next import is code that I swiped with pride from Stripe and added to the source here. Much of that code you'll see if you look at some of the samples on Stripe's website.

However, there are some differences. I'll cover that code in the next section.

Now, take a look at some of the HTML:

<div class="total-amount-section">
    Total Order Value: $<span th:text="${totalAmount}"></span>
</div>

That's the part that tells the user how much he or she has to pay for the order. Remember, Stripe accepts payment in cents but here you're showing the amount in dollars and cents.

And finally the payment form:

<form id="payment-form">
    <div id="card-element"><!--Stripe.js injects the Card Element--></div>
    <button id="submit">
        <div class="spinner hidden" id="spinner"></div>
        <span id="button-text">Pay</span>
    </button>
    <p id="card-error" role="alert"></p>
    <p class="result-message hidden">
         Payment succeeded, see the result in your
         <a href="" target="_blank">Stripe dashboard.</a> Refresh the page to pay again.
     </p>
</form>

As the comments indicate in the first line of the form, Stripe will inject the payment element between the two <div> tags. The JavaScript code will handle that so you don't have to worry about manually coding a bunch of elements that accept a credit card number, expiration date, CVC, and zip code.

The submit button on the next line submits the payment to Stripe. If everything goes as planned, you'll see a "Payment succeeded" message on the UI.

If everything does not go as planned, you'll see an error in that <p> element with the id of "card-error."

The Client-Side Code

Next, take a look at the JavaScript code. It's fairly large so I'll break it down bit-by-bit.

// A reference to Stripe.js initialized with your real test publishable API key.
var stripe = Stripe("[Your public API key]");

The first thing the code does is instantiate the Stripe object with your public API key. Make sure you substitute your real public key in the code.

Reminder: it's okay if other people see your public key. They just can't see your private key. So there's no security issues here.

Still, before going to production, you should have a data security consultant take a look at your code. That's an especially good idea with payment processing solutions.

Next line:

// Disable the button until we have Stripe set up on the page
document.querySelector("button").disabled = true;

That line just prevent users from entering data on the page until everything is set up.

var style = {
  base: {
    color: "#32325d",
    fontFamily: 'Arial, sans-serif',
    fontSmoothing: "antialiased",
    fontSize: "16px",
    "::placeholder": {
    }
  },
  invalid: {
    fontFamily: 'Arial, sans-serif',
    color: "#fa755a",
  }
};

That makes the UI look pretty. Nothing more. Feel free to consult a CSS tutorial for more info about that.

var elements = stripe.elements();
var card = elements.create("card", { style: style });

Now things are getting a little more interesting.

The first line creates an Elements object. That's an object that manages a group of... well... elements.

What's an element? In this case an element is UI component that enables users to enter credit card info. And that's exactly what's happening in that second line.

// Stripe injects an iframe into the DOM
card.mount("#card-element");

Well the comment says it all on that one. Remember: I just cribbed this code from the Stripe website. This is the code that the company recommends for payment processing.

card.on("change", function (event) {
    // Disable the Pay button if there are no card details in the Element
    document.querySelector("button").disabled = event.empty;
    document.querySelector("#card-error").textContent = event.error ? event.error.message : "";
});

The point of the code above is to make it impossible for users to submit the card for payment if they haven't entered anything. It disables the Pay button.

var form = document.getElementById("payment-form");
form.addEventListener("submit", function(event) {
    event.preventDefault();
    
    // Complete payment when the submit button is clicked
    payWithCard(stripe, card, secretKey);
});

In the above segment, the code instantiate a Form object from the HTML page. Then, it adds an event listener to that object.

Effectively, that event listener "listens" for the user to press the Pay button. Recall that the Pay button performs a form submission.

The event.preventDefault() line prohibits normal form submission. That's because Stripe wants to use its own code to handle the form submission.

The payWithCard() reference invokes a JavaScript method that you'll see in the next code fragment.

// Calls stripe.confirmCardPayment
// If the card requires authentication Stripe shows a pop-up modal to
// prompt the user to enter authentication details without leaving your page.
var payWithCard = function(stripe, card, clientSecret) {
  loading(true);
  stripe
    .confirmCardPayment(clientSecret, {
      payment_method: {
        card: card
      }
    })
    .then(function(result) {
      if (result.error) {
        // Show error to your customer
        showError(result.error.message);
      } else {
        // The payment succeeded!
        orderComplete(result.paymentIntent.id);
      }
    });
};

As promised, there's the function that handles payment.

Note that the stripe.confirmCardWithPayment() function is handled within the Stripe JavaScript library. So I won't cover that here.

But, as you can imagine, that function lives up to its name. It's the big boy that handles the actual credit card processing.

That function returns a result which may be an error. In that case, the code proceeds to the showError() function that you'll see in a moment.

If there's no error, the code proceeds to the orderComplete() function that you'll see next.

// Shows a success message when the payment is complete
var orderComplete = function(paymentIntentId) {
  loading(false);

  document
    .querySelector(".result-message a")
    .setAttribute(
      "href",
      "https://dashboard.stripe.com/test/payments/" + paymentIntentId
    );
  document.querySelector(".result-message").classList.remove("hidden");
  document.querySelector("button").disabled = true;
};

That function handles a good response from Stripe. That means the payment went through and everything is peachy.

Here's what's happening in that code: there's an HTML element with a class named "result-message" on the page. By default, that element is hidden.

That element contains a successful response message from Stripe. It also include a hyperlink that you can click on to see payment info on the Stripe website.

However, the URL for that hyperlink is unique per transaction. So Stripe needs to populate the URL at runtime.

And that is exactly what that code is doing above. It populates the URL and unhides the element so you can see it.

The last line of code disables the payment button. That's because the payment already went through.

// Show the customer the error from Stripe if their card fails to charge
var showError = function(errorMsgText) {
  loading(false);
  var errorMsg = document.querySelector("#card-error");
  errorMsg.textContent = errorMsgText;
  setTimeout(function() {
    errorMsg.textContent = "";
  }, 4000);
};

The code above displays the error message in the event that Stripe returned an error with payment processing.

And finally:

// Show a spinner on payment submission
var loading = function(isLoading) {
  if (isLoading) {
    // Disable the button and show a spinner
    document.querySelector("button").disabled = true;
    document.querySelector("#spinner").classList.remove("hidden");
    document.querySelector("#button-text").classList.add("hidden");
  } else {
    document.querySelector("button").disabled = false;
    document.querySelector("#spinner").classList.add("hidden");
    document.querySelector("#button-text").classList.remove("hidden");
  }
};

That just shows a spinner when users are waiting for a response from Stripe. It also stops the spinner once it's no longer needed.

Testing It Out

You've come this far. You might as well go the distance.

It's time to test this thing out.

Fire up the Spring Boot application by right-clicking on StripeGuide.java and selecting Run As... and Java Application from the context menu that appears.

Next, open up a browser and hit this link: http://localhost:8080/orderForm

You should see something like this:

You can enter the quantity of each product you'd like to buy manually or just use the stepper arrows to select a quantity.

For the purposes of this test, buy 1 of each item. Then click Check Out.

When you do that, you should see a "shopping cart" that looks like this:

Ideally, you'd show users the items they've ordered and not just the total value of the purchase. But I'm keeping things as simple as possible for this guide.

And look at this: our first test passed!

What test is that? The Pay button is disabled. That's because you didn't enter any payment info yet.

Now, enter a credit card number.

Don't get scared. You don't have to use a real credit card number. Just go with a standard test number like: 4242 4242 4242 4242. 

Yep. That will work.

For the expiration date, enter and month and date in the future.

Finally, enter any three numbers for the CVC and your local zip code.

Now press Pay.

If everything went according to plan, you should see something like this:

And yes, you can click that Stripe dashboard link to see the payment info on the Stripe website.

Wrapping It Up

Whew! You made it through!

Now, the rest is up to you. Do some negative testing with that code. Enter an expired credit card and see if the code works as expected.

Not sure where to get an expired credit card? Once again, feel free to use a test credit card number designed for exactly that purpose. You can find plenty of test numbers that cover different scenarios right here.

As always, feel free to just grab the code from GitHub and start tinkering.

Have fun!