If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Eventually, our customers will need to update the credit card that we have stored.
And on surface, this is pretty easy: the card is just a property on the Stripe customer
called source
. And basically, we need to nearly duplicate the checkout process:
show the user a card form, exchange their card info for a Stripe token, submit that
token, and then save it on the user as their new card.
Ok, let's rock! Step 1: add an "Update Card" button to the account page and make it re-use the same card form we already built for checkout. Because, ya know, reusing code is awesome!
In account.html.twig
, find the credit card section. Add a button, give it some
classes for styling and a js-open-credit-card-form
class:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
... lines 22 - 37 | |
<table class="table"> | |
<tbody> | |
... lines 40 - 65 | |
<tr> | |
<th>Credit Card</th> | |
<td> | |
{% if app.user.hasActiveNonCancelledSubscription %} | |
{{ app.user.cardBrand }} ending in {{ app.user.cardLast4 }} | |
<button class="btn btn-xs btn-link pull-right js-open-credit-card-form"> | |
Update Card | |
</button> | |
{% else %} | |
None | |
{% endif %} | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
... lines 83 - 88 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 93 - 94 |
In a second, we'll attach some JavaScript to this.
Next, find the col-xs-6
:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
... lines 21 - 82 | |
<div class="col-xs-6"> | |
... lines 84 - 87 | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 93 - 94 |
This is the right side of the page, and I've kept it empty until now just so we can
show the form here. Add a div with a JS class so we can hide/show this element:
js-update-card-wrapper
. Make it display: none;
by default:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
... lines 21 - 82 | |
<div class="col-xs-6"> | |
<div class="js-update-card-wrapper" style="display: none;"> | |
... lines 85 - 86 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 93 - 94 |
Inside, add a cute header:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
... lines 21 - 82 | |
<div class="col-xs-6"> | |
<div class="js-update-card-wrapper" style="display: none;"> | |
<h2>Update Credit Card</h2> | |
... line 86 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 93 - 94 |
Ok, I want to re-use the entire checkout form right here. Fortunately, we already
isolated that into its own template: _cardForm.html.twig
. Yay!
In account.html.twig
, use the Twig include()
function to bring that in:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
... lines 21 - 82 | |
<div class="col-xs-6"> | |
<div class="js-update-card-wrapper" style="display: none;"> | |
<h2>Update Credit Card</h2> | |
{{ include('order/_cardForm.html.twig') }} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 93 - 94 |
Ok! Let's hide/show this form whenever the user clicks the "Update Card" button.
At the top of the file, override the block called javascripts
, and call endblock
.
Inside, call the parent()
function:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
... lines 5 - 14 | |
{% endblock %} | |
... lines 16 - 94 |
In this project, any JS we put here will be included on the page.
Add a script
tag and a very simple document.ready()
block:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
jQuery(document).ready(function() { | |
... lines 8 - 12 | |
}); | |
</script> | |
{% endblock %} | |
... lines 16 - 94 |
Inside of that, find the .js-open-credit-card-form
element and on click
, create
a callback function. Start with the normal e.preventDefault()
:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
jQuery(document).ready(function() { | |
$('.js-open-credit-card-form').on('click', function(e) { | |
e.preventDefault(); | |
... lines 10 - 11 | |
}); | |
}); | |
</script> | |
{% endblock %} | |
... lines 16 - 94 |
Now, find the other wrapper element, which is js-update-card-wrapper
. Call slideToggle()
on that to show/hide it:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
jQuery(document).ready(function() { | |
$('.js-open-credit-card-form').on('click', function(e) { | |
e.preventDefault(); | |
$('.js-update-card-wrapper').slideToggle(); | |
}); | |
}); | |
</script> | |
{% endblock %} | |
... lines 16 - 94 |
So, fairly easy stuff.
Well, maybe we should see if it works first. Refresh! Ah, it doesn't! Huge error:
Variable "error" does not exist in
_cardForm.html.twig
at line 56
Hmm, checkout that template:
<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 {{ error ? '' : 'hidden' }}">{{ error }}</div> | |
</div> | |
</div> | |
... lines 59 - 66 | |
</form> |
Ah yes, on the checkout page, after we submit, if there was an error, we set this variable
and render it here. For now, we don't have any errors. In account.html.twig
, we could pass
an error
variable to the include()
call. But, we could also do it in ProfileController::accountAction()
.
Add error
set to null
:
... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
... lines 14 - 16 | |
public function accountAction() | |
{ | |
return $this->render('profile/account.html.twig', [ | |
'error' => null | |
]); | |
} | |
... lines 23 - 65 | |
} |
Refresh and click "Update Card". We are in business!
But, this has two cosmetic problems. First, the button says "Checkout"! That's a little scary, and misleading. Let's change it!
In _cardForm.html.twig
, replace "Checkout" with a new variable called
buttonText|default('Checkout')
:
<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"> | |
{{ buttonText|default('Checkout') }} | |
</button> | |
</div> | |
</div> | |
</form> |
So, if the variable is not defined, it'll print "Checkout".
Now, override that in account.html.twig
. Give include()
a second argument:
an array of extra variables. Pass buttonText
as Update Card
:
... lines 1 - 16 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
... lines 21 - 82 | |
<div class="col-xs-6"> | |
<div class="js-update-card-wrapper" style="display: none;"> | |
<h2>Update Credit Card</h2> | |
{{ include('order/_cardForm.html.twig', { | |
buttonText: 'Update Card' | |
}) }} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 95 - 96 |
Refresh! Cool, button problem solved.
The second problem is pretty obvious if you fill out the card number field on checkout, and then compare it with the profile page! On the checkout page, we included a lot of JavaScript that does cool stuff like format this field. And, much more importantly, the JS is also responsible for sending the credit card information to Stripe, fetching the token, putting it in the form, and submitting it. We definitely still need that.
Ok, how can we reuse the JavaScript? In checkout.html.twig
, we just inlined all
of our JS right in the template. That's not great, but since this isn't a course
about JavaScript, let's solve this as easily as possible. Copy all of the JS and
create a new template called _creditCardFormJavaScript.html.twig
inside the order/
directory. Paste this there:
<script type="text/javascript" src="https://js.stripe.com/v2/"></script> | |
<script src="{{ asset('js/jquery.payment.min.js') }}"></script> | |
<script type="text/javascript"> | |
Stripe.setPublishableKey('{{ stripe_public_key }} | |
</script> |
Now, in checkout, include that template!
... lines 1 - 3 | |
{% block javascripts %} | |
{{ parent() }} | |
{{ include('order/_creditCardFormJavaScript.html.twig') }} | |
{% endblock %} | |
... lines 9 - 55 |
Copy that and include the same thing in account.html.twig
at the top of the javascripts
block:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
{{ include('order/_creditCardFormJavaScript.html.twig') }} | |
... lines 7 - 16 | |
{% endblock %} | |
... lines 18 - 98 |
Ok, refresh and hope for the best! Ah, another missing variable: stripe_public_key
:
Variable "stripe_public_key" does not exist in
_creditCardFormJavaScript.html.twig
at line 5
We're printing this in the middle of our JS:
... lines 1 - 3 | |
<script type="text/javascript"> | |
Stripe.setPublishableKey('{{ stripe_public_key }} | |
... lines 6 - 50 | |
</script> |
And the variable comes from OrderController
. Copy that line, open ProfileController
,
and paste it there:
... lines 1 - 11 | |
class ProfileController extends BaseController | |
{ | |
... lines 14 - 16 | |
public function accountAction() | |
{ | |
return $this->render('profile/account.html.twig', [ | |
... line 20 | |
'stripe_public_key' => $this->getParameter('stripe_public_key'), | |
]); | |
} | |
... lines 24 - 66 | |
} |
Now the page works! And - at the very least - the JS formatting is rocking.
Frontend stuff is done: let's submit this and update the user's card in Stripe.
"Houston: no signs of life"
Start the conversation!
// 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.8
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.2
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.3.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
"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", // v1.2.1
"phpunit/phpunit": "^5.5", // 5.7.20
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.4
"symfony/phpunit-bridge": "^3.0", // v3.3.0
"hautelook/alice-bundle": "^1.3", // v1.4.1
"doctrine/data-fixtures": "^1.2" // v1.2.2
}
}