stymiee

Handling Online Payments Part 8 - Using JavaScript To Increase Usability

by All Star ‎04-21-2011 06:30 AM - edited ‎10-20-2011 09:57 AM (10,911 Views)

This is part eight of a multi-part series on handling online payments.

 

In Part 1 of this series we identified our goals (creating a payment form that was usable, accessible, and secure) and began by creating the form we will use to capture payment information. In Part 2 of this series we continued this process by exploring how we will handle the data submitted by that form. In Part 3 of this series we took the data received and sanitized in Part 2 and validated that it was in a format we required. In Part 4 of this series we took the errors we found in Part 3 and displayed them in a user-friendly format to minimize cart abandonment. In Part 5 of this series we processed the payment and handled the response returned to us whether it be approved, declined, or an error completing the transaction process.

 

In Part 6 of this series we changed our focus to improving upon our form to make it more user-friendly, secure, and easier to maintain. We accomplished this by preventing duplicate form submissions using the POST/REDIRECT/GET design pattern. In Part 7 of this series we prevented spambots and other malware from making automated form submissions by using a honeypot and form token. We will build upon this by using JavaScript to enhance the usability of our form making it easier for our users to complete properly.

 

Enhancing Our Form With JavaScript

 

Technically speaking our form is fully functional. It displays a form that collects all of the information we need, it verifies all of the information is correct and also not malicious, displays helpful error messages if there is an error, processes our payment if there are none, and allows us to take the user to a thank you or receipt page after a successful submission and payment. That probably is "good enough" for many businesses or non-profits. But we know we can do better then that and did so by preventing pesky duplicate form submissions when a user uses the back button and preventing spammers and other malicious users from abusing our form.

 

In this blog post we are going to shift our focus back to the user and try to improve their experience using our form with JavaScript. JavaScript is great for enhancing the user experience because it not only allows us to provide immediate results, since JavaScript is executed directly in the browser so no client to server connection is required, but if JavaScript is not supported by the user their experience isn't impacted at all. They simply just miss out on some helpful, but not required, tools to help them complete the form.

 

So how are we going to use JavaScript to enhance our form? There are two tasks I think JavaScript can perform that will make our form easier to use. They are:

 

  • Copy the billing address fields to the shipping address fields

     

    The more complicated a form is the less likely a user is to complete it. Naturally this is very counterproductive especially with a payment form. We will do our best to simplify the completion of our form by allowing users to copy their billing address to the shipping address fields with a single click of their mouse.

     

  • Add and remove error messages dynamically

     

    We can improve our communication with our users by providing instant feedback on the data they are entering into the form rather then waiting for them to submit their form. JavaScript allows us to do this by displaying error messages as soon as they are finished completing a form field.

 

In our JavaScript examples will be using the Prototype JavaScript Framework to make writing our JavaScript easier, the code snippets shorter, and make cross-browser compatibility a non-issue. If you use jQuery and would like to contribute a jQuery alternative to the Prototype code written below please send me a private message and I will provide you with my email address where you can send it.

 

Including the Prototype Framework In Our Payment Form

 

Before we can write any JavaScript that utilizes the Prototype JavaScript Framework we must include it in our HTML before any JavaScript that uses it. There are two ways you can include Prototype into your webpage:

 

  1. Download the framework and load it from your website

     

    You can download the Prototype JavaScript Framework directly from the Prototype website. To include this file in your code you can use the following code (this is assuming you have placed this file in a directory called "javascripts" in your root web directory and saved the Prototype Framework in a file called "prototype.js"):

     

    <script type="text/javascript" src="/javascripts/prototype.js"></script>
            
  2. Use Google's hosted JavaScript libraries

     

    In an effort to make web pages load faster Google offers hosted versions of the popular JavaScript framework and libraries including Prototype. Not only are their servers fast and always available, but by using their hosted version you make your web page load faster potentially in two ways. Web browsers will only fetch a limited number of resources at a time from one domain. So if you have many files (e.g. JavaScript, CSS, images, etc) to download for any one given page your users have to wait while the browser downloads them in groups. By loading the Prototype JavaScript file from Google you are loading the file from a different domain and as a result the browser will download it in tandem with files it downloads from your domain. Additionally because browsers cache files based on URL if the users has visited a website that also uses Google's hosted version of Prototype then the user's browser will use the cached copy on the user's hard drive and avoid downloading the file altogether which is as fast as you can get.

     

    To include the file from Google's servers you simply need to link to it in your HTML using this code (this is current as of Prototype version 1.7):

     

    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"></script>
            

     

    We will use Google's hosted version in our sample form.

 

A best practice when including JavaScripts in a web page is to place them at the bottom of the web page after the page's content. This is due to JavaScript blocking the downloading of other content until the JavaScript is fully downloaded and processed (this is called "blocking"). So we will place our <script> tags just before the closing <body> tag of our web page:

 

        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"></script>
    </body>
</html>

 

Copying The Billing Address Fields To The Shipping Address Fields

 

I don't think anyone reading this blog post will disagree that the easier something is to do the more likely someone is to do it. Completing a form on a website is no exception. It has been proven over and over again that complicated forms have a higher rate of abandonment then simpler forms. Between an increased opportunity for confusion and pure laziness complicated forms act as a deterrent for a user attempting to complete it. Simpler forms, on the other hand, have a lower rate of abandonment. Therefore our goal is to make our form as simple as possible. Of course we can't just eliminate form fields for the sake of simplicity because our business rules dictate that must capture that information in order to successfully complete the transaction (make payment, ship the order, etc). So our goal is to make our current form easier to use.

 

How do we do that? By making opportunities for users to complete form fields with less effort on their part. In our form we have two places where we collect an address: the billing and shipping addresses. It is very common for these addresses to be identical as many users will have an item shipped to their home address which is almost always the billing address of their credit card. We can potentially eliminate the need to complete seven form fields if we can allow the user to copy their billing address information to the shipping address fields.

 

Fortunately this is an easy job for JavaScript. We will use JavaScript to add a checkbox to our form that offers the user an opportunity to copy their billing information to the shipping fields. If that checkbox is clicked our JavaScript will then copy the billing information to the shipping fields. If the checkbox is unchecked it will clear the shipping fields of the billing information but only if the information has not changed.

 

Let's see the code first and then break it down into its core components:

 

document.observe('dom:loaded', function() {
    // The paragraph that contains the "email" form field
    var email_p = $('email').up('p');

    // Create the <p> tag that will contain our checkbox
    var p = new Element('p');

    // Create our checkbox
    var checkbox = new Element('input', {'type': 'checkbox', 'id': 'copybilling'});

    // Append the checkbox to the new <p>
    p.appendChild(checkbox);

    // Create our text explaining what this checkbox does
    var b = new Element('b').update('Copy billing address information to the shipping address');

    // Append the checkbox to the new <p>
    p.appendChild(b);

    // Create our text explaining what this checkbox does
    // Insert the new <p> tag after the paragraph with the "email" field
    Element.insert(email_p, {'after': p});

    // When the checkbox is clicked either copy or clear the shipping form fields
    $('copybilling').observe('click', function() {
        if ($('copybilling').checked == true) {
            // The checkbox is checked, copy the information over
            $('recipient_first_name').value = $F('cardholder_first_name');
            $('recipient_last_name').value  = $F('cardholder_last_name');
            $('shipping_address').value     = $F('billing_address');
            $('shipping_address2').value    = $F('billing_address2');
            $('shipping_city').value        = $F('billing_city');
            $('shipping_state').value       = $F('billing_state');
            $('shipping_zip').value         = $F('billing_zip');
        }
        else {
            // The checkbox is unchecked, remove any unchanged fields
            if ($('recipient_first_name').value == $('cardholder_first_name').value) {
                $('recipient_first_name').value = '';
            }
            if ($('recipient_last_name').value == $('cardholder_last_name').value) {
                $('recipient_last_name').value = '';
            }
            if ($('shipping_address').value == $('billing_address').value) {
                $('shipping_address').value = '';
            }
            if ($('shipping_address2').value == $('billing_address2').value) {
                $('shipping_address2').value = '';
            }
            if ($('shipping_city').value == $('billing_city').value) {
                $('shipping_city').value = '';
            }
            if ($('shipping_state').value == $('billing_state').value) {
                $('shipping_state').value = '';
            }
            if ($('shipping_zip').value == $('billing_zip').value) {
                $('shipping_zip').value = '';
            }
        }
    });
});

 

We start by wrapping our code in Prototype's observe() function. This function allows us to watch for events to happen in our document and execute some code when it occurs. In our case we are using Prototype's built in dom:loaded event to wait for the web page to be downloaded and parsed but not yet displayed by the browser. This allows us to make changes to the page after everything is loaded but before the user sees it.

 

Our first block of code creates and places the checkbox, which is wrapped in a <p>, in our form between the email text field and the recipient's first name field. We want to place this just above the shipping information as this is when the user will most likely find this feature useful. The reason we create this HTML dynamically using JavaScript as opposed to hard coding it into the form to begin with is we only want users with JavaScript to see it since it is powered by JavaScript. By using JavaScript to add this HTML for us we know that users who have disabled JavaScript in their browser will not see a checkbox that they can't use.

 

The second block of code is also wrapped inside Prototype's observe() function. This time we're observing the checkbox we created to see if it has been clicked. We then check the state of the checkbox to determine what our action will be. If the checkbox is checked we copy the billing information into the shipping form fields. If the checkbox is unchecked we erase any shipping form fields the user has not changed. The reason we don't erase them completely is we don't want to potentially erase information the user may want to keep. That's contradictory to our goal of making the form easier to complete.

 

Add And Remove Error Messages Instantly

 

Users are going to make mistakes while completing our form. This might be because they made a typo, misunderstood what information they need to provide, or just can't follow simple directions. You can attempt to create the perfect form but no matter what you do there will be problems with user input. That's why we validate that information when the form is submitted and display a helpful error message when we find a problem. However, this requires a round trip to the server and back which can be slow, and if the user repeatedly has problems, can be very tiresome and annoying enough for them to abandon the form completely.

 

We can minimize the frustration of the user by speeding up the notification of errors. We can also confirm they have provided correct information by showing a confirmation of some kind indicating it is correct. JavaScript allows us to validate the data while the users is completing the form giving them instant feedback as to whether their information is valid or erroneous. That way not only are they instantly notified of their error and can correct it immediately but they are spared the trouble of having to wait for the form to be submitted and processed possibly multiple times before they get it right.

 

Let's take a look at the JavaScript we will use for this and then break it down into parts for review.

 

function luhn(num) {
    num = (num + '').replace(/\D+/g, '').split('').reverse();
    if (!num.length) {
        return false;
    }
    var total = 0, i;
    for (i = 0; i < num.length; i++) {
        num[i] = parseInt(num[i])
        total += i % 2 ? 2 * num[i] - (num[i] > 4 ? 9 : 0) : num[i];
    }
    return (total % 10);
}

function validCreditCard(card_number) {
    var firstnumber = parseInt(card_number.substr(0,1));
    switch (firstnumber)
    {
        case 3:
            if (!card_number.match(/^3\d{3}[ \-]?\d{6}[ \-]?\d{5}$/)) {
                return false;
            }
            break;
        case 4:
            if (!card_number.match(/^4\d{3}[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/)) {
                return false;
            }
            break;
        case 5:
            if (!card_number.match(/^5\d{3}[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/)) {
                return false;
            }
            break;
        case 6:
            if (!card_number.match(/^6011[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}$/)) {
                return false;
            }
            break;
        default:
            return false;
    }
    return luhn(card_number);
}

function changeLabelColor(ele, state) {
    switch (state) {
        case 'error' :
            ele.previous('label').addClassName('labelerror');
            ele.previous('label').removeClassName('labelvalid');
            break;
        case 'valid' :
            ele.previous('label').addClassName('labelvalid');
            ele.previous('label').removeClassName('labelerror');
            break;
        case 'clear' :
            ele.previous('label').removeClassName('labelvalid');
            ele.previous('label').removeClassName('labelerror');
    }
}

document.observe('dom:loaded', function() {
    $('credit_card').observe('change', function() {
        if (!validCreditCard($F('credit_card').strip()) && $F('credit_card').strip().length != 0) {
            changeLabelColor($('credit_card'), 'error');
        }
        else if (validCreditCard($F('credit_card').strip())) {
            changeLabelColor($('credit_card'), 'valid');
        }
        else if ($F('credit_card').strip().length == 0) {
            changeLabelColor($('credit_card'), 'clear');
        }
    });
    ['expiration_month','expiration_year'].each(function(f) {
        $(f).observe('change', function() {
            if ($F('expiration_month') != '0' && $F('expiration_year') != '0') {
                changeLabelColor($('expiration_month'), 'valid');
            }
            else if ($F('expiration_month') == '0' && $F('expiration_year') == '0') {
                changeLabelColor($('expiration_month'), 'clear');
            }
        });
    });
    $('cvv').observe('change', function() {
        var regex = /^\d{3,4}$/;
        if (!$F('cvv').strip().match(regex) && $F('cvv').strip().length != 0) {
            changeLabelColor($('cvv'), 'error');
        }
        else if ($F('cvv').strip().match(regex)) {
            changeLabelColor($('cvv'), 'valid');
        }
        else if ($F('cvv').strip().length == 0) {
            changeLabelColor($('cvv'), 'clear');
        }
    });
    $('email').observe('change', function() {
        var email_regex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2}|(com|net|org|edu|int|mil|gov|arpa|biz|aero|name|coop|info|pro|museum))$/;
        if (!$F('email').match(email_regex) && $F('email_address').length != 0) {
            changeLabelColor($('email'), 'error');
        }
        else if ($F('email').match(email_regex)) {
            changeLabelColor($('email'), 'valid');
        }
        else if ($F('email').length == 0) {
            changeLabelColor($('email'), 'clear');
        }
    });
    ['billing_zip','shipping_zip'].each(function(f) {
        $(f).observe('change', function() {
            var regex = /^\d{5}$/;
            if (!$F(f).match(regex) && $F(f).length != 0) {
                changeLabelColor($(f), 'error');
            }
            else if ($F(f).match(regex)) {
                changeLabelColor($(f), 'valid');
            }
            else if ($(f).length == 0) {
                changeLabelColor($(f), 'clear');
            }
        });
    });
    ['billing_state','shipping_state'].each(function(f) {
        $(f).observe('change', function() {
            if ($F(f) != 0) {
                changeLabelColor($(f), 'valid');
            }
            else if ($F(f) == 0) {
                changeLabelColor($(f), 'clear');
            }
        });
    });
    ['cardholder_first_name','cardholder_last_name','billing_address','billing_city','telephone','recipient_first_name','recipient_last_name','shipping_address','shipping_city'].each(function(f) {
        $(f).observe('change', function() {
            if ($(f).value.strip().length != 0) {
                changeLabelColor($(f), 'valid');
            }
            else if ($(f).value.strip().length == 0) {
                changeLabelColor($(f), 'clear');
            }
        });
    });
});

 

We start off by defining three functions that our validation code will be using. The first two, luhn() and validCreditCard(), are used to validate the credit card number. You might recognize them as being similar to the ones we wrote in PHP in Part 1 of our credit card validating series. All we have done is ported them over so our JavaScript code can use them. The third function changeLabelColor() is a helper function that makes displaying error and success messages easier. If you remember when an error is discovered during server side validation with PHP we changed the color of the form field's label to red to make it easy for them to spot when there is an error and where it is. This function not only does the same for us instantly but also changes the color of the label to green if a correct value is entered. This reinforces that a correct value has been entered and that it is ok to submit the form for processing. The reason we separate this code out into a function is it allows us to reuse that code and keep the remaining JavaScript clearer and easier to manage.

 

Our second block of code once again is wrapped up in Prototype's observe() function since we want to wait for the page to be loaded before we process our JavaScript. Inside of this function we have set up observers for our form fields so we can monitor their content and update the color of the label appropriately. We start by monitoring the credit card number field and making sure it is in a proper format and passes the Luhn algorithm. We then check to make sure the expiration date fields have values chosen and that the CVV number is three or four digits depending on card type (Visa, American Express, etc). We then make sure information that needs to be in a special format (i.e. email addresses, zip codes) are indeed in a proper format and we verify that billing and shipping states are chosen. We then use a simple check to verify that the remaining required fields have values submitted although no format is specified.

 

Adding Our JavaScript To Our Form

 

Now that we have our JavaScript written we need to append it to our web page. We will place it at the bottom of our page immediately after our call to the Prototype Framework. We will also place each snippet in a separate file so we can keep our code clean and take advantage of browser caching to speed up our website. We'll put the JavaScript that copies the billing information to the shipping fields in a file called payment-form.js and place it in a directory called /javascripts. We will call our data validation JavaScript validation.js and place it in the same directory. We then can call both scripts using the code below:

 

        </form>
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js"></script>
        <script type="text/javascript" src="/javascripts/payment-form.js"></script>
        <script type="text/javascript" src="/javascripts/validation.js"></script>
    </body>
</html>

 

Updated Payment Form

 

To see our new form with our new JavaScript and CSS enhancements included just download the attachment at the end of this blog post.

 

What's Next?

 

Our payment form is better then ever thanks to our new JavaScript enhancements. Completing the form is easier and more intuitive which makes our users more likely to complete the form and reduce our rate of abandonment. In the next part of our series we will use HTML and CSS to improve the appearance of our form to make it even easier to use.

 

The Handling Online Payments Series

 

  1. Part 1 - Basic Information and Our Form
  2. Part 2 - Reading In And Sanitizing Submitted Data
  3. Part 3 - Data Validation
  4. Part 4 - Handling Validation Errors
  5. Part 5 - Processing Payment and Handling the Response
  6. Part 6 - Preventing Duplicate Submissions with POST/REDIRECT/GET
  7. Part 7 - Preventing Automated Form Submissions
  8. Part 8 - Using JavaScript To Increase Usability
  9. Part 9 - HTML and CSS Enhancements
  10. Part 10 - A Little Bit More PHP
---------------------------------------------------------------------------------------------------


John Conde is a certified Authorize.Net developer

Comments
by advernut on ‎04-26-2011 10:24 AM

Great stuff! Thanks for posting this series.  I have a question though.  I am collecting the form data to my database, but I am having trouble capturing the transaction_id.  I think the problem is that the transaction_id is generated after the form data is submitted. I have tried grabbing $transaction_id and including it in my INSERT statement with no luck. Could you please clue me in on want I'm doing wrong?

 

Thanks,

Jeff

by All Star on ‎04-26-2011 10:50 AM

Jeff,

 

I'm not sure why you aren't getting a value for the transaction ID in your database. If you want to troubleshoot it try these steps:

 

1) Print out the transaction response and verify the transaction ID is present and confirm its value:

 

$response = $transaction->authorizeAndCapture();
print_r($response); 

2) Confirm the variable is spelled correctly in the code that generates your SQL

 

3) Print out your INSERT statement and make sure the value is present

 

4) Make sure you have the field that stores the transaction ID in your database set to be a VARCHAR as it is too large to be an INT. You can also use any numeric datatype that holds numbers longer then an INT. BIGINT should work with MySQL.

 

Hopefully these tips help you figure it out.

 

John

by advernut on ‎04-26-2011 01:53 PM

John - Thank you so much for responding to my post so quickly.  The database was in fact set to int(11), which I changed to VARCHAR.  Unfortunately it's still not working.  I completed your suggested steps and still no luck. 1) - printed the response and verified that the transaction ID is present - it is present.  2) I did a Find and Replace on the word transaction_id and found all instances.  So spelling is correct.

 

 3) I even printed $transaction_id right before my INSERT statement and that worked too.  I tried using  $transation_id as the value in my SQL, but that didn't work either.  

 

I feel like I am so close to solving this, but there is something I'm not seeing, which is usually the case.  Thanks for the clues you shared so far.
Jeff

 

by advernut on ‎04-26-2011 02:31 PM

John - If I'm capturing the transaction ID to insert into my database, shouldn't that be part of the FORM?  I currently have it as a HIDDEN field.  Like this:

 

<INPUT TYPE="HIDDEN" NAME="transaction_id" VALUE="<?php if (isset($transaction_id)) echo $transaction_id; ?>"

 

Or should I just be assigning $transaction_id to the VALUES section of my SQL statement?  I've tried it many different ways already and nothing works.

 

Jeff

 

 

 

 

by All Star on ‎04-27-2011 09:36 AM

Jeff,

 

It doesn't need to be in your form since it is being collected after the form is submitted. It should definitely be in your SQL statement. When you echo out your SQL statement do you see any value in there? 

 

John

by advernut on ‎04-27-2011 10:46 AM

John - I echoed out the SQL statement and the value is NULL. I set the field that way while i'm testing, so the ID is definitely not being passed to the SQL statement.  

When I echo out $transation_id I get the correct value.  I just can't figure out how to pass it to my SQL statement.

 

Thanks,

Jeff

by advernut on ‎04-27-2011 12:20 PM

John - I finally got it to work, but a had to create a separate UPDATE statement to run after the form INSERT statement.  

Here's what I did:

 

 

Spoiler
// Transaction approved!
header('Location: thank-you-page.php?transaction_id=' . $response->transaction_id);
mysql_query("UPDATE payments SET transaction_id='$transaction_id'");
exit;

 

I could probably place the UPDATE to run right after the INSERT statemant and I still may do that.  I tried it after the redirect header to see if it would work and it did.  I think I'm on may way to finishing this up now.  Thank you for all your help.  It was much appreciated!

 

Jeff

by darkwind on ‎12-06-2012 11:10 PM

Is this compatible with IE? I hope it is.

 

I'm getting no errors when I don't fill out some of the fields, it just redirect back to the page.

 

When I fill out everything, it gives me a blank page.

 

The errors are:

 

Message: 'null' is null or not an object
Line: 110
Char: 9
Code: 0
URI: javascripts/validation.js


Message: 'null' is null or not an object
Line: 25
Char: 5
Code: 0
URI: javascripts/payment-form.js

 Thanks in advance!

About the Author
  • Authorize.Net Developer Community Manager
Announcements
Labels