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.

The Update Card Form!

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 $12.00

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!

Re-using the Card Form

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

Hide/Show the Form

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!

Fixing the Button Text

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.

Sharing Card JavaScript

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 }}');
$(function () {
var $form = $('.js-checkout-form');
$form.find('.js-cc-number').payment('formatCardNumber');
$form.find('.js-cc-exp').payment('formatCardExpiry');
$form.find('.js-cc-cvc').payment('formatCardCVC');
$form.submit(function (event) {
event.preventDefault();
// Disable the submit button to prevent repeated clicks:
$form.find('.js-submit-button').prop('disabled', true);
// Request a token from Stripe:
Stripe.card.createToken($form, stripeResponseHandler);
});
});
function stripeResponseHandler(status, response) {
// Grab the form:
var $form = $('.js-checkout-form');
if (response.error) { // Problem!
// Show the errors on the form:
$form.find('.js-checkout-error')
.text(response.error.message)
.removeClass('hidden');
$form.find('.js-submit-button').prop('disabled', false); // Re-enable submission
} else { // Token was created!
$form.find('.js-checkout-error')
.addClass('hidden');
// 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>

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.

Leave a comment!

0
Login or Register to join the conversation
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.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
    }
}
userVoice