Buy

When the user fills out this form, our JavaScript sends that info to Stripe, Stripe then sends back a token, we add the token as a hidden field in the form and then submit it. Both the checkout form and update card form will work like this. But, we need to submit the update card form to a new endpoint, that'll update the card, but not charge the user.

Submitting the Form to the new Endpoint

Open ProfileController and add a new endpoint: public function updateCreditCardAction(). Give it the URL /profile/card/update and a fancy name: account_update_credit_card. Add the @Method("POST") to be extra cool:

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 7
use Symfony\Component\HttpFoundation\Request;
... lines 9 - 12
class ProfileController extends BaseController
{
... lines 15 - 68
/**
* @Route("/profile/card/update", name="account_update_credit_card")
* @Method("POST")
*/
public function updateCreditCardAction(Request $request)
{
... lines 75 - 90
}
}

With this in place, we need to update the form to submit here. Check out _cardForm.html.twig. Hmm, the action attribute is empty:

68 lines app/Resources/views/order/_cardForm.html.twig
<form action="" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 66
</form>

That's because we want the checkout form to submit right back to /checkout. But this won't work for the update card form: it should submit to a different URL.

Instead, render a new variable called formAction and pipe that to the default filter with empty quotes:

68 lines app/Resources/views/order/_cardForm.html.twig
<form action="{{ formAction|default('') }}" method="POST" class="js-checkout-form checkout-form">
... lines 2 - 66
</form>

Now we can override this! In account.html.twig, add another variable to the include: formAction set to path() and the new route name:

99 lines app/Resources/views/profile/account.html.twig
... lines 1 - 18
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
... lines 23 - 84
<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',
formAction: path('account_update_credit_card')
}) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
... lines 98 - 99

Refresh and check out the source. Ok, the form action is ready!

Saving the Credit Card

Let's get to work in ProfileController! But actually... this will be very similar to our checkout logic, so let's go steal code! Copy the line that fetches the stripeToken POST parameter and then head back to ProfileController. Make sure you have a Request argument and the Request use statement:

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 7
use Symfony\Component\HttpFoundation\Request;
... lines 9 - 12
class ProfileController extends BaseController
{
... lines 15 - 72
public function updateCreditCardAction(Request $request)
{
... lines 75 - 90
}
}

Then, first, paste that line to fetch the token. Second, fetch the current user object with $user = $this->getUser(). And third, we need to make an API request to stripe that updates the Customer and attaches the token as their new card. That means we'll be using the StripeClient. Fetch it first with $stripeClient = $this->get('stripe_client'):

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 72
public function updateCreditCardAction(Request $request)
{
$token = $request->request->get('stripeToken');
$user = $this->getUser();
$stripeClient = $this->get('stripe_client');
... lines 79 - 90
}
}

Here's the awesome part: open StripeClient. We already have a method called updateCustomerCard():

118 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 33
public function updateCustomerCard(User $user, $paymentToken)
{
$customer = \Stripe\Customer::retrieve($user->getStripeCustomerId());
$customer->source = $paymentToken;
$customer->save();
return $customer;
}
... lines 43 - 116
}

We pass the User object and the submitted payment token and it sets it on the Customer and saves.

Victory for code organization! In the controller, just say $stripeClient->updateCustomerCard() and pass it $user and $token:

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 72
public function updateCreditCardAction(Request $request)
{
... lines 75 - 77
$stripeClient = $this->get('stripe_client');
$stripeCustomer = $stripeClient->updateCustomerCard(
$user,
$token
);
... lines 83 - 90
}
}

That takes care of things inside Stripe.

Updating cardLast4 and cardBrand

Now, what about our database? Do we store any information about the credit card? Actually, we do! In the User class, we store cardLast4 and cardBrand. With the new card, this stuff probably changed!

But wait, we've got this handled too guys! Open SubscriptionHelper and check out the handy updateCardDetails() method:

74 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 64
public function updateCardDetails(User $user, \Stripe\Customer $stripeCustomer)
{
$cardDetails = $stripeCustomer->sources->data[0];
$user->setCardBrand($cardDetails->brand);
$user->setCardLast4($cardDetails->last4);
$this->em->persist($user);
$this->em->flush($user);
}
}

Just pass it the User and \Stripe\Customer and it'll make sure those fields are set.

In ProfileController, call this: $this->get('subscription_helper')->updateCardDetails() passing $user and $stripeCustomer... which doesn't exist yet. Fortunately, updateCustomerCard() returns that, so create that variable on that line:

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 72
public function updateCreditCardAction(Request $request)
{
... lines 75 - 78
$stripeCustomer = $stripeClient->updateCustomerCard(
$user,
$token
);
// save card details!
$this->get('subscription_helper')
->updateCardDetails($user, $stripeCustomer);
... lines 87 - 90
}
}

That's it! Add a success message so that everyone feels happy and joyful, and redirect back to the profile page:

93 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 72
public function updateCreditCardAction(Request $request)
{
... lines 75 - 83
// save card details!
$this->get('subscription_helper')
->updateCardDetails($user, $stripeCustomer);
$this->addFlash('success', 'Card updated!');
return $this->redirectToRoute('profile_account');
}
}

Time to try it! Refresh and put in the fake credit card info. But use a different expiration: 10/25. Hit "Update Card". Ok, it looks like it worked. Refresh the Customer page in Stripe. The expiration was 10/20 and now it's 10/25. Card update successful!

But, we still need to handle one more case: when the card update fails.

Leave a comment!