Buy

Bad news: eventually, someone will want to cancel their subscription to your amazing, awesome service. So sad. But when that happens, let's make it as smooth as possible. Remember: happy customers!

Like everything we do, cancelling a subscription has two parts. First we need to cancel it inside of Stripe and second, we need to update our database, so we know that this user no longer has a subscription.

Setting up the Cancel Button

Start by adding a cancel button to the account page. In account.html.twig, let's move the h1 down a bit:

61 lines app/Resources/views/profile/account.html.twig
... lines 1 - 2
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
<h1>
My Account
... lines 10 - 15
</h1>
... lines 17 - 51
</div>
... lines 53 - 55
</div>
</div>
</div>
{% endblock %}
... lines 60 - 61

Next, add a form with method="POST" and make this float right:

61 lines app/Resources/views/profile/account.html.twig
... lines 1 - 2
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
<h1>
My Account
{% if app.user.subscription %}
<form action="{{ path('account_subscription_cancel') }}" method="POST" class="pull-right">
<button type="submit" class="btn btn-danger btn-xs">Cancel Subscription</button>
</form>
{% endif %}
</h1>
... lines 17 - 51
</div>
... lines 53 - 55
</div>
</div>
</div>
{% endblock %}
... lines 60 - 61

We don't actually need a form, but now we can put a button inside and this will POST up to our server. I don't always do this right, but since this action will change something on the server, it's best done with a POST request. Add a few classes for styling and say "Cancel Subscription".

I still need to set the action attribute to some URL... but we need to create that endpoint first!

Open ProfileController. This file renders the account page, but we're also going to put code in here to handle some other things on this page, like cancelling a subscription and updating your credit card.

Create a new public function cancelSubscriptionAction(). Give this a URL: @Route("/profile/subscription/cancel") and a name: account_subscription_cancel:

36 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 21
/**
* @Route("/profile/subscription/cancel", name="account_subscription_cancel")
... line 24
*/
public function cancelSubscriptionAction()
{
... lines 28 - 33
}
}

And, since we'll POST here, we might as well require a POST with @Method - hit tab to autocomplete and add the use statement - then POST:

36 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
... lines 6 - 11
class ProfileController extends BaseController
{
... lines 14 - 21
/**
* @Route("/profile/subscription/cancel", name="account_subscription_cancel")
* @Method("POST")
*/
public function cancelSubscriptionAction()
{
... lines 28 - 33
}
}

With the endpoint setup, copy the route name and go back into the template. Update action, with path() then paste the route:

61 lines app/Resources/views/profile/account.html.twig
... lines 1 - 2
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
<h1>
... lines 9 - 10
{% if app.user.subscription %}
<form action="{{ path('account_subscription_cancel') }}" method="POST" class="pull-right">
... line 13
</form>
{% endif %}
</h1>
... lines 17 - 51
</div>
... lines 53 - 55
</div>
</div>
</div>
{% endblock %}
... lines 60 - 61

And we are setup!

Cancel that Subscription in Stripe

Now, back to step 1: cancel the Subscription in Stripe. Go back to Stripe's documentation and find the section about Cancelling Subscriptions - it'll look a little different than what you see here... because Stripe updated their design right after I recorded. Doh! But, all the same info is there.

Ok, this is simple: retrieve a subscription and then call cancel() on it. Yes! So easy!

Cancelling at_period_end

Or not easy: because you might want to pass this an at_period_end option set to true. Here's the story: by default, when you cancel a subscription in Stripe, it cancels it immediately. But, by passing at_period_end set to true, you're saying:

Hey! Don't cancel their subscription now, let them finish the month and then cancel it.

This is probably what you want: after all, your customer already paid for this month, so you'll want them to keep getting the service until its over.

So let's do this! Remember: we've organized things so that all Stripe API code lives inside the StripeClient object. Fetch that first with $stripeClient = $this->get('stripe_client'):

36 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 25
public function cancelSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
... lines 29 - 33
}
}

Next, open this class, find the bottom, and add a new method: public function cancelSubscription() with one argument: the User object whose subscription should be cancelled:

89 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 77
public function cancelSubscription(User $user)
{
... lines 80 - 86
}
}

For the code inside - go copy and steal the code from the docs! Yes! Replace the hard-coded subscription id with $user->getSubscription()->getStripeSubscriptionId().

89 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 77
public function cancelSubscription(User $user)
{
$sub = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
... lines 83 - 86
}
}

Then, cancel it at period end:

89 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 77
public function cancelSubscription(User $user)
{
$sub = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
$sub->cancel([
'at_period_end' => true,
]);
}
}

Back in ProfileController, use this! $stripeClient->cancelSubscription() with $this->getUser() to get the currently-logged-in-user:

36 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 25
public function cancelSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
$stripeClient->cancelSubscription($this->getUser());
... lines 30 - 33
}
}

Then, to express how sad we are, add a heard-breaking flash message. Then, redirect back to profile_account:

36 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 25
public function cancelSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
$stripeClient->cancelSubscription($this->getUser());
$this->addFlash('success', 'Subscription Canceled :(');
return $this->redirectToRoute('profile_account');
}
}

We've done it! But don't test it yet: we still need to do step 2: update our database to reflect the cancellation.

Leave a comment!