Looking for a payment solution for your Spring Boot app? If so, then you should check out Braintree Direct.
Why? For starters, Braintree is a division of PayPal. So if you’re looking for PayPal integration, then you’re all set.
Beyond that, though, you can use Braintree to accept Most major credit cards, Apple Pay, Google Pay, and Venmo.
What’s not to love?
In this article, we’ll go over how you can implement a Braintree Direct solution within your Spring Boot application.
Feel free to review the entire source for this project on GitHub.
The Use Case
As usual, we’ll start with the use case because we’re business-focused around these parts.
Your boss, Smithers, just showed up in your office and told you that the company needed a payment solution for a new e-commerce website.
Smithers is clearly frustrated because he’s never rolled out a payment solution before. “I’ve always done admin screens!” he says in exasperation.
No problem, you reply. You assure him that you can integrate a Braintree solution into the app.
Smithers leave your office smiling.
Assumptions
To get the most out of this demo, you’ll need a working knowledge of Java, Spring Boot, and Thymeleaf. There are plenty of tutorials online that will help you understand that tech stack if you’re unfamiliar with any of it.
You should also have a basic understanding of Braintree. That doesn’t mean you need to be an expert in the integration technology (that’s why you’re reading this article, after all). But you should know how to set up an account and configure it to accept various payments.
For the purposes of this demo, the application is only accepting credit card payments. It’s not accepting PayPal, Apple Pay, Google Pay, or Venmo.
Playing in the Sandbox
Fortunately, Braintree offers a sandbox environment where you can play to your heart’s content with fake credit card transactions. That way, you can make sure that your code works without having to max out your card.
You’ll need to set up a sandbox in Braintree if you want to use the demo code locally.
Once you have that set up, you’ll get a Merchant ID. You can find it by viewing your Merchant Account info located under the Account menu at the top of the screen.
You’ll also need a private key and public key. You can access those by selecting API Keys under the Settings menu.
The POM File
You need to update your POM file to include the Braintree Java client library. Add the following dependency:
1
2
3
4
5
|
< dependency > < groupId >com.braintreepayments.gateway</ groupId > < artifactId >braintree-java</ artifactId > < version >2.79.0</ version > </ dependency > |
That’s the only addition you need to make for this project.
The Config Class
Next, you need to create a config class. Here’s what that will look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
@Configuration @ComponentScan ( "com.braintreegateway" ) public class BraintreeConfig { private static String CONFIG_FILENAME = "config.properties" ; @Bean public BraintreeGateway getBraintreeGateway() { Map<String,String> configMap = getMap(); BraintreeGateway gateway = null ; try { gateway = BraintreeGatewayFactory.fromConfigMapping(configMap); } catch (Exception e) { e.printStackTrace(); System.exit( 1 ); } return gateway; } /** * Creates the map that we'll use to initialize the gateway * Reads values from the config file */ private Map<String,String> getMap() { Map<String,String> map = new HashMap<String,String>(); try { //get the input stream from the file in the resources folder Resource resource = new ClassPathResource(CONFIG_FILENAME); InputStream is = resource.getInputStream(); //create a Properties object of key/value pairs Properties properties = new Properties(); properties.load(is); //Special thanks to Java 8 map.putAll(properties.entrySet().stream() .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()))); } catch (Exception e) { e.printStackTrace(); System.exit( 1 ); } return map; } } |
There are two annotations at the top of the code. They’re very important.
The @Configuration
annotation tells Spring that the class is a configuration class. That means it can be used to define beans that will ultimately be injected into other objects. We’ll see how that works later.
The @ComponentScan
annotation is necessary because the code will create a bean from an external library. In this case, it’s instantiating a BraintreeGateway
object from the Braintree Java client library.
Note that the code is scanning the com.braintreegateway
package. That’s the base package of the library.
At the top, the first line of code is a constant that defines the name and location of the configuration file. That file will include important info that Braintree needs, such as the public and private key.
In this case, the config file is named config.properties
and located in the root of the resources
folder.
Here’s the structure of the file:
1
2
3
4
|
BT_ENVIRONMENT=sandbox BT_MERCHANT_ID=[YOUR MERCHANT ID] BT_PUBLIC_KEY=[YOUR PUBLIC KEY] BT_PRIVATE_KEY=[YOUR PRIVATE KEY] |
The only public method in the class is annotated with @Bean
. That’s because it’s used to create an object that can be managed by the Spring container and injected into other objects.
As you can see, getBraintreeGateway()
is intuitively named and returns a BraintreeGateway
object. It does that by calling the factory that we’ll look at in just a bit.
Before it can call BraintreeGatewayFactory
, though, it needs to create a map. The code does that by reading the config file mentioned above and translating the name/value pairs in the file to key/value pairs in a Java Map object.
The second method, called getMap()
, does most of the heavy lifting to make that happen.
The Factory
Next, let’s look at the class that creates the BraintreeGateway
object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public class BraintreeGatewayFactory { public static BraintreeGateway fromConfigMapping(Map<String, String> mapping) { return new BraintreeGateway( mapping.get( "BT_ENVIRONMENT" ), mapping.get( "BT_MERCHANT_ID" ), mapping.get( "BT_PUBLIC_KEY" ), mapping.get( "BT_PRIVATE_KEY" ) ); } public static BraintreeGateway fromConfigFile(File configFile) { InputStream inputStream = null ; Properties properties = new Properties(); try { inputStream = new FileInputStream(configFile); properties.load(inputStream); } catch (Exception e) { System.err.println( "Exception: " + e); } finally { try { inputStream.close(); } catch (IOException e) { System.err.println( "Exception: " + e); } } return new BraintreeGateway( properties.getProperty( "BT_ENVIRONMENT" ), properties.getProperty( "BT_MERCHANT_ID" ), properties.getProperty( "BT_PUBLIC_KEY" ), properties.getProperty( "BT_PRIVATE_KEY" ) ); } } |
There are two public, static methods. The first one creates the object from a Map
.
Obviously, that’s the one used in this project.
The second method creates the object directly from the config file. That’s not used here.
If you’re wondering why the code uses the first method instead of the second one, it’s because the config file is embedded in the Spring JAR file. Spring has trouble reading File
objects from its own JAR.
That’s why it’s best to read those kinds of files as Stream
s. Then, the code can process the Stream
s with a Reader
or Properties
object.
The Checkout Page
To keep things simple, there’s only one Controller class in this demo. Here’s the method that handles the main checkout page:
1
2
3
4
5
6
7
8
9
10
11
|
@RequestMapping (value = "/checkouts" , method = RequestMethod.GET) public String checkout(Model model) { //get the token String clientToken = gateway.clientToken().generate(); //add the token to the model - this will be used in JavaScript code model.addAttribute( "clientToken" , clientToken); //serve new.html return "newTransaction" ; } |
There’s nothing too fancy in that. The most important part is that the code invokes the BraintreeGateway
object to create a client token. That token will be placed in the model so that the JavaScript code on the front end can use it to contact Braintree for payment processing.
Keep in mind the BraintreeGateway
object was injected into the controller class:
1
2
|
@Autowired private BraintreeGateway gateway; |
The method closes by serving up newTransaction.html.
Checkout Page HTML Code
Next, let’s look at the important parts of the HTML on the main checkout page. Here’s some of that code towards the top:
1
2
3
4
5
6
7
8
9
|
< div th:if = "${errorDetails}" class = "note note-danger" id = "serverSideErrorDiv" > < h4 class = "block" >Error</ h4 > < div id = "serverSideErrorText" th:text = "${errorDetails}" ></ div > </ div > < div class = "note note-danger" id = "errorDiv" style = "display:none" > < h4 class = "block" >Error</ h4 > < div id = "errorText" ></ div > </ div > |
Both of those div
tags are designed to hold error messages. The top one holds server-side error messages and the bottom one holds client-side error messages.
Below that, there’s a table that looks like a shopping cart. Most of the info is hard-coded. Here’s the default output:
As you can see, the only field that users can change is the price.
Here’s what the code for that table looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
< div class = "table-container" > < form id = "payment-form" method = "post" action = "/checkouts" > < table class = "table table-striped table-bordered table-hover table-checkable" id = "datatable_orders" > < thead > < tr role = "row" class = "heading" > < th width = "15%" > Order # </ th > < th width = "15%" > Purchased On </ th > < th width = "55%" > Product </ th > < th width = "15%" > Price </ th > </ tr > </ thead > < tbody > < tr role = "row" class = "filter" > < td >12345678</ td > < td >01/01/2018</ td > < td >Blue Light Saber</ td > < td > < div class = "margin-bottom-5" > < div class = "input-group input-group-sm" > < span class = "input-group-addon" id = "sizing-addon1" >$</ span > < input type = "text" class = "form-control form-filter input-sm margin-bottom-5 clearfix" maxlength = "6" name = "amount" id = "amount" th:value = "${amount != null} ? ${amount} : '10.00'" /> </ div > </ div > </ td > </ tr > </ tbody > </ table > ... </ form > </ div > |
Note that the table is encapsulated in a <form>
tag. That’s because of the single element that accepts input.
After the table code, there’s a placeholder for the Braintree drop-in.
What’s the Braintree drop-in? It’s the UI that Braintree’s JavaScript library will insert into the web page.
Here’s what the placeholder code looks like:
1
2
3
|
< div class = "bt-drop-in-wrapper" > < div id = "bt-dropin" ></ div > </ div > |
Finally, there’s the JavaScript portion of the checkout page:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
<script th:inline= "javascript" > /*<![CDATA[*/ var form = document.querySelector( '#payment-form' ); var client_token = ; braintree.dropin.create({ authorization: client_token, container: '#bt-dropin' , paypal: { flow: 'vault' } }, function (createErr, instance) { form.addEventListener( 'submit' , function (event) { event.preventDefault(); $( '#errorDiv' ).hide(); $( '#serverSideErrorDiv' ).hide(); instance.requestPaymentMethod( function (err, payload) { if (err) { console.log( 'Error' , err); showError(err); return ; } // Add the nonce to the form and submit document.querySelector( '#nonce' ).value = payload.nonce; form.submit(); }); }); }); function showError(err) { var message = String(err); $( '#errorText' ).html(message); $( '#errorDiv' ).show(); } /*]]>*/ </script> |
As you can see, the page starts off by referencing an external JavaScript library. That’s the code responsible for handling the drop-in.
Following that, you’ll see some page-specific JavaScript code. Most of it is dependent on the Braintree JavaScript library.
The code is blocked off with /*<[CDATA[*/
because the project is using Thymeleaf as its template engine of choice. Since Thymeleaf uses XHTML (instead of HTML), it’s a little fussier about what can and can’t be included in the code. Sometimes JavaScript causes problems with Thymeleaf, hence the CDATA
designation.
The first line in the JavaScript code creates a form object from the contents of the form element.
The next line creates the client token. As you can see, it sets the client_token
variable to the value of the clientToken
string put in the model by the controller class. Check out the controller code above to see how that happens.
Next, the code invokes the create() method from the braintree.dropin
object.
Two important things to note here. First, the braintree.dropin
object was created with the inclusion of the Braintree JavaScript library identified above.
Next, the create()
method is the workhorse method that plugs the payment widget into the page. As you might have guessed, it puts it in that placeholder element that you saw above.
The create()
method accepts two parameters: an array object and a callback function.
The array object contains a series of name/value pairs that are fairly intuitive. For example, the “container” name is assigned to the element ID of the div where the drop-in will appear. In this case, it’s set to “#bt-dropin” because “bt-dropin” is the ID of the placeholder div tag.
The second parameter is the callback function. That’s the JavaScript method that gets executed once the user submits the page.
As you can see from above, the code interrupts the submit function on the form. Then, it hides the two error <div>
elements.
After that, the code calls the requestPaymentMethod()
function on the drop-in instance object. That method accepts one parameter: a JavaScript function.
That function sets the nonce and submits the form.
If you’re unfamiliar with the word “nonce,” it’s a secure, single reference to payment info.
The showError()
function in that JavaScript block shows a client-side JavaScript error. If you look at the instance.requestPaymentMethod()
block just above, you’ll see that the callback function checks for the existence of err. If that exists, then it contains error info that the user needs to see. That’s why that block of code invokes the showError()
function.
Now, let’s look at what happens when that form gets submitted.
POSTing the Checkout Form
Here’s the method that handles form submission on the checkout page:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/** * We get here when the user submits the transaction - note that it's a POST */ @RequestMapping (value = "/checkouts" , method = RequestMethod.POST) public String postForm( @RequestParam ( "amount" ) String amount, @RequestParam ( "payment_method_nonce" ) String nonce, Model model, final RedirectAttributes redirectAttributes) { //get rid of whitespace amount = amount.trim(); BigDecimal decimalAmount; try { //get the decimal version of the text entered decimalAmount = new BigDecimal(amount); } catch (NumberFormatException e) { //we get here if it's not a valid number String errorMessage = getErrorMessage(amount); redirectAttributes.addFlashAttribute( "errorDetails" , errorMessage); redirectAttributes.addFlashAttribute( "amount" , amount); return "redirect:checkouts" ; } //submit the request for processing TransactionRequest request = new TransactionRequest() .amount(decimalAmount) .paymentMethodNonce(nonce) .options() .submitForSettlement( true ) .done(); //get the response Result<Transaction> result = gateway.transaction().sale(request); //if it's a successful transaction, go to the transaction results page if (result.isSuccess()) { Transaction transaction = result.getTarget(); return "redirect:checkouts/" + transaction.getId(); } else if (result.getTransaction() != null ) { Transaction transaction = result.getTransaction(); return "redirect:checkouts/" + transaction.getId(); } else { //if the transaction failed, return to the payment page and display all errors String errorString = "" ; for (ValidationError error : result.getErrors().getAllDeepValidationErrors()) { errorString += "Error: " + error.getCode() + ": " + error.getMessage() + "\n" ; } redirectAttributes.addFlashAttribute( "errorDetails" , errorString); redirectAttributes.addFlashAttribute( "amount" , amount); return "redirect:checkouts" ; } } |
There are several important things that stand out in that code.
First, take a look at the request method: POST
. That’s important because it’s using the same URL as the GET
method that serves up the page (“/checkouts
“).
Next, note that it’s accepting a couple of request parameters: amount
and payment_method_nonce
.
The first request parameter (“amount
“) is a field in the form. Have a look at the HTML code above to see that.
The second request parameter (“payment_method_nonce
“) was set by the Braintree JavaScript client library. That happened in the callback method parameter specified in instance.requestPaymentMethod()
.
The method signature also accepts a Model
object, which is typical. But in addition to that it accepts a RedirectAttributes
object, which isn’t typical. You’ll see why that’s needed in a moment.
After that, the code formats the amount
object (currently set as a String) to a BigDecimal
object. In the event that the code catches an exception when trying to format the amount, it will return the user to the main checkout screen and display the error.
That is the reason for the RedirectAttributes
object. Any attributes set in RedirectAttributes
will also be set in the Model
object created by the controller method used in the redirected URL.
If you look at the catch
block above, you’ll see that the code sets two Flash attributes: errorDetails
and amount
. Both of those will be accessed by the checkout page after the redirect.
The errorDetails
object is a String containing the error message that’s created with the getErrorMessage()
method. The amount
object is a String
that holds the value the user entered into the amount field on HTML form.
If the amount was formatted correctly, the code submits the transaction request for processing by creating a TransactionRequest object with method chaining. You can learn more about transactions from the Braintree docs.
After that, the code grabs the result from the transaction request to retrieve the status of the submission. To do that, it creates a type-safe Result
object. In this case, it’s enforcing the Transaction
type to prevent ClassCastException
. That’s why you see <Transaction>
next to the class name.
Finally, the code evaluates the result object to check for a success (or failure). Either way, it returns the user to a new page.
If the transaction failed, the user is redirected back to the main checkout page with error details.
If the transaction was successful, the code redirects the user to “checkouts/{transactionId}
” where {transactionId}
is the ID of the transaction returned from the Result
object.
Let’s take a look at that redirect now.
Successful Form Submission
Staying in the controller class, let’s look at the method that handles a successful payment transaction.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@RequestMapping (value = "/checkouts/{transactionId}" ) public String getTransaction( @PathVariable String transactionId, Model model) { Transaction transaction; CreditCard creditCard; Customer customer; try { //find the transaction by its ID transaction = gateway.transaction().find(transactionId); //grab credit card info creditCard = transaction.getCreditCard(); //grab the customer info customer = transaction.getCustomer(); } catch (Exception e) { System.out.println( "Exception: " + e); return "redirect:/checkouts" ; } //set a boolean that determines whether or not the transaction was successful model.addAttribute( "isSuccess" , Arrays.asList(TRANSACTION_SUCCESS_STATUSES).contains(transaction.getStatus())); //put the relevant objects in the model model.addAttribute( "transaction" , transaction); model.addAttribute( "creditCard" , creditCard); model.addAttribute( "customer" , customer); //server transactionResults.html return "transactionResults" ; } |
As you can see from the get-go, the method passes in a transaction ID that’s actually part of the URL itself. In other words, the transaction ID isn’t set as a request parameter.
Within the method, the code uses the injected BraintreeGateway
object to find the transaction by its ID. Then it uses that Transaction
object to grab credit card and customer info.
Several Model
attributes are set. The first is a boolean (“isSuccess
“) that’s set to true if the transaction was successful.
After that, the code sets the Transaction
, CreditCard
, and Customer
objects in the model, respectively.
Finally, the method returns the String "transactionResults"
which means that it will serve up transactionResults.html.
Let’s look at that code next.
The Successful Transaction Page
Once the user has successfully submitted a transaction, the app will display a page with transaction details for confirmation purposes. Here’s a look at the table code that displays that info:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<table class = "table table-hover table-light" > <thead> <tr class = "uppercase" > <th> ID </th> <th> Type </th> <th> Amount </th> <th> Status </th> <th> Created </th> </tr> </thead> <tbody> <tr> <td th:text= "${transaction.getId()}" ></td> <td th:text= "${transaction.getType()}" ></td> <td th:text= "${transaction.getAmount()}" ></td> <td th:text= "${transaction.getStatus()}" ></td> <td th:text= "${new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss Z').format(transaction.getUpdatedAt().getTime())}" ></td> </tr> </tbody> </table> |
Nothing really fancy there. The code is just grabbing various fields from the Transaction
object that was added to the Model
in the previous section.
Testing It Out
Once the code is completed, it’s time to test it out locally. To do that, simply right-click on the Spring Boot main application class within Eclipse and select Run As and Java Application from the context menus that appear.
Once the server has started, hit the main page at the following URL:
http://localhost:8090/checkouts
You should see something like this:
Keep in mind: it might take a second or so for the Payment section to appear. That’s because the code is going out to Braintree and constructing it on the fly.
As you can see, the app isn’t accepting PayPal right now. You might see something different on your screen if you’re accepting PayPal and have a linked sandbox account.
Click on Card in the payment section. You should see something that looks like this:
For the card number, enter “4111111111111111”. Yes, you can use that as a fake credit card number.
For the expiration date, enter any month and year in the future. For example: “03/25”.
Finally, click the Test Payment button. Don’t worry, you’re in the sandbox and using a fake card number so no money will actually change hands.
After some processing time, you should see a new screen that looks like this:
If you see that, then everything worked perfectly!
Testing a Client-Side Error
Next, let’s make sure that client-side error checking is working and it displays the error message to the user.
If you’re still on the success page, click on Another Transaction towards the bottom.
Once you’re back to the main checkout page, just click Test Payment without entering any credit card info.
You should see this:
That’s a pretty lame error message returned by Braintree, but at least you know that client-side validation is working.
Next, let’s look at server-side error reporting.
Testing a Server-Side Error
While you’re still on the checkout page, change the price to something that’s not a valid price. For example, enter “aaa” in the price field.
Then, enter credit card info as you did the first time you were on this page.
Finally, click Test Payment.
You should see this:
And that’s exactly what you would expect to see because “aaa” is not a valid price.
Wrapping It Up
After some more testing, you roll out your credit card processing code to production. Your Braintree solution enables people to buy your company’s products online and Smithers is happy.
Now, it’s time to do some more exploring.
Link a PayPal sandbox account and experiment with PayPal payments. Do the same for the other payment methods.
Tinker with the security options on the Braintree website. That way, you can do your part to prevent credit card fraud.
Look for refactoring opportunities and improve the code.
Remember, you can always review the entire source tree on GitHub.
Have fun!