If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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
:
... 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:
... 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!
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')
:
... 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:
... 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.
... 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.
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()
:
... 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()
:
... 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()
:
... 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
:
... 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())
:
... 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.
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:
... 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
:
... 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()
:
... 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:
... 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:
... 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.
I made it even simpler like this but essentially you're right, the code of this section didn't work for me either:
`\Stripe\Subscription::update($subscription->getStripeSubscriptionId(),[
'plan' => $stripeSubscription->plan->id,
'cancel_at_period_end' => false,
]);`
Hey Kiuega!
Thanks for sharing! You're right that in the newer version of the API, they make reactivating easier / more natural then it was before. I like the change.
Cheers!
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 ?
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!
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:)
// composer.json
{
"require": {
"php": ">=5.5.9, <7.4",
"symfony/symfony": "3.1.*", // v3.1.10
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.8
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.2
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.3.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
"stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
"stripe/stripe-php": "^3.15", // v3.23.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
"phpunit/phpunit": "^5.5", // 5.7.20
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.4
"symfony/phpunit-bridge": "^3.0", // v3.3.0
"hautelook/alice-bundle": "^1.3", // v1.4.1
"doctrine/data-fixtures": "^1.2" // v1.2.2
}
}
Hello, for me, the reactivateSubscription() doesn't work. There is no changements on Stripe. So I follow this link (actual Stripe version) : https://stripe.com/docs/billing/subscriptions/canceling-pausing#reactivating-canceled-subscriptions
And now it works with :