Using Stripe with Laravel

As a freelancer, it's extremely convenient to be able to accept credit card payments and even more so when you add an online component to that. There are tons of online payment processors and services but most of them are anything but developer friendly. Convoluted logic, user-experiences, and off-site pages are just some of the common issues. Enter Stripe, an online payment processing service geared specifically towards developers. With an easy to use API, testing environments, and tons of features, Stripe is my personal first choice. Stripe can handle one-time payments, recurring payments, invoices, and more all via their API. One of my favorite features of Stripe is the fact that they will handle all of the sensitive information for you (ie credit card info) so that all of the usual security concerns that come with online payments are handled by people who really know what they're doing. Since Laravel is my current back-end framework of choice and since I recently re-worked my own Laravel/Stripe implementation, we're going to look at creating a basic one-time payment form using Stripe and Laravel. Luckily for us someone has created a Laravel specific package for the Stripe API which can be found here on GitHub. Before we jump in here are some things you'll need to tackle/handle first:

  1. Sign up for a Stripe account and generate API keys
  2. Have an existing Laravel project to modify or create a new one (instructions here)
  3. Follow the install and config instructions for the Abodeo/laravel-stripe pacakge

As a side note before you begin, this tutorial uses jQuery and v2 of the Stripe API. As such, if you wish to deviate from either of these, you'll need to adjust the logic accordingly. This also means that you'll need to include jQuery and the Stripe Checkout javascript libraries in your view.

Once you've accomplished those things, you're ready to being creating your payment page. Bear in mind that we'll be creating an extrememly simple payment form. In most cases you'll probably want something a little bit more sophisticated but for the purpose of learning this will do fine. This project has three main components (as most web forms do); the markup, front-end logic, and back-end logic. First let's take a look at our markup:

<?php
  if($submission) {
      $email = Input::get('email');
      $amount = Input::get('amount');
      $description = Input::get('description');
  } else {
      $email = '';
      $amount = '';
      $description = '';
  }
?>

<section class="payment">
  <h1>Make A Payment</h1>

  @if(!$submission || $message != '')
          <form action="" method="post" id="paymentForm">
      @unless($message == '')
              <h1 class="error">{{ $messageTitle }}</h1>
              <p class="error">{{ $message }}</p>
      @endunless
              <label for="email">Email: </label>
              <input type="email" name="email" id="email" required="required" value="{{$email}}"/>
              <label for="amount">Amount: </label>
              <input type="number" name="amount" id="amount" min="0" step=".01" required="required" value="{{$amount}}" />
              <label for="description">Description: </label>
              <input type="text" name="description" id="description" required="required" value="{{$description}}" />
              <label>&nbsp;</label>
              <button id="submitPayment">Pay with Card</button>
          </form>
  @else
          <h2 style="text-align: center;">Your payment of ${{$amount}} was successful!</h2>
  @endif
</section>

So far things are fairly simple. As you will have noticed, I'm using the Blade templating engine included with Laravel to make a few things easier. We have a basic form with three inputs and we're doing some simple checks to determine where we are in the submission process. We're also gathering or displaying certain information based upon what's happening. Next we'll have a look at our front-end logic:

function validateEmail(email) {
        var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        return re.test(email);
    }
    $(document).ready(function() {
        $('#submitPayment').click(function(e){
            e.preventDefault();
            var email = $("#email").val();
            var amount = $("#amount").val()*100;
            var desc = $("#description").val();

            if(email != '' && validateEmail(email)) {
                if($("#email").hasClass('error')) {
                    $("#email").removeClass('error');
                }
                if(amount != '' && amount > 0) {
                    if($("#amount").hasClass('error')) {
                        $("#amount").removeClass('error');
                    }
                    if(desc != '') {
                        if($("#description").hasClass('error')) {
                            $("#description").removeClass('error');
                        }
                        var token = function(res){
                            var $input = $('<input type=hidden name=stripeToken />').val(res.id);
                            $('#paymentForm').append($input).submit();
                        };
                        StripeCheckout.open({
                            key: "@stripeKey",
                            address: false,
                            amount: amount,
                            name: 'David Myers',
                            email: email,
                            description: desc,
                            panelLabel: 'Make Payment',
                            token: token,
                            image: 'img/avatar.png'
                        });
                        return false;
                    } else {
                        if(!$("#description").hasClass('error')) {
                            $("#description").addClass('error');
                        }
                        alert('You must enter a valid payment description.');
                    }
                } else {
                    if(!$("#amount").hasClass('error')) {
                        $("#amount").addClass('error');
                    }
                    alert('You must enter a valid payment amount.');
                }
            } else {
                if(!$("#email").hasClass('error')) {
                    $("#email").addClass('error');
                }
                alert('You must enter a valid email address.');
            }
        });
    });

Let's step through the main blocks of that. The first function is a regular expression used for email validation. You may have noticed that we're using an HTML5 email input field which is required. Doing so means that browsers that support it will do email validation for us. However unfortunately, not every browser supports the newer HTML5 inputs. We're then registering an event handler on our form submission button. Once that is triggered the first thing we do is prevent the default form submission behavior, which also prevents built-in browser form validation; hence why we have our own email validation. The reason we still use HTML5 inputs and require them is for two reasons. The first is because semantic expressive markup is a best practice. The second is because it gives us several layers of protection in case something goes wrong. Always better safe than sorry.

Once the form submission is triggered and we stop the default behavior, we begin to collect our form inputs and do simple validation on their values. This is all in an effort to make the process quicker for the end-user. The Stripe Checkout library that we included retrieves a Stripe transaction token for us and returns it as the global variable res. Once we finish our validation we grab that token and append it to the form then use it to open up the Stripe Checkout form. We also use the information from our initial form to populate the Checkout form for the user. Retrieving our own information before opening the Checkout form serves two purposes. The first is that it separates the payment information (cc number, exp date, etc) from their personal information. Also, it allows us to retrieve more information about the transaction than we would otherwise have. Once the user submits the Stripe Checkout form, all of the information collected is then submitted to the current page in POST request, which leads to the back-end logic.

$messageTitle = '';
$message = '';

if(Input::has('stripeToken'))
{
    $token = Input::get('stripeToken');
    if(Input::has('email'))
    {
        $email = Input::get('email');
        if(Input::has('amount'))
        {
            $amount = Input::get('amount');
            if(Input::has('description'))
            {
                $description = Input::get('description');
                try {
                    $customerList = Stripe_Customer::all();
                    $customers = $customerList->data;
                    $customer = null;

                    if(!empty($customers)) {
                        foreach($customers as $person) {
                            if($person->email == $email) {
                                $customer = $person;
                            }
                        }
                        if(empty($customer)) {
                            $customer = Stripe_Customer::create(array(
                                'email' => $email,
                                'card' => $token
                            ));
                        }
                    } else {
                        $customer = Stripe_Customer::create(array(
                            'email' => $email,
                            'card' => $token
                        ));
                    }
                    $charge = Stripe_Charge::create(array(
                        'customer' => $customer->id,
                        'amount' => ($amount*100),
                        'currency' => 'usd',
                        'description' => $description
                    ));
                }
                catch(Stripe_CardError $e) {
                    $messageTitle = 'Card Declined';
                    // Since it's a decline, Stripe_CardError will be caught
                    $body = $e->getJsonBody();
                    $err  = $body['error'];
                    $message = $err['message'];
                }
                catch (Stripe_InvalidRequestError $e)
                {
                    // Invalid parameters were supplied to Stripe's API
                    $messageTitle = 'Oops...';
                    $message = 'It looks like my payment processor encountered an error with the payment information. Please contact me before re-trying.';
                }
                catch (Stripe_AuthenticationError $e)
                {
                    // Authentication with Stripe's API failed
                    // (maybe you changed API keys recently)
                    $messageTitle = 'Oops...';
                    $message = 'It looks like my payment processor API encountered an error. Please contact me before re-trying.';
                }
                catch (Stripe_ApiConnectionError $e)
                {
                    // Network communication with Stripe failed
                    $messageTitle = 'Oops...';
                    $message = 'It looks like my payment processor encountered a network error. Please contact me before re-trying.';
                }
                catch (Stripe_Error $e)
                {
                    // Display a very generic error to the user, and maybe send
                    // yourself an email
                    $messageTitle = 'Oops...';
                    $message = 'It looks like my payment processor encountered an error. Please contact me before re-trying.';
                }
                catch (Exception $e)
                {
                    // Something else happened, completely unrelated to Stripe
                    $messageTitle = 'Oops...';
                    $message = 'It appears that something went wrong with your payment. Please contact me before re-trying.';
                }
            }
            else
            {
                $messageTitle = 'You must enter a payment description.';
            }
        }
        else
        {
            $messageTitle = 'You must enter a valid amount.';
        }
    }
    else
    {
        $messageTitle = 'You must enter a valid email address.';
    }
}
return View::make(theme_view('payment'), array(
    'submission' => true,
    'messageTitle' => $messageTitle,
    'message' => $message,
    'amount' => $amount
));

The back-end logic can either be in a controller or in a route. Our back-end logic beings by instantiating some message variables and then begins to retrieve the submitted input. Once all of the inputs have been retrieved (which is a quick process) we retrieve a list of all customers associated with our account using the Stripe API. We then loop through our existing customers to see if the current user is an existing customer or new. If they're new, we quickly create a new customer and if not we use the customer object previously returned from the API. Once we have a customer (either new or old) we create a new charge using their customer ID, the amount, and the description they entered. The customer and charge logic is wrapped in a try-catch block because the Stripe API throws exceptions on failure and does nothing on success. So we catch every possible Stripe exception to make sure we cover every possibility. Regardless of whethre or not any exceptions are thrown and caught, our logic then proceeds to return to the view with all of the pertinent information.

This leads us back to our markup. Now that you've seen the back-end logic, you see where the message variables are coming from and what they signify. All in all, using the Stripe API is extremely simple. There are quite a few things to keep track of, but overall the process is isn't complicated. Complex perhaps, but not complicated. Situations like this are the ones that make me grateful for great tools and the people that build them.

comments powered by Disqus