Integrating Braintree with Angular applications.

One of the common requirements in any application is to integrate it with a Payment System. One such system is Braintree. Braintree offers a very simple solution to integrate it into your applications using the Drop-in UI mechanism. But there are challenges when you want it to integrate in an Angular application.

This article is an explanation of my reusable npm module ngx-braintree. This reusable module consists of a component, ngx-braintree which you can use to integrate Braintree in your Angular applications. It works with Angular 4.x and Angular 5.x applications.

If you want to quickly get started into using ngx-braintree component in your Angular applications, please visit ngx-braintree on npm. Follow the usage instructions and you will be good to go.

But if you want to understand how ngx-braintree component works, then read on.

You can track this project here on GitHub.

Before we start on understanding how ngx-braintree works, lets take a look at the following points:

  1. What is actually required to do to integrate Braintree into Angular applications?
  2. Why we have to do some restructuring/architecting to make it work in Angular applications

And then we will see how to integrate in an Angular way.

For an architectural understanding of how Braintree Dropin UI works, please refer the ‘How It Works’ diagram at Braintree

So let’s address the above points

What is actually required to do to integrate Braintree into Angular applications?

So, what Braintree actually wants us to do is to include their library and a small script section as shown below (taken from their site):

<head>
<meta charset="utf-8">
<script src="https://js.braintreegateway.com/web/dropin/1.9.2/js/dropin.min.js"></script>
</head>
<body>
    <div id="dropin-container"></div>
    <button id="submit-button">Request payment method</button>
    <script>
        var button = document.querySelector('#submit-button');

        braintree.dropin.create({
        authorization: 'CLIENT_AUTHORIZATION',
        container: '#dropin-container'
        }, function (createErr, instance) {
        button.addEventListener('click', function () {
            instance.requestPaymentMethod(function (requestPaymentMethodErr, payload) {
        // Submit payload.nonce to your server
        });
    });
});
</script>
</body>

Why we have to do some restructuring/architecting to make it work in Angular applications

For now, forget about the following script tag in the head section of the above code. We will come to it later.

<script src="https://js.braintreegateway.com/web/dropin/1.8.0/js/dropin.min.js"><script>

Looking at the above code raises the following questions/concerns because of which I feel restructuring/architecting is needed:

  1. Where should I include the JavaScript code section in the above code? Should I directly include it in the component’s template?
  2. Or should I move the above JavaScript code to the component’s TypeScript class?
  3. If I move it into the component’s TypeScript class, will I be able to write plain JavaScript there (as the above code is in JavaScript and not in TypeScript)?
  4. If I’m able to write plain JavaScript in the class, how can I access the library’s braintree object (braintree.dropin.create...) in the TypeScript class?
  5. Or else is there a type definition file (.d.ts) that Braintree officially provides so that I can write the above code in TypeScript itself?

Lets answer all of the above questions:

  1. Should I directly include in the component’s template itself? No, because script tags in component’s template wont work.
  2. Or should I move the above JavaScript code to the component’s TypeScript class and leave the html part in the template? Yes, Looks like a good approach
  3. If I move it into the component’s TypeScript class, and as the Braintree example is in JavaScript, will I be able to write the same plain JavaScript there? Yes
  4. How can I be sure that I’ll be able to access the Braintree library and then the braintree object (braintree.dropin.create...) in a TypeScript class? Shortly we will see how
  5. Or else, is there a type definition file (.d.ts) that Braintree officially provides so that I can code in TypeScript itself? No, Braintree officially doesn’t provide any type definition file (atleast at the time of this writing) for us to integrate using TypeScript.

This component doesn’t depend on a third party type definition file/or create a type definition file for the Braintree library. But instead uses the library officially provided by Braintree, as is.

Integrating Braintree, the Angular way.

The following is what I did:

  1. I split the JavaScript code shown above into two parts. The braintree.dropin.create... part goes into the ngOnInit of the ngx-braintree component. This is where the UI will be created as soon as the clientToken is received from Braintree.
  2. The instance.requestPaymentMethod(function (err, payload)... part into a seperate method named Pay(), that gets called when the user clicks Pay.
  3. I created a directive that can be used on the component, that adds the Braintree library whenever ngx-braintree component loads and removes it whenever the component unloads. This is how the library’s braintree object will be readily available when the ngx-braintree component comes into the scene.

So, lets look at the component code:

Note: This project is being constantly developed with new features and improvements. So, the sample code in this article may not always match with the GitHub project. But as this article is aimed at understanding how the integration of Braintree with Angular applications can be done, what all is present here should suffice to understand the integration and even to code along with. Please make note that the GitHub project has more features and all of them are not discussed here.

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { NgxBraintreeService } from './ngx-braintree.service';
declare var braintree: any;

@Component({
    selector: 'ngx-braintree',
    template: `
        <div *ngIf="showDropinUI && clientToken" ngxBraintreeDirective>
        <div id="dropin-container"></div>
            <button (click)="pay()" *ngIf="clientToken">{{buttonText}}</button>
        </div>
        <div *ngIf="clientTokenNotReceived" class="error">
            Error! Client token not received.
        </div>`,
    styles: [`
    button { 
        background-color: #009CDE;
        color: #f9f9f9; 
        border: none; 
        border-radius: 4px; 
        height: 40px;
        line-height: 40px; 
        font-size: 16px; 
        cursor: pointer; }
    .error{
        color: #D8000C;
        background-color: #FFBABA;
        border: none; 
        border-radius: 4px; 
        height: 40px;
        line-height: 40px;
    }`]
})
export class NgxBraintreeComponent implements OnInit {

@Input() clientTokenURL: string;
@Input() createPurchaseURL: string;
@Output() paymentStatus: EventEmitter<any> = new EventEmitter<any>();
clientToken: string;
showDropinUI = true;
clientTokenNotReceived = false;
interval: any;
instance: any;

// Optional inputs
@Input() buttonText = 'Buy';

constructor(private service: NgxBraintreeService) { }

ngOnInit() {
    this.service
        .getClientToken(this.clientTokenURL)
        .subscribe((clientToken: string) => {
            this.clientToken = clientToken;
            this.clientTokenNotReceived = false;
            this.interval = setInterval(() => { this.createDropin(); }, 0);
        }, (error) => {
            this.clientTokenNotReceived = true;
            console.log(`Client token not received. Please make sure your braintree server api is configured properly, running and accessible.`);
        });
    }

createDropin() {
    if (typeof braintree !== 'undefined') {
        braintree.dropin.create({
        authorization: this.clientToken,
        container: '#dropin-container'
    }, (createErr, instance) => {
        this.instance = instance;
    });
        clearInterval(this.interval);
    }
}

pay(): void {
    if (this.instance) {
        this.instance.requestPaymentMethod((err, payload) => {
        this.showDropinUI = false;
        this.service
            .createPurchase(this.createPurchaseURL, payload.nonce)
            .subscribe((status: any) => {
                this.paymentStatus.emit(status);
            });
        });
    }
}   
}

The declare var braintree: any;statement tells, that the braintree variable will be accessible at runtime (when the ngxBraintreeDirective used on this component dynamically adds the braintree library during the component load). We will look at the directive code shortly.

What we are doing in the template section is as suggested by Braintree. To create a div with an id (id="dropin-container"). This is the div in which Braintree drops a UI for the user to enter his payment details. And the button underneath it is to submit once the user has filled in the payment details.

The component has two Input properties and an Output property.

@Input() clientTokenURL: string;
@Input() createPurchaseURL: string;
@Output() paymentStatus: EventEmitter<any> = new EventEmitter<any>();

clientTokenURL – is YOUR server-side API GET URL. ngx-braintree expects the response from this URL in the following JSON format

{ "token":"braintree_client_token_generated_on_your_server" }

It is important that your API method generates and sends the token in the above mentioned JSON format for ngx-braintree to render the drop-in UI successfully. This is your server-side API GET method which calls Braintree and gets the clientToken for the Drop-in UI. ngx-braintree starts displaying the UI as soon as it receives the clientToken that your server provides.

A sample server API method that generates the clientToken in the above said JSON format, is as shown below (.NET Code).

    public class ClientToken
    {
        public string token { get; set; }

        public ClientToken(string token)
        {
            this.token = token;
        }
    }

    [Route("api/braintree/getclienttoken")]
    public HttpResponseMessage GetClientToken()
    {
        var gateway = new BraintreeGateway
        {
            Environment = Braintree.Environment.SANDBOX,
            MerchantId = "your_braintree_merchant_id",
            PublicKey = "your_braintree_public_key",
            PrivateKey = "your_braintree_private_key"
        };

        var clientToken = gateway.ClientToken.Generate();
        ClientToken ct = new ClientToken(clientToken);
        return Request.CreateResponse(HttpStatusCode.OK, ct, Configuration.Formatters.JsonFormatter);
    }

createPurchaseURL – is YOUR server-side API POST URL.
This is YOUR server-side API POST method which is called when the user clicks Pay. ngx-braintree will post the payment method nonce to the URL you provide through which you process the payment from your server and return the response. ngx-braintree posts the nonce to the URL you provide, in the following format:

{"nonce":"3252291f-b6fd-0f73-2a58-251a90d10221"}

It is important for your POST API method to be able to receive and read the above response to successfully handle the purchase.

A sample server API POST method is as shown below (.NET Code).

    public class Nonce
    {
        public string nonce;

        public Nonce(string nonce)
        {
            this.nonce = nonce;
        }
    }

    [Route("api/braintree/createpurchase")]
    public HttpResponseMessage Post([FromBody]Nonce nonce)
    {
        var gateway = new BraintreeGateway
        {
            Environment = Braintree.Environment.SANDBOX,
            MerchantId = "your_braintree_merchant_id",
            PublicKey = "your_braintree_public_key",
            PrivateKey = "your_braintree_private_key"
        };

        var request = new TransactionRequest
        {
            Amount = 899.00M,
            PaymentMethodNonce = nonce.nonce,
            Options = new TransactionOptionsRequest
            {
                SubmitForSettlement = true
            }
        };

        Result<Transaction> result = gateway.Transaction.Sale(request);
        HttpResponseMessage response = Request.CreateResponse(result);
        return response;
    }

paymentStatus – is the event that you should listen to. The paymentStatus event is emitted when a payment process finishes. The event emits the response that your purchase URL API method (createPurchaseURL) returns. Returning the same response, helps you in accessing the response object on the client side and also helps you make decisions whether to redirect user to the payment confirmation page (if the payment succeeded) or to do something else if anything went wrong.

Make sure the values of clientTokenURL and createPurchaseURL are enclosed in single quotes

And also an optional Input property:

@Input() buttonText: string = 'Buy';

buttonText – Let’s you configure the text that displays on the button.

As mentioned, the JavaScript code is split into ngOnInit() and pay(). The dropin UI is actually created in the createDropin() method which is actually called in ngOnInit() life cycle hook once the clientToken is received from the server. It’s important to notice the way I called the createDropin() method. I’m calling it via a setInterval. There is a reason to call it like this. By the time the ngOnInit() lifecycle hook is executed the braintree object may still be unavailable to the component. In other words, the directive is still in the process of adding the braintree library. And if we access the braintree object before it has been added, our component crashes and throws an error braintree is undefined. So using the setInterval mechanism like this, mimics multithreading in JavaScript. We first make sure the braintree object is available and only after that use it.

Finally, the pay() method is called when the user clicks Pay. And we then emit an event that emits the response received from your Braintree server API. The host listens to this event (paymentStatus) and can decide whether to redirect the user to payment confirmation page if the payment succeeded or perform some other action if the payment failed.

The following is the ngxBraintreeDirective that actually adds the Braintree library:

import { Directive, OnInit, OnDestroy, Renderer2, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

@Directive({
    selector: '[ngxBraintreeDirective]'
})
export class NgxBraintreeDirective implements OnInit, OnDestroy {

script: any;

constructor(private renderer: Renderer2, @Inject(DOCUMENT) private document: any) { }

ngOnInit() {
    this.script = this.renderer.createElement('script');
    this.script.type = 'text/javascript';
    this.script.src = 'https://js.braintreegateway.com/web/dropin/1.8.0/js/dropin.min.js';
    this.renderer.appendChild(this.document.body, this.script);
}

ngOnDestroy() {
    this.renderer.removeChild(this.document.body, this.script);
}

}

We make use of Renderer to add and remove the Braintree Library.

And finally, the following is the service:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class NgxBraintreeService {

    constructor(private http: HttpClient) { }

    getClientToken(clientTokenURL: string): Observable<string> {
        return this.http
        .get(clientTokenURL, { responseType: 'json' })
        .map((response: any) => {
            return response.token;
        })
        .catch((error) => {
            return Observable.throw(error);
        });
    }

    createPurchase(createPurchaseURL: string, nonce: string): Observable<any> {
        const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this.http
        .post(createPurchaseURL, { nonce: nonce }, { 'headers': headers })
        .map((response: any) => {
            return response;
        });
    }

}

Note: Notice that the getClientToken method in the above code expects the response from your server in the following JSON format. So, its important that you conform to it for the dropin UI to be successfully displayed.

{ "token":"braintree_client_token_generated_on_your_server" }

Also take note that the createPurchase method in the above code posts the nonce in a JSON format as well.

{"nonce":"3252291f-b6fd-0f73-2a58-251a90d10221"}

And finally, where ever you want to use the braintree tree component in your application you can use it as shown below:

<ngx-braintree 
    [clientTokenURL]="'your_server_get_url_that_gives_you_clientToken'" 
    [createPurchaseURL]="'your_server_post_url_that_handles_payment'" 
    (paymentStatus)="onPaymentStatus($event)"
    [buttonText]="'Pay'">
</ngx-braintree>

You can track this project here.

Any issues/features can be suggested here.

I hope this helps in integrating Braintree with your Angular applications.

Advertisements