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.

merchantaccount

 

You’ll also need a private key and public key. You can access those by selecting API Keys under the Settings menu.

apikeys

 

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.

config

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 Streams. Then, the code can process the Streams 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:

cart

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&nbsp;# </th>
                <th width="15%"> Purchased&nbsp;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 TransactionCreditCard, 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:

checkoutscreen

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:

creditcardinfo

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:

success

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:

clientsideerror

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:

serversideerror

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!