Buy

Building the Custom Checkout Form

Earlier, we were rushing to get the site up and the sheep shopping. That's why we used Stripe's pre-built embedded form. And this is completely fine if you like it. But I want to build a custom form that looks like native on our site.

To do that, go back to the Stripe docs. Instead of embedded form, click "Custom Form". Using a custom form is very similar: we still send the credit card information to Stripe, and Stripe will still give us back a token. The difference is that we are responsible for building the HTML form.

Setting up the Stripe JavaScript

To help communicate with Stripe, we need some JavaScript. Copy the first JavaScript code and then find the checkout.html.twig template. At the top, override {% block javascripts %} and then call the {{ parent() }} function. Paste the script tag below:

52 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
{{ parent() }}
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
... lines 8 - 11
{% endblock %}
... lines 13 - 52

This is just the Twig way of adding some new JavaScript to our page. The base layout also has a javascripts block and jQuery is already included:

83 lines app/Resources/views/base.html.twig
... line 1
<html>
... lines 3 - 14
<body>
... lines 16 - 73
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"></script>
... lines 78 - 79
{% endblock %}
</body>
</html>

Next, we need to tell the JavaScript about our publishable key. Copy that code from the docs and add it in the block:

52 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
{{ parent() }}
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
</script>
{% endblock %}
... lines 13 - 52

We already know from our original code that we have a variable called stripe_public_key. Inside of the JavaScript quotes, print stripe_public_key:

52 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
</script>
{% endblock %}
... lines 13 - 52

Awesome!

Rendering the HTML Form

With that done, it's time to build the form itself. And surprise! I already built us a basic HTML form. Delete the old, embedded form code. Replace it with {{ include('order/_cardForm.html.twig') }}:

52 lines app/Resources/views/order/checkout.html.twig
... lines 1 - 13
{% block body %}
<div class="nav-space-checkout">
<div class="container">
<div class="row">
... lines 18 - 44
<div class="col-xs-12 col-sm-6">
{{ include('order/_cardForm.html.twig') }}
</div>
</div>
</div>
</div>
{% endblock %}

This will read this other template file I prepared: _cardForm.html.twig:

68 lines app/Resources/views/order/_cardForm.html.twig
<form action="" method="POST" class="js-checkout-form checkout-form">
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-user"></i>
</span>
<input data-stripe="name" class="form-control" type="text" autocomplete="off" id="card-name" required placeholder="Card Holder Name"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-credit-card"></i>
</span>
<input data-stripe="number" type="text" autocomplete="off" class="form-control js-cc-number" id="card-number" required placeholder="Card Number"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-4 col-sm-3 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-calendar-o"></i>
</span>
<input data-stripe="exp" type="text" size="4" autocomplete="off" class="form-control js-cc-exp" id="card-expiration" required="required" placeholder="mm/yy"/>
</div>
</div>
<div class="col-xs-4 col-sm-3 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-lock"></i>
</span>
<input data-stripe="cvc" type="text" size="4" autocomplete="off" class="form-control js-cc-cvc" id="card-cvc" required="required" placeholder="CVC"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-3 col-sm-offset-2 form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-map-marker"></i>
</span>
<input type="text" autocomplete="off" class="form-control" id="card-zip" placeholder="Zip"/>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 text-center">
<div class="alert alert-danger js-checkout-error hidden"></div>
</div>
</div>
<div class="row">
<div class="col-xs-8 col-sm-6 col-sm-offset-2 text-center">
<button type="submit" class="js-submit-button btn btn-lg btn-danger">
Checkout
</button>
</div>
</div>
</form>

As you can see, this is a normal HTML form. Its method is post and its action is still empty so that it will submit right back to the same URL and controller. Then, there's just a bunch of fields that are rendered to look good with Bootstrap.

Let's see how awesome my design skills are: go back and refresh. Hey, it looks pretty good! Probably because someone styled this for me.

Do NOT Submit Card Data to your Server

There are a few really important things about this form. Most importantly, notice that the input fields have no name attribute. This is crucial. Eventually, we will submit this form, but we do not want to submit these fields because we do not want credit card information passing through our server. Because these fields do not have a name attribute, they are not submitted.

So instead of name, Stripe asks you to use a data-stripe attribute. This tells Stripe which data this field holds. Since this is the cardholder name, we have data-stripe="name". Then below, data-stripe="number", data-stripe="exp" and so-on.

But I'm not choosing these values at random. Inside Stripe's documentation, it tells you which data-stripe value to use for each piece. If you follow the rules, Stripe's JavaScript will do all the work of collecting this data and sending it to Stripe.

OK, let's hook up that JavaScript logic next.

Leave a comment!

  • 2017-02-16 weaverryan

    Hey Blueblazer172!

    Ah, you're right! This is the first I've heard of this deprecation :/. So, first, I'm chatting with Stripe to find out more about this. My guess is that they simply think that their new system is a bit less error-prone - I don't think there's any security problem with the old system (except, perhaps, that it's a bit easier to make a security mistake, like give your fields name attributes, which would cause them to be submitted to your server). I'll let you know if (hopefully when) I hear back from them!

    Until then, my best guess is: using a custom checkout form is *fine* still. And Stripe has a great history of keeping old versions of the API available for a LONG time... years. That being said, I'll definitely look into the new system, and see if we need to update the tutorial. It looks fairly straightforward: a new stripe.js v3 file, and some extra JavaScript in place of physically creating the fields. But, that's just on first glance.

    If I were you, I would checkout the new Elements system, and see if you can figure it out. Ultimately, it will give you back a token, just like the old system... then everything else is the same. But, if it's tricky, I think you're fine with the old system.

    Thanks for pointing this out! Cheers!

  • 2017-02-15 Blueblazer172

    the official docs at https://stripe.com/docs/cus... say that it is no longer safe...

    "This method of using Stripe.js to collect card information using a custom payment form has been deprecated. When creating a payment form to collect card information, use Stripe Elements—our pre-built UI components. Check out our Elements migration guide to learn how to migrate your checkout flow to Elements."

    what can i do ? :P

  • 2016-09-16 Greg

    Hi
    Thanks for you awesome tutorial.

  • 2016-09-06 Shairyar Baig

    Ya it done, thanks.

  • 2016-09-05 weaverryan

    Hey Shairyar!

    It depends :). We've built the site so that we *always* ask the user for a new credit card. But, you *could* update your app so that you don't require an existing user to provide a credit card. In this case, if their card fails (e.g. because its expired), you'll actually get the same error we talk about here: https://knpuniversity.com/s...

    Also, though we haven't published it yet, we go even a little bit *further* with error handling in part 2: https://knpuniversity.com/s...

    Let me know if this clarifies!

  • 2016-09-04 Shairyar Baig

    what happens if it is the existing customer that we are charging but their card on stripe has been expired? i am assuming stripe will send back an error asking to update their card? then we will ask the customer to provide updated card information send that to stripe to charge? will that change the customer id?