Buy

Unless I cancel my subscription... which I can't actually do yet - we'll add that soon - in 1 month, Stripe will renew my subscription by automatically charging the credit card I have on file. Eventually, we'll need to allow the user to update their credit card info from right here on the account page. But let's start simple: by at least reminding them which card they have on file by showing the card brand

  • like VISA - and the last 4 card numbers.

This is yet another piece of data that's already stored in Stripe, but we're going to choose to also store it in our database, so we can quickly render the info to the user.

Printing the Credit Card Details

In the User class - aka our user table - I've already added two new columns: cardBrand and cardLast4:

84 lines src/AppBundle/Entity/User.php
... lines 1 - 11
class User extends BaseUser
{
... lines 14 - 25
/**
* @ORM\Column(type="string", nullable=true)
*/
private $cardBrand;
/**
* @ORM\Column(type="string", length=4, nullable=true)
*/
private $cardLast4;
... lines 35 - 63
public function getCardBrand()
{
return $this->cardBrand;
}
public function setCardBrand($cardBrand)
{
$this->cardBrand = $cardBrand;
}
public function getCardLast4()
{
return $this->cardLast4;
}
public function setCardLast4($cardLast4)
{
$this->cardLast4 = $cardLast4;
}
}

But these are empty right now: we're not actually setting this data yet.

Before we do that, let's update the template to print these fields. Open the profile/account.html.twig template. Down by the card details, let's say if app.user.cardBrand, then print some information about the user's credit card, like app.user.cardBrand ending in app.user.cardLast4:

53 lines app/Resources/views/profile/account.html.twig
... lines 1 - 2
{% block body %}
<div class="nav-space">
<div class="container">
... lines 6 - 11
<div class="row">
<div class="col-xs-6">
<table class="table">
<tbody>
... lines 16 - 31
<tr>
<th>Credit Card</th>
<td>
{% if app.user.cardBrand %}
{{ app.user.cardBrand }} ending in {{ app.user.cardLast4 }}
{% else %}
None
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
... lines 45 - 47
</div>
</div>
</div>
{% endblock %}
... lines 52 - 53

Those fields on the User object are empty now, so let's fix that!

The Card Details on the Stripe Customer

Head to the Stripe API docs and click on Customers. The card information is attached to the customer under a field called sources. Yes, sources with an s at the end because you could attach multiple cards to a customer if you wanted. But we're not: on checkout, we set just one card on the customer, and replace any existing card, if there was one.

In other words, sources will always have just one entry. That one entry will have a data key, and that will describe the card: giving us all the info you see here.

Now to the plan: use the Stripe API to populate the card information on the User table right during checkout.

Setting the Card Details

In OrderController::chargeCustomer(), we either create or retrieve the \Stripe\Customer. Assign both calls to a new $stripeCustomer variable:

127 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 88
if (!$user->getStripeCustomerId()) {
$stripeCustomer = $stripeClient->createCustomer($user, $token);
} else {
$stripeCustomer = $stripeClient->updateCustomerCard($user, $token);
}
... lines 94 - 123
}
}
... lines 126 - 127

In StripeClient, the createCustomer() method already returns the \Stripe\Customer object, so we're good here:

78 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 19
public function createCustomer(User $user, $paymentToken)
{
$customer = \Stripe\Customer::create([
'email' => $user->getEmail(),
'source' => $paymentToken,
]);
... lines 26 - 29
return $customer;
}
... lines 33 - 76
}

The updateCustomerCard() method, however, retrieves the customer... but gets lazy and doesn't return it. Fix that with return $customer:

78 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 - 76
}

Back in OrderController, we've got the \Stripe\Customer... so we're mega dangerous! But instead of updating the fields on User right here, let's do it in SubscriptionHelper. Add a new public function updateCardDetails() method with a User object that should be updated and the \Stripe\Customer object that's associated with it:

72 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 5
use AppBundle\Entity\User;
... lines 7 - 8
class SubscriptionHelper
{
... lines 11 - 62
public function updateCardDetails(User $user, \Stripe\Customer $stripeCustomer)
{
... lines 65 - 69
}
}

Now, this is pretty easy: $cardDetails = $stripeCustomer->sources->data[0]. Then, $user->setCardBrand($cardDetails) - go cheat with the Stripe API - the fields we want are brand and last4. So, $cardDetails->brand. And $user->setCardLast4($cardDetails->last4). Save only the user to the database with the classic $this->em->persist($user) and $this->em->flush($user):

72 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 62
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);
}
}

Finally, call that method! $this->get('subscription_helper')->updateCardDetails() and pass it $user and $stripeCustomer:

127 lines src/AppBundle/Controller/OrderController.php
... lines 1 - 11
class OrderController extends BaseController
{
... lines 14 - 83
private function chargeCustomer($token)
{
... lines 86 - 88
if (!$user->getStripeCustomerId()) {
$stripeCustomer = $stripeClient->createCustomer($user, $token);
} else {
$stripeCustomer = $stripeClient->updateCustomerCard($user, $token);
}
// save card details
$this->get('subscription_helper')
->updateCardDetails($user, $stripeCustomer);
... lines 98 - 123
}
}
... lines 126 - 127

No matter how you checkout, we're going to make sure your card details are updated in our database!

Before we try it out and prove how awesome we are, I want to add one more thing: I want to be able to tell the user when they will be billed next.

Leave a comment!