Getting Started with Stripe in Spring Boot
Stripe is an online payment system almost similar to Paypal, and it enables transactions to be processed by leveraging credit cards provided by the customers when buying products online.
In this tutorial, the reader will learn how to integrate stripe into their application by creating a simple web application that collects card details and submits a payment to stripe.
Table of Contents
- Prerequisites
- Create a stripe account
- Project set up
- Set up Stripe
- Create a request DTO
- Create a PaymentIntent
- Create a model for the product
- Create payment and checkout pages
- Create a controller for the payment
- Test the application
- Conclusion
Prerequisites
To follow along with this tutorial, you need to have:
- Knowledge in Spring Boot.
- Knowledge in Thymeleaf.
- JDK 11+ installed on your computer.
- Intellij IDEA installed on your computer.
Create a stripe account
Creating a stripe account is required to have access to public and private keys that we will use later to test our application.
When we log in to our stripe account, we will be redirected to the dashboard, and this is where we will verify that the payment was successful. The API keys section contains our private and public keys, and we can differentiate a live key from a test key by checking which characters are prefixed on the keys.
A test key is prefixed with pk_test_
denoting private key or sk_test_
denoting secret key, and a live key is prefixed with pk_live_
denoting private key or sk_live_
denoting secret key.
Project set up
We will use Spring Initialzr to generate a Spring Boot application with the following dependencies:
- Spring Web
- Thymeleaf
- Spring Boot Dev Tools
- Validation
To ensure the public and private keys are available to the application, we add the essential contents in the application.properties
file, and then we can inject the values using @Value
annotation. The @Value
annotation is used for expression-driven or property-driven dependency injection.
stripe.api.key=sk_test_
stripe.public.key=pk_test_
Setting up stripe
Setting up stripe requires adding a dependency for the stripe API in the POM.xml file for Maven users, and the dependency can be obtained from maven central repository. Maven will download the provided stripe API version and add it to the classpath to ensure it is available during development.
Stripe has a runtime dependency gson
, a JSON library that we will add to maven to ensure no dependency errors occur when we run the application.
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>20.77.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
Spring Boot uses Jackson by default, a JSON marshaller, and there are edge cases when you have both of them on the classpath. Make sure to exclude Jackson from the classpath using the following configuration in your POM.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
Create a request Data Transfer Object
This is a DTO that the browser will send to our server. Create a CreatePayment
class inside a package named dto
with fields amount
and featureRequest
. Generate getter and setter methods for the two fields.
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class CreatePayment {
@NotNull
@Min(4)
private Integer amount;
@NotNull
@Size(min = 5, max = 200)
private String featureRequest;
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getFeatureRequest() {
return featureRequest;
}
public void setFeatureRequest(String featureRequest) {
this.featureRequest = featureRequest;
}
}
@NotNull
- Indicates that the field is required and can not be empty.
@Min()
- Used to restrict the value provided to a specific minimum value.
@Size
- Provide the range of strings that the field can support by specifying the minimum and maximum values.
Create a PaymentIntent
A PaymentIntent
is an object used by stripe to record customers' information, track charge attempts, and change the state of payment from one stage to another. The following image shows how a PaymentIntent
is created and tracks the payment from providing card details, attempting payment, and finalizing the payment.
Create a PaymentController
class inside a package named controller
and add a post mapping method with a /create-payment-intent
endpoint. The browser calls the/create-payment-intent
endpoint, which has to call stripe to create the payment intent.
The PaymentIntentCreateParams
tells stripe the currency to be used, what the product the user wants to buy and how much the product costs using setCurrency()
and setAmount()
methods.
import com.example.StripeWithSpringBoot.dto.CreatePayment;
import com.example.StripeWithSpringBoot.dto.CreatePaymentResponse;
import com.stripe.exception.StripeException;
import com.stripe.model.PaymentIntent;
import com.stripe.param.PaymentIntentCreateParams;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class PaymentController {
@PostMapping("/create-payment-intent")
public CreatePaymentResponse createPaymentIntent(@RequestBody @Valid CreatePayment createPayment)throws StripeException {
PaymentIntentCreateParams createParams = new
PaymentIntentCreateParams.Builder()
.setCurrency("usd")
.putMetadata("featureRequest", createPayment.getFeatureRequest())
.setAmount(createPayment.getAmount() * 100L)
.build();
PaymentIntent intent = PaymentIntent.create(createParams);
return new CreatePaymentResponse(intent.getClientSecret());
}
}
Create a model for the product
Create a CheckoutForm
class inside a model
package with the fields amount
, featureRequest
, and email
. Next, generate getter and setter methods for the fields. This class represents customer details for a particular payment and will be provided during the checkout process.
import javax.validation.constraints.*;
public class CheckoutForm {
@NotNull
@Min(4)
private Integer amount;
@NotNull
@Size(min = 5,max = 200)
private String featureRequest;
@Email
private String email;
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getFeatureRequest() {
return featureRequest;
}
public void setFeatureRequest(String featureRequest) {
this.featureRequest = featureRequest;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Email
- indicates that this field should only accept a string with an email structure.
Create Payment and Checkout pages
First, create a file named client.js
that we will use to process the request from the customer to the server and response from the server to the browser. When the user goes to the page, a payment intent is created, the payment intent calls stripe, informs the customer wants to pay, and returns a secret key.
Stripe has a javascript library called the javascript elements, and when you call elements.create()
on a specific div
, then stripe will insert the component into the div.
The payment happens at the confirmCardPayment()
method, which accepts customers' secrets and cards, and once you submit it, everything happens between stripe and the browser, and no one ever sees the data.
// A reference to Stripe.js initialized with your real test publishable API key.
var stripe = Stripe(stripePublicKey);
// The items the customer wants to buy
var purchase = {
email:email
amount: amount,
featureRequest: featureRequest
};
// Disable the button until we have Stripe set up on the page
document.querySelector("button").disabled = true;
fetch("/create-payment-intent", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(purchase)
})
.then(function(result) {
return result.json();
})
.then(function(data) {
var elements = stripe.elements();
var style = {
base: {
color: "#32325d",
fontFamily: 'Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#32325d"
}
},
invalid: {
fontFamily: 'Arial, sans-serif',
color: "#fa755a",
iconColor: "#fa755a"
}
};
var card = elements.create("card", { style: style });
// Stripe injects an iframe into the DOM
card.mount("#card-element");
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 : "";
});
var form = document.getElementById("payment-form");
form.addEventListener("submit", function(event) {
event.preventDefault();
// Complete payment when the submit button is clicked
payWithCard(stripe, card, data.clientSecret);
});
});
// 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, {
receipt_email: email,
payment_method: {
card: card,
billing_details: {
email: email
}
}
})
.then(function(result) {
if (result.error) {
// Show error to your customer
showError(result.error.message);
} else {
// The payment succeeded!
orderComplete(result.paymentIntent.id);
}
});
};
/* ------- UI helpers ------- */
// 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;
};
// 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);
};
// 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");
}
};
Create a file named index.html
under the resources section, and we will use this page to display a payment form to the customer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Accept a card payment</title>
<meta name="description" content="A demo of a card payment on Stripe" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="global.css" />
<script src="client.js" defer></script>
</head>
<body>
<!-- Display a payment form -->
<div class="container">
<div class="form-card">
<form id="payment-form" th:action="@{/}" th:object="${checkoutForm}" method="post">
<p><b>Payment details for a product</b></p>
<div class="form-control">
<input type="text" th:field="*{email}" placeholder="Email address" required />
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" style="color: red;"></p>
</div>
<div class="form-control">
<input type="text" th:field="*{amount}" placeholder="Amount" required />
<p th:if="${#fields.hasErrors('amount')}" th:errors="*{amount}" style="color: red;"></p>
</div>
<div class="form-control">
<input type="text" th:field="*{featureRequest}" placeholder="Feature Request" required />
<p th:if="${#fields.hasErrors('featureRequest')}" th:errors="*{featureRequest}" style="color: red;"></p>
</div>
<p><input type="submit" class="btn btn-primary"></p>
</form>
</div>
</div>
</body>
</html>
Once a customer has filled in all the details and pressed the submit button, they will be redirected to a checkout page to enter their credit card details and complete payment.
Create a file named checkout.html
under the resources section, in which we will display a checkout form to the customer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Accept a card payment</title>
<meta name="description" content="A demo of a card payment on Stripe" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="global.css" />
<script src="https://js.stripe.com/v3/"></script>
<script src="https://polyfill.io/v3/polyfill.min.js?version=3.52.1&features=fetch"></script>
<script th:inline="javascript">
/*<![CDATA[*/
var stripePublicKey = /*[[${stripePublicKey}]]*/ null;
var amount = /*[[${amount}]]*/ null;
var email = /*[[${email}]]*/ null;
var featureRequest = /*[[${featureRequest}]]*/ null;
/*]]>*/
</script>
<script src="client.js" defer></script>
</head>
<body>
<!-- Display a payment form -->
<div class="container">
<div class="form-card">
<form id="payment-form">
<p><b>Credit card details to complete payment</b></p>
<p>Press the pay now button to complete your payment of <span th:text="${amount}"></span>USD.</p>
<div id="card-element" class="form-control"><!--Stripe.js injects the Card Element--></div>
<button id="submit" class="btn btn-primary">
<div class="spinner hidden" id="spinner"></div>
<span id="button-text">Pay now</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>
</div>
</div>
</body>
</html>
Create a file named global.css
that we will use to color our payment and checkout pages.
/* Variables */
:root{
--primary-color: #047aed;
}
.container{
max-width: 1100px;
margin: 0 auto;
overflow: auto;
padding: 0 40px;
}
.form-card{
background-color: #fff;
color: #333;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
padding: 20px;
margin: 10px;
}
.btn{
display: inline-block;
padding: 10px 30px;
cursor: pointer;
background: var(--primary-color);
color: #fff;
border: none;
border-radius: 5px;
}
.btn-primary{
background-color: var(--primary-color);
color: #fff;
}
.form-control{
margin: 30px 0;
}
.form-card input[type='text']{
border: 0;
border-bottom: 1px solid #b4becb;
width: 100%;
padding: 3px;
font-size: 16px;
}
.form-card input:focus{
outline: none;
}
.hidden{
display: none;
}
Creating a Controller for the Payment
- Create a
WebController
class inside thecontroller
package and inject the stripe public key using@Value
annotation. - Add a method that returns the payment form when the customer issues a request to the
/
endpoint. - Create another method to return the checkout page after entering the payment details and hitting the submit button.
import com.example.StripeWithSpringBoot.model.CheckoutForm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.validation.Valid;
@Controller
public class WebController {
@Value("${stripe.public.key}")
private String stripePublicKey;
@GetMapping("/")
public String home(Model model){
model.addAttribute("checkoutForm",new CheckoutForm());
return "index";
}
@PostMapping("/")
public String checkout(@ModelAttribute
@Valid CheckoutForm checkoutForm,
BindingResult bindingResult,
Model model){
if (bindingResult.hasErrors()){
return "index";
}
model.addAttribute("stripePublicKey",stripePublicKey);
model.addAttribute("amount",checkoutForm.getAmount());
model.addAttribute("email", checkoutForm.getEmail());
model.addAttribute("featureRequest", checkoutForm.getFeatureRequest());
return "checkout";
}
}
Testing the Application
Stripe provides several test numbers that we can use to test the payment, and in this tutorial, we will make use of 4242 4242 4242 4242
, which is a Visa card number. Provide any 3
digits for the CVC and any future date for the date field such that when we run the application and navigate to localhost:8080
, the following payment page is displayed to the browser.
When we enter all the input fields and press the submit button, we will be redirected to the checkout page to enter the payment details so enter 4242 4242 4242 4242
as the test card number, any three CVC numbers, and any future date, and press the submit button.
When you press the pay now button, the payment will be processed successfully, and a link to our stripe dashboard will be added to the form. The Stripe dashboard displays the amount of money paid, the date, the customer, and the payment method, as shown in the following image.
Conclusion
In this tutorial, we have learned how to integrate stripe in a Spring Boot application by leveraging stripe payment intent that ensures at least one payment is made for each transaction. The following tutorial will help the reader learn how to store the details returned by stripe to our database by creating a webhook containing the details.
Peer Review Contributions by: Jerim Kaura