Buy
Buy

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

... 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:

... 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:

... 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:

... 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:

... 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

Tip

Since 2018-07-27, Stripe changed the way you cancel a subscription at period end. Use this code for > the updated API:

$sub->cancel_at_period_end = true;
$sub->save();

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'):

... 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:

... 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().

... 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:

... 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:

... 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:

... 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!

  • 2018-09-11 Diego Aguiar

    Hey rmed19

    Thanks for notifying us about those changes, we will add a note about that

    Cheers!

  • 2018-09-05 rmed19

    Changes since API version 2018-05-21:

    You can no longer set at_period_end in the subscription DELETE endpoints. The DELETE endpoint is reserved for immediate canceling going forward. Use cancel_at_period_end on the subscription update endpoints instead.