Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

Checkout Form JS Handling Logic

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $9.00

Now that we've got the form in place, we need to add some JavaScript that will send all the card information to Stripe. Once again, Stripe wrote a lot of this code for us. Over-achiever.

Copy their JavaScript and scroll up and paste it with our other JavaScript:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
$(function () {
var $form = $('#payment-form');
$form.submit(function (event) {
// Disable the submit button to prevent repeated clicks:
$form.find('.submit').prop('disabled', true);
// Request a token from Stripe:
Stripe.card.createToken($form, stripeResponseHandler);
// Prevent the form from being submitted:
return false;
});
});
... lines 25 - 47
</script>
{% endblock %}
... lines 50 - 89

Then, back on the docs, scroll down a little further to another function called stripeResponseHandler(). Copy that too and paste it:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
Stripe.setPublishableKey('{{ stripe_public_key }}');
$(function () {
var $form = $('#payment-form');
$form.submit(function (event) {
// Disable the submit button to prevent repeated clicks:
$form.find('.submit').prop('disabled', true);
// Request a token from Stripe:
Stripe.card.createToken($form, stripeResponseHandler);
// Prevent the form from being submitted:
return false;
});
});
function stripeResponseHandler(status, response) {
// Grab the form:
var $form = $('#payment-form');
if (response.error) { // Problem!
// Show the errors on the form:
$form.find('.payment-errors').text(response.error.message);
$form.find('.submit').prop('disabled', false); // Re-enable submission
} else { // Token was created!
// Get the token ID:
var token = response.id;
// Insert the token ID into the form so it gets submitted to the server:
$form.append($('<input type="hidden" name="stripeToken">').val(token));
// Submit the form:
$form.get(0).submit();
}
}
</script>
{% endblock %}
... lines 50 - 89

Prepping the JS

Let's look at the code: it uses a jQuery document.ready block to find the form and attach an on submit handler function:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 11
$(function () {
var $form = $('#payment-form');
$form.submit(function (event) {
... lines 15 - 22
});
});
... lines 25 - 47
</script>
{% endblock %}
... lines 50 - 89

Because basically, when the user submits the form, we don't want to submit the form! Ahem, I mean, we want to stop, send all the information to Stripe, wait for the token to come back, put that in the form, and then submit it.

In our case, I've given the form a class called js-checkout-form:

<form action="" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 66
</form>

Copy that class, and change the JavaScript to look for .js-checkout-form:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 11
$(function () {
var $form = $('.js-checkout-form');
... lines 14 - 22
});
... lines 24 - 50
</script>
{% endblock %}
... lines 53 - 92

This is referenced in one other spot further below. Update that too:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
// Grab the form:
var $form = $('.js-checkout-form');
... lines 28 - 49
}
</script>
{% endblock %}
... lines 53 - 92

It's not the most organized JS code.

Oh, and you'll notice that I use these js- classes a lot in my html. That's a standard that I like to use whenever I give an element a class not because I want to style it, but because I want to find it with JavaScript.

When this form is submitted, add event.preventDefault() to prevent the form from actually submitting:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 11
$(function () {
... line 13
$form.submit(function (event) {
event.preventDefault();
... lines 16 - 21
});
});
... lines 24 - 50
</script>
{% endblock %}
... lines 53 - 92

This does more-or-less the same thing as returning false at the end of the function, but with some subtle differences.

Oof - let me fix some of this bad indentation. Next, the code finds the submit button so it can disable it. In our form, the button has a js-submit-button class:

<form action="" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 59
<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>

Copy that and update the code here, and once more down below:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 11
$(function () {
... line 13
$form.submit(function (event) {
... lines 15 - 16
// Disable the submit button to prevent repeated clicks:
$form.find('.js-submit-button').prop('disabled', true);
... lines 19 - 21
});
});
... line 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
... lines 30 - 34
$form.find('.js-submit-button').prop('disabled', false); // Re-enable submission
... lines 36 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

Fetching and Using the Token

Finally, here is the meat of the code. When we call Stripe.card.createToken():

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 11
$(function () {
... line 13
$form.submit(function (event) {
... lines 15 - 19
// Request a token from Stripe:
Stripe.card.createToken($form, stripeResponseHandler);
});
});
... lines 24 - 50
</script>
{% endblock %}
... lines 53 - 92

Stripe's Javascript will automatically fetch all the credit card data by reading the data-stripe attributes. Then, it sends those to stripe via AJAX. When that call finishes, it will execute the stripeResponseHandler function, and hopefully the response will contain that all-important token:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 49
}
</script>
{% endblock %}
... lines 53 - 92

Now, if there was a problem with that card - like an invalid expiration - we need to show that error to the user. To do that, it looks for a payment-errors class and puts the message inside of that:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
// Show the errors on the form:
$form.find('.js-checkout-error')
.text(response.error.message)
... lines 34 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

I have a div ready for this. Its class is js-checkout-error and its hidden by default:

<form action="" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 53
<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>
... lines 59 - 66
</form>

Change the selector to .js-checkout-error, set the text, but then also call removeClass('hidden') so the element shows up:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
// Show the errors on the form:
$form.find('.js-checkout-error')
.text(response.error.message)
.removeClass('hidden');
... lines 35 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

Below in the else, life is good!! I'll paste the .js-checkout-error code from before and modify it to re-add the hidden class - since now things are successful:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
... lines 30 - 36
} else { // Token was created!
$form.find('.js-checkout-error')
.addClass('hidden');
... lines 40 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

When things work, the response comes back with a token, which we get via response.id:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
... lines 30 - 36
} else { // Token was created!
... lines 38 - 40
// Get the token ID:
var token = response.id;
... lines 43 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

To send this to the server, we just smash it into a new input hidden field called... drumroll ... stripeToken:

... lines 1 - 3
{% block javascripts %}
... lines 5 - 8
<script type="text/javascript">
... lines 10 - 24
function stripeResponseHandler(status, response) {
... lines 26 - 28
if (response.error) { // Problem!
... lines 30 - 36
} else { // Token was created!
... lines 38 - 43
// Insert the token ID into the form so it gets submitted to the server:
$form.append($('<input type="hidden" name="stripeToken">').val(token));
... lines 46 - 48
}
}
</script>
{% endblock %}
... lines 53 - 92

This is precisely what the embedded form did. Once the form is submitted, the controller will hum along like nothing ever changed.

Testing the Error and Success

But, that's assuming we didn't mess something up! That's a big but. Go back and refresh the page.

First, test that the error handling works by adding an expiration date in the past. Put in the real credit card number -- oof, ugly formatting - we'll fix that. Then, use an expired expiration. Hit checkout and... boom!

It sent the info to Stripe, Stripe came back with an error, we put the error in the box, and showed that box to our user. In other words, we're awesome. Change this to a future expiration and try again.

It's alive!!!

The only problem I can think of now is how ugly entering a credit card number is: all those numbers just run together. The expiration field is a mess too. Oof. Let's fix that - it's surprisingly easy!

Leave a comment!

2
Login or Register to join the conversation
Default user avatar
Default user avatar Céline Ollagnier | posted 5 years ago

Hi KNP Univ!
I've used Ryan's code and it's working fine, but the code in Stripe documentation as changed (https://stripe.com/docs/ele....
Do you know why? Is it an adaptation to their log or perhaps a security issue?
I'm not a specialist of JS but well I'm curious (as you say in English 'curiosity killed the cat' but not the dev ^_~)
Thanks :)

Reply

Hi Céline Ollagnier!

Hmm, curious indeed! Love it! I'm not completely sure why using my key worked. The private key that you seen in the video WAS definitely changed/revoked. However, the public key is still the same - so using just that key might allow you to do a few things with JavaScript :).

Now, about the new "Elements" stuff" - https://stripe.com/docs/elements. I messages Stripe about this. Basically, as you know, before, if you wanted to create a custom checkout form, you just built the custom checkout form. This is still a valid way to do it, and it's not insecure (when I saw Elements, I wondered if there was a security issue they had discovered with creating a custom login form: there is not). They created Elements to (A) make it simpler to create a custom login form, especially one with rich credit card number fields, etc and (B) make it impossible for you to accidentally submit credit card data to your server (in our tutorial, we never use the name="" attribute to avoid accidentally sending the data to our server - but you could imagine other people messing this up). So, I definitely invite you to checkout Elements, especially if it works well for you! We're hoping - at some point - to add a little "extra" piece to this tutorial about it.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "twig/twig": "^1.24.1", // v1.35.2
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.2
        "hautelook/alice-bundle": "^1.3", // v1.3.1
        "doctrine/data-fixtures": "^1.2" // v1.2.1
    }
}
userVoice