Buy

Reactivate/Un-cancel my Subscription!

So, if someone cancels, they can't un-cancel. And that's a bummer!

In the Stripe API docs, under the "canceling" section, there's actually a spot about reactivating canceled subscriptions, and it's really interesting! It says that if you use the at_period_end method of canceling, and the subscription has not yet reached the period end, then reactivating is easy: just set the subscription's plan to the same plan ID that it had originally. Internally, Stripe knows that means I want to not cancel the subscription anymore.

Route and Controller Setup

Let's hook it up! We're going to need a new endpoint that reactivates a subscription. In ProfileController, add a new public function reactivateSubscriptionAction(). Give it a route set to /profile/subscription/reactivate and a name: account_subscription_reactivate:

58 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 41
/**
* @Route("/profile/subscription/reactivate", name="account_subscription_reactivate")
*/
public function reactivateSubscriptionAction()
{
... lines 47 - 55
}
}

Good start! With this in place, copy the route name, open account.html.twig and go up to the "TODO" we added a few minutes ago. Paste the route, just to stash it somewhere, then copy the entire cancel form and put it here. Update the form action with the new route name, change the text, and use btn-success to make this look like a really happy thing:

73 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.hasActiveSubscription %}
{% if app.user.subscription.isCancelled %}
<form action="{{ path('account_subscription_reactivate') }}" method="POST" class="pull-right">
<button type="submit" class="btn btn-success btn-xs">Reactivate Subscription</button>
</form>
{% else %}
<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 %}
{% endif %}
</h1>
... lines 23 - 63
</div>
... lines 65 - 67
</div>
</div>
</div>
{% endblock %}
... lines 72 - 73

Refresh and enjoy the nice, new Reactivate Subscription button. Beautiful!

Expired Subscriptions Cannot be Reactivated

Let's get to work in the controller. Like everything, this will have two parts. First, we need to reactivate the subscription in Stripe and second, we need to update our database. For the first part, fetch the trusty StripeClient service object with $stripeClient = $this->get('stripe_client'):

58 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 44
public function reactivateSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
... lines 48 - 55
}
}

Next, open that class. Add a new public function reactivateSubscription(). It will need a User argument whose subscription we should reactivate:

105 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 88
public function reactivateSubscription(User $user)
{
... lines 91 - 102
}
}

As the Stripe docs mentioned, we can only reactivate a subscription that has not been fully canceled. If today is beyond the period end, then the user will need to create an entirely new subscription. That's why we only show the button in our template during this period.

But just in case, add an "if" statement: if !$user->hasActiveSubscription(), then we'll throw a new exception with the text:

Subscriptions can only be reactivated if the subscription has not actually ended.

105 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 88
public function reactivateSubscription(User $user)
{
if (!$user->hasActiveSubscription()) {
throw new \LogicException('Subscriptions can only be reactivated if the subscription has not actually ended yet');
}
... lines 94 - 102
}
}

Nothing should hit that code, but now we'll know if something does.

Reactivate in Stripe

To reactivate the Subscription, we first need to fetch it. In the Stripe API docs, find "Retrieve a Subscription." Every object can be fetched using the same retrieve method. Copy this. Then, add, $subscription = and paste. Replace the subscription ID with $user->getSubscription()->getStripeSubscriptionId():

105 lines src/AppBundle/StripeClient.php
... lines 1 - 90
if (!$user->hasActiveSubscription()) {
throw new \LogicException('Subscriptions can only be reactivated if the subscription has not actually ended yet');
}
$subscription = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
... lines 98 - 105

And remember, if any API call to Stripe fails - like because this is an invalid subscription ID - the library will throw an exception. So we don't need to add extra code to check if that subscription was found.

Finally, reactivate the subscription by setting its plan property equal to the original plan ID, which is $user->getSubscription()->getStripePlanId():

105 lines src/AppBundle/StripeClient.php
... lines 1 - 90
if (!$user->hasActiveSubscription()) {
throw new \LogicException('Subscriptions can only be reactivated if the subscription has not actually ended yet');
}
$subscription = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
// this triggers the refresh of the subscription!
$subscription->plan = $user->getSubscription()->getStripePlanId();
... lines 100 - 105

Then, send the details to Stripe with $subscription->save():

105 lines src/AppBundle/StripeClient.php
... lines 1 - 90
if (!$user->hasActiveSubscription()) {
throw new \LogicException('Subscriptions can only be reactivated if the subscription has not actually ended yet');
}
$subscription = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
// this triggers the refresh of the subscription!
$subscription->plan = $user->getSubscription()->getStripePlanId();
$subscription->save();
... lines 101 - 105

And just in case, return the $subscription:

105 lines src/AppBundle/StripeClient.php
... lines 1 - 8
class StripeClient
{
... lines 11 - 88
public function reactivateSubscription(User $user)
{
if (!$user->hasActiveSubscription()) {
throw new \LogicException('Subscriptions can only be reactivated if the subscription has not actually ended yet');
}
$subscription = \Stripe\Subscription::retrieve(
$user->getSubscription()->getStripeSubscriptionId()
);
// this triggers the refresh of the subscription!
$subscription->plan = $user->getSubscription()->getStripePlanId();
$subscription->save();
return $subscription;
}
}

Love it! Back in ProfileController, reactivate the subscription with, $stripeClient->reactivateSubscription($this->getUser()):

58 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 44
public function reactivateSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
$stripeSubscription = $stripeClient->reactivateSubscription($this->getUser());
... lines 49 - 55
}
}

And we are done on the Stripe side.

Updating our Database

The other thing we need to worry about - which turns out to be really easy - is to update our database so that this, once again, looks like an active subscription. It's easy, because we've already done the work for this. Check out SubscriptionHelper: we have a method called addSubscriptionToUser(), which is normally used right after the user originally buys a new subscription:

74 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 45
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user)
{
$subscription = $user->getSubscription();
if (!$subscription) {
$subscription = new Subscription();
$subscription->setUser($user);
}
$periodEnd = \DateTime::createFromFormat('U', $stripeSubscription->current_period_end);
$subscription->activateSubscription(
$stripeSubscription->plan->id,
$stripeSubscription->id,
$periodEnd
);
$this->em->persist($subscription);
$this->em->flush($subscription);
}
... lines 64 - 72
}

But we can could also call this after reactivating. In reality, this method simply ensures that the Subscription row in the table is up-to-date with the latest stripePlanId, stripeSubscriptionId, periodEnd and endsAt:

74 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 45
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user)
{
... lines 48 - 54
$subscription->activateSubscription(
$stripeSubscription->plan->id,
$stripeSubscription->id,
$periodEnd
);
... lines 60 - 62
}
... lines 64 - 72
}

These last two are the most important: because they changed when we deactivated the subscription. So by calling activateSubscription():

125 lines src/AppBundle/Entity/Subscription.php
... lines 1 - 10
class Subscription
{
... lines 13 - 94
public function activateSubscription($stripePlanId, $stripeSubscriptionId, \DateTime $periodEnd)
{
$this->stripePlanId = $stripePlanId;
$this->stripeSubscriptionId = $stripeSubscriptionId;
$this->billingPeriodEndsAt = $periodEnd;
$this->endsAt = null;
}
... lines 102 - 123
}

All of that will be reversed, and the subscription will be alive!

Let's do it! In ProfileController, add a $stripeSubscription = in front of the $stripeClient call. Below that, use $this->get('subscription_helper')->addSubscriptionToUser() and pass it $stripeSubscription and the current user:

58 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 44
public function reactivateSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
$stripeSubscription = $stripeClient->reactivateSubscription($this->getUser());
$this->get('subscription_helper')
->addSubscriptionToUser($stripeSubscription, $this->getUser());
... lines 52 - 55
}
}

And that is everything!

Give your user a happy flash message and redirect back to the profile page:

58 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 11
class ProfileController extends BaseController
{
... lines 14 - 44
public function reactivateSubscriptionAction()
{
$stripeClient = $this->get('stripe_client');
$stripeSubscription = $stripeClient->reactivateSubscription($this->getUser());
$this->get('subscription_helper')
->addSubscriptionToUser($stripeSubscription, $this->getUser());
$this->addFlash('success', 'Welcome back!');
return $this->redirectToRoute('profile_account');
}
}

I think we're ready to try this! Go back and refresh the profile. Press reactivate and... our "cancel subscription" button is back, "active" is back, "next billing period" is back and "credit card" is back. In Stripe, the customer's most recent subscription also became active again. Oh man, this is kind of fun to play with: cancel, reactivate, cancel, reactivate. The system is solid.

Leave a comment!

  • 2017-03-09 Blueblazer172

    i actually removed the subscriptions by myself in stripe xD so silly that i didn't see that

    thanks for the hint for the reactivating :) i'm going to reproduce it like this:)

  • 2017-03-08 weaverryan

    Hey Blueblazer172!

    Hmm. When you get this error, can you go find this subscription in your Stripe dashboard? As far as I know, subscriptions are never deleted (they can become "cancelled" but you can still find them - you can even filter for "canceled" subscriptions in Stripe). That would be step 1 - see if you can find that subscription! My initial instinct is that something went weird while testing. Did you possible reset your Stripe data on the dashboard? Or switch from test to live mode? Let me know!

    About reactivating only 3 times, I think that would need to be handled in your code. For example, if this were important to you, I might add a field to my user - reactivateCount - which would default to 0. Each them they reactivate, increment this by 1. Then, check to see if this value is 3 before allowing them to reactivate. You could even reset this back to 0 after each renewal if you wanted (this would happen when handling webhooks!).

    Cheers!

  • 2017-03-07 Blueblazer172

    Hi,
    when i click on reactivate it throws this exception:

    No such subscription: sub_AF9bUobPfZHKz9

    500 Internal Server Error - InvalidRequest

    why is that so ?

    btw is there a way to let the user only activate or reactivate for 3 times so that they can't spam the server ?