Case Studies



How to add Braintree to your custom Kentico forms

We recently had a client who wanted a very simple donation feature for their not for profit site. They are using Kentico 11 and had not used Braintree before.

We decided to create our own custom Form Control that users could be used in the Kentico Form Builder module, see this link for the Kentico documentation on creating custom form controls, the Braintree settings go in the web.config and the bulk of the heavy lifting is done by the Braintree API and a few lines of code.

The key part to remember is that the form control gets set value at key times, once when it first loads, the next time when it is displaying the values. So use this to allow for default values for the $ amount when first loadig, but if the transaction has been successful, store details about that transaction that lets you work out that this is data being loaded, e.g. SUCCESS | Transaction Ref No: 234wsrfs | Amount $23.33

We also found that we could not make the amount variable per form, so you will need 1 form for each variable price unless you want to make this a donation page, we used a code of "DONATION" to allow this control to become variable.


What our form looked like:

The key Braintree API call:

public string runTransaction(bool validateonly = false)
    {
        DisplayClear(ref LabelResult);

        if (LabelTransactionVerfication.Text.Length > 0)
        {
            return LabelTransactionVerfication.Text;
        }
        else
        {
            string number = TextBoxCardNumber.Text;
            string cvv = TextBoxCVV.Text;
            string month = TextBoxMonth.Text;
            string year = TextBoxYear.Text;
            string name = TextBoxName.Text;
            Hiddenamount.Value = getPrice();

            if (!checkfield(number, "please provide a credit card number"))
                return "";

            if (!checkfield(cvv, "please provide a credit card cvv"))
                return "";

            if (!checkfield(month, "please provide a credit card month"))
                return "";

            if (!checkfield(year, "please provide a credit card year"))
                return "";

            if (TextBoxName.Text.Replace("•", "").Replace(" ", "").Length == 0)
            {
                DisplayErrorMessage(ref LabelResult, "Payment Failed - please provide the name of the card holder");
                return "";
            }

            string AmountPay = getPrice();

            if (!checkfield(AmountPay, "please provide correct amount"))
                return "";



            string rMsg = "";
            Boolean rBool;
            Braintree.BraintreeGateway gateway = new BraintreeGateway();

            var _with1 = gateway;
            _with1.Environment = Braintree.Environment.SANDBOX;
            _with1.PublicKey = System.Web.Configuration.WebConfigurationManager.AppSettings["BraintreePublicKey"];
            _with1.PrivateKey = System.Web.Configuration.WebConfigurationManager.AppSettings["BraintreePrivateKey"];
            _with1.MerchantId = System.Web.Configuration.WebConfigurationManager.AppSettings["BrainTreeMerchantID"];

            Result<Transaction> result;
            if (validateonly)
            {
                return "validdata";
            }
            else
            {
                Decimal amount;
                amount = Convert.ToDecimal(AmountPay);

                if (amount <=0)
                {
                    DisplayErrorMessage(ref LabelResult, "Minimum amount is 1 cent");
                    return "";
               }
            

                TransactionRequest transactionRequest = new TransactionRequest();



                var _with3 = transactionRequest;
                _with3.Amount = amount;
                _with3.PaymentMethodNonce = "nonce-from-the-client";
                _with3.CreditCard = new TransactionCreditCardRequest();

                var _with2 = _with3.CreditCard;
                _with2.Number = number;
                _with2.CVV = cvv;
                _with2.ExpirationMonth = month;
                _with2.ExpirationYear = year;
                _with2.CardholderName = name;
                result = gateway.Transaction.Sale(transactionRequest);
            }

            rMsg = result.Message;
            rBool = result.IsSuccess();

            if (rBool)
            {
                DisplaySuccessMessage(ref LabelResult, "Success " + rMsg);
                //show payment was successfull 
                TransactionSuccessful = true;
                transactionID = result.Target.Id;

                CollectPayment.Visible = false;
                DivSuccessfulPayment.Visible = true;

                Result<Transaction> result2 = gateway.Transaction.SubmitForSettlement(transactionID);

                if (result2.IsSuccess())
                {
                    Transaction settledTransaction = result2.Target;
                }
                else
                {
                    Console.WriteLine(result2.Errors);
                }

                LabelTransactionVerfication.Text = "Successful Transaction:" + transactionID + " - $" + AmountPay + " - xxxx xxxx xxxx " + number.Substring(number.Length - 5, 4);

                return LabelTransactionVerfication.Text;
            }
            else
            {
                DisplayErrorMessage(ref LabelResult, "Payment Failed " + rMsg);
                return "";
            }

        }
    }



if you would like a copy of the actual code, let me know, support@formition.com 





Case Studies



We encountered an issue that was quite difficult to track down and wanted to share what we learnt

In our case, we had a prototype site with .net core 2.2, react working, but we found on some browsers user tests failed, where others were fine (Windows 10  and Chrome 77+ and Samsung 9 and 10 Chrome and Safari on iPhone X)

But for iPhone 8 (Safari), Samsung 6 (Internet) and other legacy browsers such as IE 11 and EDGE we had a major problem. Users could not log in and got caught in a login, redirect loop back to the login page.

After debugging this we realised that in some cases the .net auth cookies do not get sent back to the server in a fetch request. The cookies were being set fine, but they were not being included in the post back so subsequent posts back on other pages, the user was treated and not logged in and sent back to the login page.

Using Fetch in legacy applications is tricky because in some cases the Fetch feature is native to the browser and the behaviour is just different. We found the one trick to make the credentials cookie pass back to the web service:

 credentials: 'same-origin'

 

We had to add the above to every fetch request throughout our system to cater for legacy browsers.

//authenticate user
        fetch('api/Login/AuthenticateUser', {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',                
            },
            credentials: 'same-origin',
            body: JSON.stringify(datax)
        }).then(response => response.json())
            .then(data => { use data here }
            );

 

in other more simpler fetch requests it just looks like this:

fetch('api/Login/IsAuthenticated', { credentials: "same-origin" })
            .then(response => response.json())
            .then(data => {
                console.log('isAuthenticated:' + JSON.stringify(data));
                if (data.message === 'true') { this.setState({ isAuthenticated: true, username: data.dataobject.username }); }
                else {
                    this.setState({ isAuthenticated: false, username: '' });
                }
            });



Our authentication setup incase you want to validate yours.

 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options => {
            options.LoginPath = "/";
            // Cookie settings
            options.Cookie.HttpOnly = true;
            options.ExpireTimeSpan = TimeSpan.FromDays(30);
            options.SlidingExpiration = true;
            options.LoginPath = "/Login/";
            options.AccessDeniedPath = "/AccessDenied";
            options.SlidingExpiration = true;
        });


It is also recommended through other sites I found to set cookie option is essential to true, incase any browsers have a requirement that there must be permission first, but as yet we have not tested this

options.Cookie.IsEssential = true;


This took me days to identify and fix and I wanted to help anyone else out who is looking at this problem specifically.




Top