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.

Upgrading Subscription Plans: The UI

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

Imagine this: a sheep customer is so happy with the Farmer Brent subscription that they want to upgrade to the New Zealander! Awesome! Amazing! But also... currently impossible.

Time to fix that. Plan upgrades can be complex, because someone needs to calculate how much of the current month has been used and prorate funds towards the upgrade. Stripe's documentation talks about this. I'll guide you through everything, but this section is worth a read.

Printing the Current Plan

Buy a new Farmer Brent Subscription, and then head to the account page.

Let's focus on the plan upgrade user interface first. Here, I need to see which plan I'm currently subscribed to and a button to upgrade to the other plan.

Open the account.html.twig template and ProfileController.

Add a new variable: $currentPlan = null. Then, only if $this->getUser()->hasActiveSubscription(), set $currentPlan = $this->get('subscription_helper')->findPlan() passing that $this->getUser()->getSubscription()->getStripePlanId():

... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 17
public function accountAction()
{
$currentPlan = null;
if ($this->getUser()->hasActiveSubscription()) {
$currentPlan = $this->get('subscription_helper')
->findPlan($this->getUser()->getSubscription()->getStripePlanId());
}
... lines 25 - 30
}
... lines 32 - 106
}

The findPlan() method will give us a fancy SubscriptionPlan object.

Pass a new currentPlan variable into the template, set to $currentPlan:

... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 17
public function accountAction()
{
$currentPlan = null;
if ($this->getUser()->hasActiveSubscription()) {
$currentPlan = $this->get('subscription_helper')
->findPlan($this->getUser()->getSubscription()->getStripePlanId());
}
return $this->render('profile/account.html.twig', [
... lines 27 - 28
'currentPlan' => $currentPlan
]);
}
... lines 32 - 106
}

Then in the template, find the "Active Subscription" spot, and print currentPlan.name:

... lines 1 - 18
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 24 - 39
<table class="table">
<tbody>
<tr>
<th>Subscription</th>
<td>
{% if app.user.hasActiveSubscription %}
{% if app.user.subscription.isCancelled %}
... lines 47 - 49
{% else %}
{{ currentPlan.name }}
<span class="label label-success">Active</span>
{% endif %}
... lines 55 - 56
{% endif %}
</td>
</tr>
... lines 60 - 83
</tbody>
</table>
</div>
... lines 87 - 95
</div>
</div>
</div>
{% endblock %}
... lines 100 - 101

Refresh the page! Great! Step 1 done: we have the "Farmer Brent" plan.

Adding the Upgrade Button

Now, step two: add an upgrade button that mentions the plan they could switch to. Since we only have 2 plans, it's pretty simple: if they're on the Farmer Brent, we want to allow them to upgrade to the New Zealander. And if they're on the New Zealander, we should let them downgrade to the Farmer Brent.

To find the other plan, open SubscriptionHelper and add a new public function called findPlanToChangeTo with a $currentPlanId argument:

... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 45
/**
* @param $currentPlanId
* @return SubscriptionPlan
*/
public function findPlanToChangeTo($currentPlanId)
{
if (strpos($currentPlanId, 'farmer_brent') !== false) {
$newPlanId = str_replace('farmer_brent', 'new_zealander', $currentPlanId);
} else {
$newPlanId = str_replace('new_zealander', 'farmer_brent', $currentPlanId);
}
return $this->findPlan($newPlanId);
}
... lines 60 - 106
}

I'll paste in the logic: it's kind of silly, but it gets the job done. I'm using str_replace instead of something simpler, because in a few minutes, we're going to add yearly plans, and I still want this function to... um... function.

Back to the controller! Add another variable: $otherPlan = null. Then, $otherPlan = $this->get('subscription_helper')->findPlanToChangeTo() and pass it $currentPlan->getPlanId(). Pass this into the template as an otherPlan variable:

... lines 1 - 12
class ProfileController extends BaseController
{
... lines 15 - 17
public function accountAction()
{
$currentPlan = null;
$otherPlan = null;
if ($this->getUser()->hasActiveSubscription()) {
$currentPlan = $this->get('subscription_helper')
->findPlan($this->getUser()->getSubscription()->getStripePlanId());
$otherPlan = $this->get('subscription_helper')
->findPlanToChangeTo($currentPlan->getPlanId());
}
return $this->render('profile/account.html.twig', [
... lines 31 - 32
'currentPlan' => $currentPlan,
'otherPlan' => $otherPlan,
]);
}
... lines 37 - 111
}

There, after the "Active" label, add a button with some classes: a few for styling, one to float right and one - js-change-plan-button - that we'll use in a minute via JavaScript. Make the text: "Change to" and then otherPlan.name:

... lines 1 - 24
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 30 - 45
<table class="table">
<tbody>
<tr>
<th>Subscription</th>
<td>
{% if app.user.hasActiveSubscription %}
{% if app.user.subscription.isCancelled %}
... lines 53 - 55
{% else %}
{{ currentPlan.name }}
<span class="label label-success">Active</span>
<button class="btn btn-xs btn-link pull-right js-change-plan-button" data-plan-id="{{ otherPlan.planId }}" data-plan-name="{{ otherPlan.name }}">
Change to {{ otherPlan.name }}
</button>
{% endif %}
... lines 65 - 66
{% endif %}
</td>
</tr>
... lines 70 - 93
</tbody>
</table>
</div>
... lines 97 - 105
</div>
</div>
</div>
{% endblock %}
... lines 110 - 111

Oh, and add one more attribute: data-plan-name and print otherPlan.name. We'll read that attribute in JavaScript.

Bootstrapping the JavaScript

In fact, let's play with the JavaScript right now: copy the js-change-plan-button class and find the JavaScript block at the top of this file. Use jQuery to locate that element, then on click, add a callback. Start with the always-in-style e.preventDefault():

... lines 1 - 2
{% block javascripts %}
... lines 4 - 7
<script>
jQuery(document).ready(function() {
... lines 10 - 15
$('.js-change-plan-button').on('click', function(e) {
e.preventDefault();
... lines 18 - 19
})
});
</script>
{% endblock %}
... lines 24 - 111

Start really simple: we'll use a library that I already installed called Sweet Alerts. Call swal() and pass a message Loading Plan Details:

... lines 1 - 2
{% block javascripts %}
... lines 4 - 7
<script>
jQuery(document).ready(function() {
... lines 10 - 15
$('.js-change-plan-button').on('click', function(e) {
e.preventDefault();
swal('Loading Plan Details...');
})
});
</script>
{% endblock %}
... lines 24 - 111

Ok, let's see what this Sweet Alerts thing looks like! Refresh that page! Nice! Click the "Change to New Zealander" link. This is Sweet Alert. It's cute, it's easy, and it'll help us do our job.

Because next, we need to do some serious work: we need to calculate how much we should charge the user to upgrade from the Farmer Brent to the New Zealander, and then show it to the user. That's tricky, because the user is probably in the middle of the month that they've already paid for, so they deserve some credits!

Thankfully, Stripe is going to be a champ and help us out.

Leave a comment!

0
Login or Register to join the conversation
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