Integrating Stripe payment with Spring Boot

Sudhakarketha
6 min readApr 9, 2024

--

Discover how to seamlessly integrate Stripe, a leading payment gateway, into your Spring Boot application. Simplify payment processing and enhance user experiences with Stripe’s robust API and developer-friendly tools. Follow along as we guide you through the step-by-step process of setting up Stripe, implementing payment logic, and elevating your application’s payment capabilities.

To integrate Stripe payment gateway in a Spring Boot application, you can follow these steps:


stripe account : create a stripe account to get your own public and secret keys.
You will use the keys in your application.

Go to the Stripe website and sign up for an account.

Navigate to the dashboard and find your API keys under the Developers -> API keys section.

create a new project

the following image shows the dependencies and the project structure of the application.

you can also need to add stripe dependency to the project. Open the file named pom.xml and copy and paste the following XML into the file

<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>22.0.0</version>
</dependency>

Add public and secret keys

If you did not keep a copy of the keys, go to stripe dashboard to get copy. to add the keys, Open the application properties file and them as shown below

stripe.api.publicKey=YOUR_PUBLIC_KEY
stripe.api.secretKey=YOUR_SECRET_KEY

Load the secret key

create a file named config.java in the config package.

package com.javawhizz.stripePayment.config;

import com.stripe.Stripe;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {
@Value("${stripe.api.secretKey}")
private String secretKey;

@PostConstruct
public void initSecretKey(){
Stripe.apiKey = secretKey;
}
}
  • @Configuration: This annotation indicates that this class provides bean definitions for the application context. It's typically used on classes that contain @Bean methods.
  • @PostConstruct: This annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization.
  • The secretKey field is annotated with @Value("${stripe.api.secretKey}"), which injects the value of stripe.api.secretKey property from the application properties file.

Create Request DTO

Define a DTO (Data Transfer Object) for capturing payment information:

package com.javawhizz.stripePayment.model;

import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Request {
@NotNull
@Min(4)
private Long amount;

@Email
private String email;

@NotBlank
@Size(min = 5, max = 200)
private String productName;
}

The PaymentInfoRequest class serves as a data transfer object (DTO) for capturing payment information from the client-side. It contains attributes such as amount, email, and productName, which are essential for processing payments.

  • amount: Represents the amount of the payment. It is annotated with @NotNull to ensure it is not null and @Min(4) to ensure it is at least 4.
  • email: Represents the email address of the user making the payment. It is annotated with @Email to ensure it is a valid email address.
  • productName: Represents the name of the product being purchased. It is annotated with @NotBlank to ensure it is not null or empty, and @Size(min = 5, max = 200) to specify the size constraints.

create Response DTO

package com.javawhizz.stripePayment.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Response {
private String intentID;
private String clientSecret;
}

Create payment intent controller

Create a controller to handle payment-related requests:

package com.javawhizz.stripePayment.controller;

import com.javawhizz.stripePayment.model.Request;
import com.javawhizz.stripePayment.model.Response;
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;

@RestController
public class PaymentIntentController {
@PostMapping("/create-payment-intent")
public Response createPaymentIntent(@RequestBody Request request)
throws StripeException {
PaymentIntentCreateParams params =
PaymentIntentCreateParams.builder()
.setAmount(request.getAmount() * 100L)
.putMetadata("productName",
request.getProductName())
.setCurrency("usd")
.setAutomaticPaymentMethods(
PaymentIntentCreateParams
.AutomaticPaymentMethods
.builder()
.setEnabled(true)
.build()
)
.build();

PaymentIntent intent =
PaymentIntent.create(params);

return new Response(intent.getId(),
intent.getClientSecret());
}
}

Create controller for the application

create a file named AppController.java in the controller package.

package com.javawhizz.stripePayment.controller;

import com.javawhizz.stripePayment.model.Request;
import jakarta.validation.Valid;
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;

@Controller
public class AppController {
@Value("${stripe.api.publicKey}")
private String publicKey;
@GetMapping("/")
public String home(Model model){
model.addAttribute("request", new Request());
return "index";
}

@PostMapping("/")
public String showCard(@ModelAttribute @Valid Request request,
BindingResult bindingResult,
Model model){
if (bindingResult.hasErrors()){
return "index";
}
model.addAttribute("publicKey", publicKey);
model.addAttribute("amount", request.getAmount());
model.addAttribute("email", request.getEmail());
model.addAttribute("productName", request.getProductName());
return "checkout";
}
}

Create a product details page

create a file named index.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
</head>
<body>

<div class="container">
<div class="row">
<div class="col-md-12">
<form th:action="@{/}" th:object="${request}" method="post">
<h2>Enter product details</h2>
<div class="mb-3">
<label for="email"
class="form-label">Email address</label>
<input type="email"
th:field="*{email}"
required class="form-control"
id="email"
aria-describedby="emailHelp">
<div id="emailHelp"
class="form-text">
We'll never share your email with anyone else.
</div>
<p th:if="${#fields.hasErrors('email')}"
th:errors="*{email}"
style="color: red"/>
</div>
<div class="mb-3">
<label for="amount"
class="form-label">Amount</label>
<input type="number"
required th:field="*{amount}"
class="form-control"
id="amount">
<p th:if="${#fields.hasErrors('amount')}"
th:errors="*{amount}"
style="color: red"/>

</div>
<div class="mb-3">
<label for="productName"
class="form-label" >Product Name</label>
<input type="text"
required
th:field="*{productName}"
class="form-control"
id="productName">
<p th:if="${#fields.hasErrors('productName')}"
th:errors="*{productName}"
style="color: red"/>

</div>
<button type="submit"
class="btn btn-primary">Submit</button>
</form>

</div>

</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
</body>
</html>

Create the checkout page

create a file named checkout.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>Accept a payment</title>
<meta name="description" content="A demo of a payment on Stripe" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/checkout.css" />
<script src="https://js.stripe.com/v3/"></script>

<script th:inline="javascript">

/*<![CDATA[*/

var publicKey = /*[[${publicKey}]]*/ null;
var amount = /*[[${amount}]]*/ null;
var email = /*[[${email}]]*/ null;
var productName = /*[[${productName}]]*/ null;

/*]]>*/
</script>

<script src="/checkout.js" defer></script>
</head>
<body>
<!-- Display a payment form -->
<form id="payment-form">
<h2>Like the content: Support JavaWhizz</h2>
<span>You are about to make a payment of: </span>
<span th:text="*{amount}"></span>
<span>USD</span>
<div id="link-authentication-element">
<!--Stripe.js injects the Link Authentication Element-->
</div>
<div id="payment-element">
<!--Stripe.js injects the Payment Element-->
</div>
<button id="submit">
<div class="spinner hidden" id="spinner"></div>
<span id="button-text">Pay now</span>
</button>
<div id="payment-message" class="hidden"></div>
</form>
</body>
</html>

Implement the javascript code for the checkout page

create a file named checkout.js

// This is your test publishable API key.
const stripe = Stripe(publicKey);

// The items the customer wants to buy
const request = {
amount: amount,
email: email,
productName: productName
}

let elements;

initialize();
checkStatus();

document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);

let emailAddress = '';
// Fetches a payment intent and captures the client secret

let paymentIntentID = '';
async function initialize() {
const response = await fetch("/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

const { intentID, clientSecret } = await response.json();

paymentIntentID = intentID;

const appearance = {
theme: 'stripe',
};
elements = stripe.elements({ appearance, clientSecret });

const linkAuthenticationElement = elements.create("linkAuthentication");
linkAuthenticationElement.mount("#link-authentication-element");

linkAuthenticationElement.on('change', (event) => {
emailAddress = event.value.email;
});

const paymentElementOptions = {
layout: "tabs",
defaultValues: {
billingDetails:{
email: request.email
}
}
};

const paymentElement = elements.create("payment", paymentElementOptions);
paymentElement.mount("#payment-element");
}

console.log(paymentIntentID);

async function handleSubmit(e) {
e.preventDefault();
setLoading(true);

const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "https://dashboard.stripe.com/test/payments/"+paymentIntentID,
receipt_email: emailAddress
},
});

if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occurred.");
}

setLoading(false);
}

// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);

if (!clientSecret) {
return;
}

const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);

switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}

// ------- UI helpers -------

function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");

messageContainer.classList.remove("hidden");
messageContainer.textContent = messageText;

setTimeout(function () {
messageContainer.classList.add("hidden");
messageContainer.textContent = "";
}, 4000);
}

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

Run and test the Application

if the application runs wothout any errors, you should see the following page.

enter the product details and press the submit button. next it redirects to the checkout page.

since you don’t want to use your original card for the transaction. stripe provides the test cards that you can use to test the payments.

--

--