Skip to content

Instantly share code, notes, and snippets.

@kimus
Created October 24, 2013 13:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kimus/7136895 to your computer and use it in GitHub Desktop.
Save kimus/7136895 to your computer and use it in GitHub Desktop.
How to use PAYMILL with Squarespace

How to use PAYMILL with Squarespace

What follows is not a step-by-step guide for using PAYMILL. You will need a some knowledge of Javascript (inc. jQuery), a bit of PHP and preferably some experience with cross domain requests. Additionally, you will have to upload files to Heroku, which requires Git and terminal commands, or any other service that you can host the PHP files.

Let's look at a brief overview of the process:

  • The User fills in their credit card details in their browser;
  • The PAYMILL JS library generates a token on PAYMILL's servers and hands it back to the browser;
  • The browser sends this token to our own server;
  • Our server uses the PAYMILL server-side library to submit the charge to PAYMILL;
  • Our server sends a response back to the browser.

The reason for this slightly convoluted method, is that the user's credit card details never touch our server, sparing us from a great many security concerns.

Squarespace Injection Code

Footer:

<script type="text/javascript">
    Y.use('node', function(Y)
    {
        Y.on('domready', function()
        {
            // check if we are in the shopping cart page
            var chkout = Y.one(".checkout-button");
            if (chkout)
            {
                // remove default events from button
                chkout.unsubscribeAll();
                chkout.on('click', function() 
                {
                    window.location.replace('/shop-checkout');
                });
            }
        });
    });
</script>

This code will disable the default Checkout button and redirects to our custom checkout page form.

The Form

You now Could create a new NOT-LINKED page and call it "Shop Checkout" (just make sure the link is /shop-checkout).

After adding a page you can click in ADD BLOCK button and choose the "CODE" option.

Add the following:

<div class="sqs-block form-block">
    <div class="sqs-block-content">
        <div class="form-wrapper">
            <div class="form-inner-wrapper payment-form-container">
                <form id="payment-form" action="">
                    <div class="sqs-checkout-form-section">
                        <fieldset>
                            <div class="field-list clear">
                                <div class="form-item field required" style="float:left; width:65%; margin-right:10px; ">
                                    <label class="title">Card Number <span class="required">*</span></label>
                                    <div style="position:relative">
                                        <input class="field-element" id="card-number" type="text" size="20" name="number"/>
                                        <img class="card-type-logo" style="position: absolute; right: 20px; top: 10px;"/>
                                    </div>
                                </div>
                                <div class="form-item field required" style="float:right; width:30%">
                                    <label class="title">CVC <span class="required">*</span></label>
                                    <input class="field-element" type="text" size="4" name="cvc" />
                                </div>
                                <div class="form-item field required">
                                    <label class="title shipping-option-label">Expiration Date (MM/YYYY)</label>
                                    <input class="field-element" style="width:16%;" type="text" size="2" name="exp_month" />
                                    <span class="field-element" style="background:none; border:none;"> / </span>
                                    <input class="field-element" style="width:30%;" type="text" size="4" name="exp_year" />
                                </div>
                                <div class="form-item field required">
                                    <label class="title shipping-option-label">Holder Name <span class="required">*</span></label>
                                    <input type="text" name="cardholder" class="field-element" />
                                </div>
                                <div class="form-item field required">
                                    <label class="title shipping-option-label">Email Address <span class="required">*</span></label>
                                    <input type="text" name="email" class="field-element" />
                                </div>
                            </div>
                            <p class="payment-errors error-summary" style="display:none;"></p>
                            <div class="form-list clear">
                                <div class="form-item field sqs-pill-shopping-cart">
                                    <input type="hidden" id="amount-field" name="amount_int" />
                                    <input type="hidden" name="currency" value="EUR" />

                                    <div class="subtotal" style="top:0">
                                        <div class="label">Amount</div>
                                        <div class="sqs-money-native price"></div>
                                    </div>
                                </div>
                            </div>                                  
                        </fieldset>
                        
                        <div class="clear">
                            <input type="submit" class="button paymill-checkout-button" style="float:left;" disabled="disabled" value="Submit Payment" />
                            <div class="wait" style="float:left;height:44px;width:44px"></div>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

It's basic HTML, so I won't go over it.

The Javascript

Includes:

<script type="text/javascript">
	var PAYMILL_PUBLIC_KEY = '<YOUR_PUBLIC_KEY>';
</script>
<script src="https://bridge.paymill.com/"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="http://fgnass.github.io/spin.js/dist/spin.min.js"></script>
<script type="text/javascript">OUR SCRIPT HERE</script>

First off we set our public key (found in PAYMILL account settings). Make sure to use the testing key for the moment.

Next, we load the PAYMILL bridge library, jQuery, Spin (spinning wheel) and Our script.

Our Script

<script type="text/javascript">
    $(document).ready(function()
    {
        // check if we are in the checkou custom page
        var chkout = $(".paymill-checkout-button");
        if (chkout.length > 0)
        {
            $.ajax({
                url: '/api/open/GetShoppingCart', 
                success : function (data) 
                {
                    // hide shopping cart
                    $('.sqs-widget.sqs-pill-shopping-cart').hide();

                    // get shopping cart amount value
                    var amount_int = data.shoppingCart.grandTotalCents / 100;
                    
                    // set the amount field value
                    $("#amount-field").val(Number(amount_int).toFixed(2));
                    $("#payment-form .subtotal .price").text(Number(amount_int).toFixed(2));


                    $('.paymill-checkout-button').removeAttr("disabled");
                }
            });
        }

        function showError(fId, errmsg)
        {
            var fld = $("#payment-form").find('[name=' + fId + ']');

            fld.parent().addClass('error');

            var err = $('<div class="error-summary"></div>').insertAfter(fld);
            err.text(errmsg);
        }

        function clearErrors(form)
        {
            form.find('.form-item').removeClass('error');
            form.find('.form-item .error-summary').remove();
            $(".payment-errors").css("display", "none");
        }
        
        function validate(name, value, value2)
        {
            var valid = true;

            switch(name)
            {
                case 'number':
                    valid = (value !== '' && paymill.validateCardNumber(value));
                    break;

                case 'exp_month':
                case 'exp_year':
                    name = 'exp_year';
                    valid = (value !== '' && value2 !== '' && paymill.validateExpiry(value, value2));
                    break;

                case 'cvc':
                case 'cardholder':
                case 'email':
                    valid = (value !== '');
                    break;

                default:
                    break;
            }

            if (!valid)
            {
                showError(name, 'Please correct this field');
            }

            return valid;
        }

        $("#payment-form").find('.form-item input').on('blur', function()
        {
            var form = $("#payment-form");
            clearErrors(form);
            
            var fld = $(this);
            var n = fld.attr('name');
            var v = fld.val();
            validate(n, n == 'exp_year' ? form.find('[name=exp_month]').val() : v, v);
        });

        $("#payment-form").submit(function (event)
        {
            // Disable submit Button
            $('.paymill-checkout-button').attr("disabled", "disabled");

            clearErrors($(this));

            // add a spinning wheel
            var spinner = new Spinner({lines: 14, length: 5, width: 2, radius: 8, color:'#666'}).spin($('.wait').get(0));


            // serialize all the data in the form to JSON
            var params = {};
            var fields = $(this).serializeArray();
            var valid = true;

            $.each(fields, function(idx, field)
            {
                var n = field.name;
                var v = field.value;

                params[n] = v;

                valid = validate(n, n == 'exp_year' ? params.exp_month : v, v);
                return valid;
            });

            if (!valid)            
            {
                $(".payment-errors").css("display", "inline-block");
                $(".paymill-checkout-button").removeAttr("disabled");

                spinner.stop();
            }
            else
            {
                params.amount_int = params.amount_int * 100; // E.g. "15" for 0.15 Eur

                paymill.createToken(params, function(error, result)
                {
                    if (error) 
                    {
                        // Shows the error above the form
                        $(".payment-errors").text(error.apierror);
                        $(".payment-errors").css("display", "inline-block");

                        $(".paymill-checkout-button").removeAttr("disabled");
                        
                        spinner.stop();
                    }
                    else
                    {
                        // store the token
                        params['paymillToken'] = result.token;
                        
                        // don't send this info to the server
                        delete params.number;
                        delete params.cvc;
                        delete params.exp_month;
                        delete params.exp_year;

                        $.ajax({
                            url: 'https://YOUR-SITE-ID.heroku.com/paymill.php',
                            type: 'POST',
                            data: JSON.stringify(params),
                            success: function(data)
                            {
                                if (data.status == 'error')
                                {
                                    $(".payment-errors").text(data.message);
                                    $(".payment-errors").css("display", "inline-block");
                                    $(".paymill-checkout-button").removeAttr("disabled");
                                }
                                else
                                {
                                    $('.payment-form-container').html("<h2>We appreciate your purchase!</h2>");
                                    
                                    // handle the data from server if needed
                                    
                                    $(".submit-button").removeAttr("disabled");
                                }
                                
                                spinner.stop();
                            },
                            error: function()
                            {
                                $(".payment-errors").text("unknown error");
                                $(".payment-errors").css("display", "inline-block");
                                $(".paymill-checkout-button").removeAttr("disabled");

                                spinner.stop();
                            }
                        });
                    }
                });

            }

            return false;
        });


        // add an image of the card number type on user typing
        $("input#card-number").on('keyup', function()
        {
            var type, i;
            var types = [
                {length: [12, 13, 14, 15, 16], matcher: /^(5018|5020|5038|6304|6759|676[1-3])/, name: "maestro"},
                {length: [16], matcher: /^5[1-5]/, name: "mastercard"},
                {length: [13, 14, 15, 16], matcher: /^4/, name: "visa"},
                {length: [15], matcher: /^3[47]/, name: "amex"},
                {length: [14], matcher: /^(36|38|30[0-5])/, name: "dinersclub"},
                {length: [16], matcher: /^(6011|65|64[4-9]|622)/, name: "discover"},
                {length: [16, 17, 18, 19], matcher: /^62/, name: "unionpay"},
                {length: [16], matcher: /^35/, name: "jcb"}
            ];
            var value = $.trim(this.value);

            if (value && paymill.validateCardNumber(value))
            {
                // only look at digits
                value = value.replace(/\D/g, "");
                for (i = 0; i < types.length; i++)
                {
                    if (types[i].matcher.test(value))
                    {
                        if ($.inArray(value.length, types[i].length) !== -1)
                        {
                            type = types[i].name;
                        }
                        break;
                    }
                }
            }
            
            var logoImage = $( ".card-type-logo" );    
            if (type)
            {
                // the creditcard-icons are loaded from the PayButton implementation
                // this could be changed with your own icons
                logoImage.attr( "src", "https://button.paymill.de/32x20_" + type + ".png" );
            }
            else
            {
                logoImage.removeAttr( "src" );
            }
        });
    });
</script>

Our script disables the default form submit when clicked and collects all the form values. These are turned into a token by PAYMILL. Once the token is generated we pass it to the final step that finishes the payment.

Initially, we check for errors - if any are found, we display the error (in that .payment-errors at the beginning of our form) and re-enable the submit button. The user can then try again.

We use $.ajax to send a POST request to our server. It's comprised of the following:

  • The URL: Make sure it is https (assuming your service supports this - Heroku does).
  • The data: The JSON request. In this example we are only sending the amount, currency, email and card holder's name.
  • The success function: The data argument is the $data JSON we send back to the browser in the PHP script. You'll need to create a reliable method for handling the different responses.

In the end you will get something like this:

The Form

The PHP

Do steps 1-3 of this handy guide for Heroku.

Then do this and figure out how to update your app.

You'll need to download the PAYMILL PHP library and copy the files into your app's directory.

Create a php file (mine is just postmill.php) and copy in this code:

<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json");

define('PAYMILL_API_HOST', 'https://api.paymill.com/v2/');
define('PAYMILL_API_KEY', '<YOUR_PRIVATE_KEY>');
set_include_path(
        implode(PATH_SEPARATOR, array(
            realpath(realpath(dirname(__FILE__)) . '/lib'),
            get_include_path())
        )
);

$data = json_decode(file_get_contents('php://input'));

if (isset($data->paymillToken)) 
{
    require_once "Services/Paymill/Transactions.php";
    require_once "Services/Paymill/Clients.php";
    require_once "Services/Paymill/Payments.php";

    $transactionsObject = new Services_Paymill_Transactions(PAYMILL_API_KEY, PAYMILL_API_HOST);
    $clientsObject = new Services_Paymill_Clients(PAYMILL_API_KEY, PAYMILL_API_HOST);
    $paymentsObject = new Services_Paymill_Payments(PAYMILL_API_KEY, PAYMILL_API_HOST);

    $clientsParam = array(
        'email' => $data->email,
        'description' => $data->cardholder
    );
    $client = $clientsObject->create($clientsParam);

    $paymentsParam = array(
        'token' => $data->paymillToken,
        'client' => $client['id']
    );
    $payment = $paymentsObject->create($paymentsParam);

    $transactionsParam = array(
        'payment' => $payment['id'],
        'amount' => $data->amount_int,
        'currency' => $data->currency,
        'description' => 'Transaction of ' . $data->cardholder
    );
    $result = $transactionsObject->create($transactionsParam);
    
    $response = new StdClass();
    if ($result['error'])
    {
        $response->status = 'error';
        $response->message = $result['error'];
    }
    else
    {
        $response->id = $result->id;
        $response->status = $result->status;
    }

    echo json_encode($response);
}
?>

You need to download the Paymill PHP Wrapper at https://github.com/Paymill/Paymill-PHP and put the containing "lib" folder into your project.

The header allows us to access the server from a different domain. In practice this should be set to your site's origin (i.e https://YOUR-SITE-ID.squarespace.com) instead of * (wildcard). You can find the origin by inspecting the headers of your php file in Chrome's network tab. Note http/https are different and you would get a cross domain error on your YOUR-SITE-ID.squarespace.com subdomain.

Define the PAYMILL Api URL and set you private Key.

Next, if the token was sent load (require_once) the PAYMILL PHP library.

The next block of code is where the magic happens. Assuming everything is ok, the card is charged and the success message set.

We then pass the transaction response from PAYMILL. This is then encoded in JSON and passed back to the browser and is handled by our function in $.ajax().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment