Stripe Payment Intents Quick Start
Broadleaf Commerces offers an out-of-the-box Stripe solution that requires little configuration and is easily set up.
The quick start solution implements the https://stripe.com/docs/payments/payment-intents/quickstart model offered by the Stripe API.
This implementation should be useful for those with a simple checkout flow.
You must have completed the Stripe Payment Intents Environment Setup before continuing
Adding Stripe Payment Intent Support
These instructions assume integration with the default Heat Clinic Demo Site provided with the framework.
- Add the following code to your
head.html
template. This will pull in the Stripe Javascript library and style the Credit Card Elements components: ```html
2. Add the following code to your payment method form. In Reference Site, this would be `paymentMethods.html` template. This will authorize your Checkout Session using Stripe's JS library and take the user's payment information.
```html
<div id="Stripe" class="tab-pane">
<div id="stripe_payment_method" th:fragment="form">
<div class="form-row">
<label for="cardholder-name">
Cardholder Name
</label>
<input id="cardholder-name" type="text">
</div>
<div class="form-row">
<label for="card-element">
Credit or debit card
</label>
<!-- Use this code for the enabling Payment Element functionality-->
<!-- <div id="payment-element">-->
<!-- <!– Mount the Payment Element here –>-->
<!-- </div>-->
<div id="card-element">
<!-- A Stripe Element will be inserted here. -->
</div>
<!-- Used to display form errors. -->
<div id="card-errors" role="alert"></div>
</div>
<button id="card-button" class="btn">Submit Payment</button>
<input id="js-stripe_session_id"
type="hidden"
th:attr="data-key=${#stripe.generateClientToken()}"/>
</div>
</div>
Add the following Javascript wherever it will be run on the initialization of your payment method form. In Reference Site, this would be
Checkout.initialize
incheckoutOperations.js
.Checkout.initializeStripe = function () { console.log("Initializing Stripe"); var input = $('#js-stripe_session_id'); var pk = input.attr("data-key"); var stripe = Stripe(pk.toString()); //Use this code for the enabling Payment Element functionality // const options = { // mode: 'payment', // currency: 'usd', // amount: 1099, // }; // const elements = stripe.elements(options); // // const paymentElement = elements.create("payment"); // paymentElement.mount("#payment-element"); var elements = stripe.elements(); var cardElement = elements.create('card', { 'style': { 'base': { 'fontFamily': 'Arial, sans-serif', 'fontSize': '16px', 'color': '#C1C7CD', }, 'invalid': { 'color': 'red', }, } }); cardElement.mount('#card-element'); var cardholderName = document.getElementById('cardholder-name'); var cardButton = document.getElementById('card-button'); cardButton.addEventListener('click', function (ev) { console.log("Card Button clicked"); stripe.createPaymentMethod('card', cardElement, { billing_details: {name: cardholderName.value} }).then(function (result) { console.log("Payment Method Created " + result); if (result.error) { var displayError = document.getElementById('card-errors'); displayError.textContent = result.error.message; } else { var options = { type: 'POST', url: '/checkout/stripe/confirm_payment_method', data: { payment_method_id: result.paymentMethod.id } }; BLC.ajax(options, function(data) { if (data.error) { console.log("Error from Broadleaf"); var displayError = document.getElementById('card-errors'); displayError.textContent = data.error.message; } else if (data.requiresActionRedirectUrl) { console.log("Payment Requires Action: " + data); window.location = data.requiresActionRedirectUrl; } else { console.log("Payment Confirmed: " + data); window.location = '/confirmation/' + data.orderNumber; } }); } }); }); cardElement.addEventListener('change', function (event) { var displayError = document.getElementById('card-errors'); if (event.error) { displayError.textContent = event.error.message; } else { displayError.textContent = ''; } }); };
Note that call Checkout.initializeStripe() should be done on change tab with payment method(if you have more than 1) or maybe on page load if you have only 1
For testing purposes you can call it from browser console when you've changed tab if you are using ReferenceSite
- Add the following Controller to your Site application. This handles the form you created above in order to process the payment nonce and confirm the transaction.
package com.blcdemo.controller.checkout;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.common.payment.PaymentGatewayType;
import org.broadleafcommerce.common.payment.PaymentTransactionType;
import org.broadleafcommerce.common.payment.PaymentType;
import org.broadleafcommerce.common.payment.dto.PaymentRequestDTO;
import org.broadleafcommerce.common.payment.dto.PaymentResponseDTO;
import org.broadleafcommerce.core.checkout.service.exception.CheckoutException;
import org.broadleafcommerce.core.order.domain.NullOrderImpl;
import org.broadleafcommerce.core.order.domain.Order;
import org.broadleafcommerce.core.order.domain.OrderAttribute;
import org.broadleafcommerce.core.order.domain.OrderAttributeImpl;
import org.broadleafcommerce.core.payment.domain.OrderPayment;
import org.broadleafcommerce.core.payment.domain.PaymentTransaction;
import org.broadleafcommerce.core.web.controller.checkout.BroadleafCheckoutController;
import org.broadleafcommerce.core.web.order.CartState;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.broadleafcommerce.payment.service.gateway.StripeCustomerServiceImpl;
import com.broadleafcommerce.vendor.stripe.service.exception.StripePaymentIntentRequiresActionException;
import com.broadleafcommerce.vendor.stripe.service.payment.MessageConstants;
import com.broadleafcommerce.vendor.stripe.service.payment.StripePaymentGatewayType;
import com.broadleafcommerce.vendor.stripe.service.type.StripePaymentType;
import com.stripe.model.PaymentIntent;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// TODO
// If you want more detailed error handling, you will need to modify this code.
// This code also assumes you are using a similar checkout flow to Broadleaf's Reference Site.
// If you have a custom checkout flow, you may need to change this code.
@Controller
public class StripeCheckoutController extends BroadleafCheckoutController {
private static final Log LOG = LogFactory.getLog(StripeCheckoutController.class);
public static final String STRIPE_ORDER_UUID = "STRIPE_ORDER_UUID";
public static final String STRIPE_CUSTOMER_ID = "STRIPE_CUSTOMER_UUID";
@Resource(name = "blStripeCustomerService")
protected StripeCustomerServiceImpl stripeCustomerService;
@RequestMapping(value = "/checkout/stripe/confirm_payment_method", method = RequestMethod.POST, produces = "application/json")
public @ResponseBody Map<String, Object> confirmStripePaymentMethod(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "payment_method_id", required = true) String paymentMethodId,
RedirectAttributes redirectAttributes) throws IOException, ServletException {
Map<String, Object> responseMap = new HashMap<String, Object>();
PaymentIntent intent = null;
try {
Order cart = CartState.getCart();
OrderAttribute stripeOrderUUID = cart.getOrderAttributes().get(STRIPE_ORDER_UUID);
PaymentTransaction lastFailureTransaction = getLastFailureTransaction();
if (stripeOrderUUID == null ||
(lastFailureTransaction != null && stripeOrderUUID.getValue().equals(lastFailureTransaction.getAdditionalFields().get(MessageConstants.STRIPE_ORDER_UUID)))) {
stripeOrderUUID = new OrderAttributeImpl();
stripeOrderUUID.setOrder(cart);
stripeOrderUUID.setName(STRIPE_ORDER_UUID);
stripeOrderUUID.setValue(UUID.randomUUID().toString());
cart.getOrderAttributes().put(STRIPE_ORDER_UUID, stripeOrderUUID);
}
LOG.info("Attempting to CREATE Stripe Customer, CREATE Payment Intent from Payment Method ID, and perform initial AUTH");
PaymentRequestDTO paymentIntentRequestDTO = dtoTranslationService.translateOrder(cart);
paymentIntentRequestDTO.getAdditionalFields().put(MessageConstants.PAYMENT_METHOD_ID, paymentMethodId);
CustomerAttribute stripeCustomerIdAttr = cart.getCustomer().getCustomerAttributes().get(STRIPE_CUSTOMER_ID);
if (stripeCustomerIdAttr == null) {
paymentIntentRequestDTO.getAdditionalFields().put(MessageConstants.IDEMPOTENCY_KEY, UUID.randomUUID().toString());
PaymentResponseDTO customerResponseDTO = stripeCustomerService.createGatewayCustomer(paymentIntentRequestDTO);
String stripeCustomerId = customerResponseDTO.getResponseMap().get(MessageConstants.CUSTOMER_ID);
if (stripeCustomerId == null) {
throw new PaymentException("Error creating Stripe Customer");
}
stripeCustomerIdAttr = new CustomerAttributeImpl();
stripeCustomerIdAttr.setCustomer(cart.getCustomer());
stripeCustomerIdAttr.setName(STRIPE_CUSTOMER_ID);
stripeCustomerIdAttr.setValue(stripeCustomerId);
cart.getCustomer().getCustomerAttributes().put(STRIPE_CUSTOMER_ID, stripeCustomerIdAttr);
}
OrderPayment paymentIntent = orderPaymentService.create();
paymentIntent.setType(StripePaymentType.PAYMENT_INTENT);
paymentIntent.setPaymentGatewayType(StripePaymentGatewayType.STRIPE);
paymentIntent.setAmount(cart.getTotalAfterAppliedPayments());
paymentIntent.setOrder(cart);
// Populate Billing Address per UI requirements
// For this example, we'll copy the address from the temporary Credit Card's Billing address and archive the payment,
// (since Heat Clinic's checkout template saves and validates the address in a previous section).
OrderPayment tempPayment = null;
for (OrderPayment payment : cart.getPayments()) {
if (PaymentGatewayType.TEMPORARY.equals(payment.getGatewayType()) &&
PaymentType.CREDIT_CARD.equals(payment.getType())) {
tempPayment = payment;
break;
}
}
if (tempPayment != null){
paymentIntent.setBillingAddress(addressService.copyAddress(tempPayment.getBillingAddress()));
orderService.removePaymentFromOrder(cart, tempPayment);
}
// Create the UNCONFIRMED transaction for the payment
PaymentTransaction transaction = orderPaymentService.createTransaction();
transaction.setAmount(cart.getTotalAfterAppliedPayments());
transaction.setRawResponse("Stripe Payment Intent");
transaction.setSuccess(true);
transaction.setType(PaymentTransactionType.UNCONFIRMED);
transaction.setOrderPayment(paymentIntent);
transaction.getAdditionalFields().put(MessageConstants.PAYMENT_METHOD_ID, paymentMethodId);
transaction.getAdditionalFields().put(MessageConstants.CUSTOMER_ID, stripeCustomerIdAttr.getValue());
transaction.getAdditionalFields().put(MessageConstants.PAYMENT_INTENT_CONFIRM, "true");
transaction.getAdditionalFields().put(MessageConstants.IDEMPOTENCY_KEY, stripeOrderUUID.getValue());
paymentIntent.addTransaction(transaction);
orderService.addPaymentToOrder(cart, paymentIntent, null);
orderService.save(cart, true);
if (!(cart instanceof NullOrderImpl)) {
try {
String orderNumber = "";
if (paymentGatewayCheckoutService != null && cart.getId() != null) {
orderNumber = paymentGatewayCheckoutService.initiateCheckout(cart.getId());
}
responseMap.put("orderNumber", orderNumber);
return responseMap;
} catch (Exception e) {
if (e.getCause() instanceof CheckoutException) {
CheckoutException ce = (CheckoutException) e.getCause();
if (ce.getRootCause() instanceof StripePaymentIntentRequiresActionException) {
StripePaymentIntentRequiresActionException requiresAction = (StripePaymentIntentRequiresActionException) ce.getRootCause();
responseMap.put("requiresActionRedirectUrl", requiresAction.getPaymentIntent().getNextAction().getRedirectToUrl().getUrl());
return responseMap;
}
}
if (LOG.isErrorEnabled()) {
LOG.error(e);
}
}
}
} catch (Exception e) {
if (LOG.isErrorEnabled()) {
LOG.error(e);
}
}
responseMap.put("errorMessage", processErrorMessage());
return responseMap;
}
@RequestMapping(value = "/checkout/stripe/confirm_payment_intent", method = RequestMethod.GET)
public String confirmStripePaymentIntent(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "payment_intent", required = true) String paymentIntentId,
@RequestParam(value = "payment_intent_client_secret", required = false) String paymentIntentClientSecret,
@RequestParam(value = "source_type", required = false) String sourceType,
RedirectAttributes redirectAttributes) throws IOException, ServletException {
LOG.info("Attempting to CONFIRM Payment Intent and perform initial AUTH");
try {
Order cart = CartState.getCart();
//Find existing Order Payment Intent
OrderPayment orderPaymentIntent = null;
for (OrderPayment payment : cart.getPayments()) {
if (StripePaymentGatewayType.STRIPE.equals(payment.getGatewayType()) &&
StripePaymentType.PAYMENT_INTENT.equals(payment.getType()) &&
payment.getArchived() != 'Y') {
orderPaymentIntent = payment;
break;
}
}
List<PaymentTransaction> unconfirmedTransactions = orderPaymentIntent.getTransactionsForType(PaymentTransactionType.UNCONFIRMED);
PaymentTransaction transaction = unconfirmedTransactions.get(0);
transaction.getAdditionalFields().remove(MessageConstants.PAYMENT_METHOD_ID);
transaction.getAdditionalFields().put(MessageConstants.PAYMENT_INTENT_ID, paymentIntentId);
//override the idempotency key as this needs to be unique for the payment intent confirmation
String currentIdempotencyKey = transaction.getAdditionalFields().get(MessageConstants.IDEMPOTENCY_KEY);
transaction.getAdditionalFields().put(MessageConstants.IDEMPOTENCY_KEY, currentIdempotencyKey + "_" + paymentIntentId);
orderService.save(cart, true);
if (!(cart instanceof NullOrderImpl)) {
try {
String orderNumber = initiateCheckout(cart.getId());
return getConfirmationViewRedirect(orderNumber);
} catch (Exception e) {
handleProcessingException(e, redirectAttributes);
}
}
return getCheckoutPageRedirect();
} catch (Exception e) {
if (LOG.isErrorEnabled()) {
LOG.error(e);
}
}
return getCheckoutPageRedirect();
}
protected String processErrorMessage() {
StringBuilder result = new StringBuilder();
PaymentTransaction transaction = getLastFailureTransaction();
if (transaction != null) {
String errorCode = transaction.getAdditionalFields().get("errorCode");
String errorMessage = transaction.getAdditionalFields().get("errorMessage");
result.append(errorCode == null ? "" : errorCode + ":")
.append(errorMessage == null ? "" : errorMessage);
}
return result.toString();
}
protected PaymentTransaction getLastFailureTransaction() {
List<OrderPayment> payments = CartState.getCart().getPayments();
Collections.reverse(payments);
for (OrderPayment payment : payments) {
if (StripePaymentGatewayType.STRIPE.equals(payment.getGatewayType()) &&
StripePaymentType.PAYMENT_INTENT.equals(payment.getType())) {
if (CollectionUtils.isNotEmpty(payment.getTransactions())) {
for (PaymentTransaction transaction : payment.getTransactions()) {
if (!transaction.getSuccess()) {
return transaction;
}
}
}
}
}
return null;
}
@Override
public void handleProcessingException(Exception e, RedirectAttributes redirectAttributes) throws PaymentException {
if (LOG.isTraceEnabled()) {
LOG.trace("A Processing Exception Occurred finalizing the order. Adding Error to Redirect Attributes.");
}
PaymentTransaction transaction = getLastFailureTransaction();
String errorMessage = null;
if (transaction != null) {
errorMessage = transaction.getAdditionalFields().get("errorCode");
} else {
errorMessage = PaymentGatewayAbstractController.getProcessingErrorMessage();
}
redirectAttributes.addAttribute(PaymentGatewayAbstractController.PAYMENT_PROCESSING_ERROR, errorMessage);
}
}
- (Optional) The above Controller throws an exception in order to break out of the checkout workflow before we have authentication from Stripe. This will print an error and stack trace in your logs, which may not be desired. You can add the following override to your CoreConfig.java (or equivalent configuration file) to not log this Stripe exception.
// TODO - This code assumes you are using the Broadleaf Framework without modification to the referenced Sequence Processor.
// If you have modified blCheckoutWorkflowActivities, you will need to change this code.
@Bean
public SequenceProcessor blCheckoutWorkflow(final @Qualifier("blCheckoutWorkflowActivities") Object activities) {
SequenceProcessor<CheckoutSeed, CheckoutSeed> checkoutWorkflow = new SequenceProcessor<>();
CheckoutProcessContextFactory checkoutProcessContextFactory = new CheckoutProcessContextFactory();
checkoutWorkflow.setProcessContextFactory(checkoutProcessContextFactory);
checkoutWorkflow.setActivities((List<Activity<ProcessContext<CheckoutSeed>>>) activities);
DefaultErrorHandler defaultErrorHandler = new DefaultErrorHandler();
List<String> unloggedExceptions = new ArrayList<>();
unloggedExceptions.add("org.broadleafcommerce.core.inventory.service.InventoryUnavailableException");
// Add the Stripe exception
unloggedExceptions.add("com.broadleafcommerce.vendor.stripe.service.exception.StripePaymentIntentRequiresActionException");
defaultErrorHandler.setUnloggedExceptionClasses(unloggedExceptions);
checkoutWorkflow.setDefaultErrorHandler(defaultErrorHandler);
return checkoutWorkflow;
}
Done!
At this point, all the configuration should be complete and you are now ready to test your integration with Stripe Payment Intents API. Add something to your cart and proceed with checkout.
The following credit card numbers (provided by Stripe at https://stripe.com/docs/payments/payment-intents/quickstart#manual-confirmation-flow) allow you to test all three possible flows. Any expiration date in the future and any three digit CVV code will validate.
4000000000003220 - This test card requires 3D Secure 2 on all transactions and will trigger 3D Secure 2 in test mode.
4000008400001629 - This test card requires 3D Secure 2 but payments will be declined with a card_declined failure code after authentication.
4000000000003055 - This test card supports but does not require 3D Secure 2 and will not require additional authentication steps in test mode.