If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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:
... 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:
... 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:
... 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
:
... lines 1 - 3 | |
{% block javascripts %} | |
... lines 5 - 8 | |
<script type="text/javascript"> | |
Stripe.setPublishableKey('{{ stripe_public_key }}'); | |
</script> | |
{% endblock %} | |
... lines 13 - 52 |
Awesome!
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') }}
:
... 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
:
<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> |
And 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.
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.
Hello!
Do yo know where I can find the documentation about the custom form on Stripe documentation website?
Thx!
Hey be_tnt
I found these two pages
https://stripe.com/en-mx/pa...
https://stripe.com/docs/pay...
I hope it helps.Cheers!
Hello,
I am trying to follow the tutoriel but with Symfony 4 and new checkout Stripe form... So I have a question please.
Is create a Stripe session is mandatory ?
https://stripe.com/docs/payments/checkout/server#integrate
`// Set your secret key: remember to change this to your live secret key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
\Stripe\Stripe::setApiKey("sk_test_Ag");
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'name' => 'T-shirt',
'description' => 'Comfortable cotton t-shirt',
'images' => ['https://example.com/t-shirt.png'],
'amount' => 500,
'currency' => 'usd',
'quantity' => 1,
]],
'success_url' => 'https://example.com/success',
'cancel_url' => 'https://example.com/cancel',
]);`
Thank you.
Hey Camille,
I suppose so, it's required in case you're going to use this Checkout feature from Stripe: https://stripe.com/docs/pay...
Cheers!
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?
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!
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
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!
// 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
}
}
Hi
Thanks for you awesome tutorial.