Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

Reactivate/Un-cancel my Subscription!

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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:

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

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

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

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

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

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:

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

Leave a comment!

6
Login or Register to join the conversation
Kiuega Avatar
Kiuega Avatar Kiuega | posted 3 years ago | edited

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 :


public function reactivateSubscription(User $user)
    {
        if(!$user->hasActiveSubscription())
        {
            throw new \LogicException("L'abonnement ne peut pas être désactivé s'il n'est pas terminé");
        }

        $subscription = \Stripe\Subscription::retrieve(
            $user->getSubscription()->getStripeSubscriptionId()
        );
        $subscription = \Stripe\Subscription::update($user->getSubscription()->getStripeSubscriptionId(),[
            'cancel_at_period_end' => false,
            'items' => [
                [
                  'id' => $subscription->items->data[0]->id,
                  'plan' => $user->getSubscription()->getStripePlanId(),
                ],
            ]
        ]);

        $subscription->save();

        return $subscription;
    }```

3 Reply
Carlos M. Avatar
Carlos M. Avatar Carlos M. | Kiuega | posted 3 years ago | edited

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,

]);`

1 Reply

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!

Reply
Default user avatar
Default user avatar Blueblazer172 | posted 5 years ago

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 ?

Reply

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!

Reply
Default user avatar
Default user avatar Blueblazer172 | weaverryan | posted 5 years ago

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

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice