Laravel, Laravel Cashier, Vue
Creating a Stripe Subscription with Laravel Cashier + Laravel Passport
Part 7 of 7 in Using Laravel Cashier with VueJS SPA and Laravel Passport API
Dan Pastori
Creating a Stripe Subscription with Laravel Cashier + Laravel Passport Part 7 of 7 in Using Laravel Cashier with VueJS SPA and Laravel Passport API

Now we are getting down to business! We have just finished allowing the user to save a payment method with their account (Managing Stripe Payment Methods in VueJS SPA and Laravel API) and we are now ready for the user to create a Stripe subscription to the plan within our app!

Before we display any options for the plan and what we are allowing the users to subscribe to, we will have to display the possible payment options that they have available to select. Luckily in the last tutorial (Managing Stripe Payment Methods in VueJS SPA and Laravel API), we have everything we need to get started!

1. Allow user to select stored payment option

So we have a simple form where we allow the user to store payment methods. After the payment method is stored, we save it as the default and then load available payment methods for the user to select. The user will have to select one of these to start their subscription.

In the last tutorial we added the proper API route and VueJS component method to load these payments. Let’s open up the SubscriptionManagement.vue component and build a simple template to allow these payment methods to be selected. To do that, add the following HTML to the template after the Save Payment Method button. We will post the final component’s code in the last tutorial to see how it all works.


...
<div class="mt-3 mb-3">
    OR
</div>

<div v-show="paymentMethodsLoadStatus == 2 
    && paymentMethods.length == 0"
    class="">
        No payment method on file, please add a payment method.
</div>

<div v-show="paymentMethodsLoadStatus == 2 
        && paymentMethods.length > 0">
    <div v-for="(method, key) in paymentMethods" 
            v-bind:key="'method-'+key" 
            v-on:click="paymentMethodSelected = method.id"
            class="border rounded row p-1"
            v-bind:class="{
            'bg-success text-light': paymentMethodSelected == method.id    
        }">
            <div class="col-2">
                {{ method.brand.charAt(0).toUpperCase() }}{{ method.brand.slice(1) }}
            </div>
            <div class="col-7">
                Ending In: {{ method.last_four }} Exp: {{ method.exp_month }} / {{ method.exp_year }}
            </div>
            <div class="col-3">
                <span v-on:click.stop="removePaymentMethod( method.id )">Remove</span>
            </div>
    </div>
</div>

A couple things to notice. First, there are a few more variables to add. Those are the paymentMethodsLoadStatus and paymentMethods variables. Let’s get those added to our local data() object as well:


data(){
    return {
        ...
        paymentMethods: [],
        paymentMethodsLoadStatus: 0,
        paymentMethodSelected: {}
    }
},

The paymentMethodsLoadStatus simply keeps track of where the loading of the payment methods is in respect to the state of the request. We need to update our loadPaymentMethods() method to look like:


/*
    Loads all of the payment methods for the
    user.
*/
loadPaymentMethods(){
    this.paymentMethodsLoadStatus = 1;

    axios.get('/api/v1/user/payment-methods')
        .then( function( response ){
            this.paymentMethods = response.data;

            this.paymentMethodsLoadStatus = 2;
        }.bind(this));
},

Now, this gets updated with the state of where the payment methods loading is and we display when it equals 2 which means everything is loaded.

The other variable, paymentMethodSelected which is initialized to an empty object, allows the user to select a payment method to use when they are paying for their subscription. If you look at this piece of the template:


<div v-for="(method, key) in paymentMethods" 
            v-bind:key="'method-'+key" 
            v-on:click="paymentMethodSelected = method.id"
            class="border rounded row p-1"
            v-bind:class="{
            'bg-success text-light': paymentMethodSelected == method.id    
        }">

where we iterate over the payment methods, when the user clicks the payment method, it sets the paymentMethodSelected to be what they clicked on. This is what we pass to our API to subscribe a user. I also have a simple helper class that when the user selects their payment method, the background is bg-success which is green in Bootstrap 4.

Finally, if you look at:


<span v-on:click.stop="removePaymentMethod( method.id )">Remove</span>

You will see we finally link our removePaymentMethod() method to our template, allowing the user to remove a saved payment method. Remember, this was created in the last tutorial tutorial.

2. Display the subscriptions the user can subscribe to

Now that we have the payment method to be selected, we have to display the subscription options. Let’s first add our template to display the options.


<div class="mt-3 row rounded border" 
        v-bind:class="{'bg-success text-light': selectedPlan == 'plan_XXX'}" 
        v-on:click="selectedPlan = 'plan_XXX'">
    <div class="col-6">
        Basic
    </div>
    <div class="col-6">
        $10/mo.
    </div>
</div>

<div class="mt-3 row rounded border" 
        v-bind:class="{'bg-success text-light': selectedPlan == 'plan_YYY'}" 
        v-on:click="selectedPlan = 'plan_YYY'">
    <div class="col-6">
        Professional
    </div>
    <div class="col-6">
        $15/mo.
    </div>
</div>

<div class="mt-3 row rounded border" 
        v-bind:class="{'bg-success text-light': selectedPlan == 'plan_ZZZ'}" 
        v-on:click="selectedPlan = 'plan_ZZZ'">
    <div class="col-6">
        Enterprise
    </div>
    <div class="col-6">
        $20/mo.
    </div>
</div>

For sake of example, I just made little div elements that name the subscription and when clicked set it locally. You just need to record what is selected from the user and send that to the API route we are building.

Let’s break this down. These are the 3 subscription plans that we made in Creating SAAS Products in Stripe to Sell with Laravel Cashier. The first thing to look at is the plan_XXX key. We will have to grab those from our Stripe Dashboard under Billing → Products → Product Name → Pricing Plans. Then you click on the Pricing Plan you created and find the plan ID:

Once you have those inserted into your template, add the selectedPlan to the local data:


data(){
    return {
        ...

        selectedPlan: '',
    }
},

Now when the user clicks the row, the local selectedPlan will be set to what the user chose to subscribe to. The user can now select a plan and a payment method. That’s what we need for them to subscribe!

NOTE: There are multiple ways you could choose to display these plans. Even making an API route to save and load them where you can dynamically display them and adjust their features. That’d involve some extra integration with Stripe outside the scope of this tutorial. This tutorial should provide enough to subscribe to a plan through an API drive app type structure.

3. Add route to process subscription

Now it’s time to send the request to the API to subscribe a customer! We first need to add a route to our routes/api.php file to handle our subscription.

This will be a multi-purpose route that allows the user to update their subscription. Let’s add the following route to the API routes:


Route::put('/user/subscription', 'API\[email protected]');

This will respond to a PUT request on /api/v1/user/subscription. The reason we have it respond to a PUT is it will update the subscription. If the user isn’t subscribed, a subscription will be created. If the user is subscribed and they change their subscription it will update them to the new package.

Let’s build the handler method in our UserController.php:


/**
 * Updates a subscription for the user
 * 
 * @param Request $request The request containing subscription update info.
 */
public function updateSubscription( Request $request ){
    $user = $request->user();
    $planID = $request->get('plan');
    $paymentID = $request->get('payment');

    if( $user->subscribed('Super Notes') ){
        $user->newSubscription( 'Super Notes', $planID )
                ->create( $paymentID );
    }else{
        $user->subscription('Super Notes')->swap( $planID );
    }
    
    return response()->json([
        'subscription_updated' => true
    ]);
}

So there’s A LOT of dense functionality in this method, let’s break it down!

First, we grab the 3 pieces of data we need to fulfill the request the $user, the $planID which is what the user is subscribing to, and the $paymentID which is the method the user is using to pay for the subscription.

The first thing we check is if the user is already subscribed to our product with the subscribed() method added to the user by the Billable trait. The parameter is the product name Super Notes found under Billing → Products → Product Name in the Stripe Dashboard:

If the user is NOT subscribed, we create a new subscription using the newSubscription() method that accepts the product name as the first argument and the $planID as the second argument. We then chain the create() method to the subscription which will create the new subscription and pass the $paymentID as the parameter. This will create the subscription and pay for it using the saved payment ID.

If the user IS already subscribed to the product, then we swap the plans. This allows the user to change the plan they are subscribed to easily and effectively. Stripe will take care of the rest! What we do is find the subscription with the name of our product and run the swap() method with the new $planID as the argument. This is a very simple implementation that you can expand on in a lot of ways by looking at the docs: https://laravel.com/docs/6.x/billing#changing-plans.

That’s all we need to do on the API side!

One thing I’d like to clarify is the way we structured our SAAS product in Stripe is one product with multiple plans. You can structure it a variety of ways, just make sure the user is subscribing to the right product through the API and passing the plan associated with that product.

4. Add Frontend Subscribe Method

This will be the last piece tying everything together! What we need to do is simply add this method to our methods object in the SubscriptionManagement.vue component:


updateSubscription(){
    axios.put('/api/v1/user/subscription', {
        plan: this.selectedPlan,
        payment: this.paymentMethodSelected
    }).then( function( response ){
        alert('You Are Subscribed!');
    }.bind(this));
},

What this does, is submit a request to our API and alerts the user when they are subscribed! Obviously, upon the successful response, modify it to be a good UX such as an alert banner, but this is the final goal!

Before submitting this method, I’d also validate that both the plan and payment are selected. Just things that are more outside the scope of getting a user subscribed through an SPA + API.

Upon success you should see this recorded in 2 places. The first in your local database in the subscriptions table created with Laravel Cashier:

And MOST importantly, in your Stripe Dashboard under Billing → Subscriptions:

Conclusion

That’s it! We now have our Single Page Application set up to handle and manage payments and allow the user to subscribe to a plan. There’s a lot of moving parts but once you have it set up, everything will flow with ease!

Our final SubscriptionManagement.vue file should look like:


<template>
    <div>
        <h3>Manage Your Subscription</h3>

        <label>Card Holder Name</label>
        <input id="card-holder-name" type="text" v-model="name" class="form-control mb-2">

        <label>Card</label>
        <div id="card-element">

        </div>

        <button class="btn btn-primary mt-3" id="add-card-button" v-on:click="submitPaymentMethod()">
            Save Payment Method
        </button>

        <div class="mt-3 mb-3">
            OR
        </div>

        <div v-show="paymentMethodsLoadStatus == 2 
            && paymentMethods.length == 0"
            class="">
                No payment method on file, please add a payment method.
        </div>

        <div v-show="paymentMethodsLoadStatus == 2 
                && paymentMethods.length > 0">
            <div v-for="(method, key) in paymentMethods" 
                    v-bind:key="'method-'+key" 
                    v-on:click="paymentMethodSelected = method.id"
                    class="border rounded row p-1"
                    v-bind:class="{
                    'bg-success text-light': paymentMethodSelected == method.id    
                }">
                    <div class="col-2">
                        {{ method.brand.charAt(0).toUpperCase() }}{{ method.brand.slice(1) }}
                    </div>
                    <div class="col-7">
                        Ending In: {{ method.last_four }} Exp: {{ method.exp_month }} / {{ method.exp_year }}
                    </div>
                    <div class="col-3">
                        <span v-on:click.stop="removePaymentMethod( method.id )">Remove</span>
                    </div>
            </div>
        </div>

        <h4 class="mt-3 mb-3">Select Subscription</h4>

        <div class="mt-3 row rounded border p-1" 
             v-bind:class="{'bg-success text-light': selectedPlan == 'plan_XXX'}" 
             v-on:click="selectedPlan = 'plan_XXX'">
            <div class="col-6">
                Basic
            </div>
            <div class="col-6">
                $10/mo.
            </div>
        </div>

        <div class="mt-3 row rounded border p-1" 
             v-bind:class="{'bg-success text-light': selectedPlan == 'plan_YYY'}" 
             v-on:click="selectedPlan = 'plan_YYY'">
            <div class="col-6">
                Professional
            </div>
            <div class="col-6">
                $15/mo.
            </div>
        </div>

        <div class="mt-3 row rounded border p-1" 
             v-bind:class="{'bg-success text-light': selectedPlan == 'plan_ZZZ'}" 
             v-on:click="selectedPlan = 'plan_ZZZ'">
            <div class="col-6">
                Enterprise
            </div>
            <div class="col-6">
                $20/mo.
            </div>
        </div>

        <button class="btn btn-primary mt-3" id="add-card-button" v-on:click="updateSubscription()">
            Subscribe
        </button>
    </div>
</template>

export default {
    data(){
        return {
            stripeAPIToken: 'pk_test_XXX',

            stripe: '',
            elements: '',
            card: '',

            intentToken: '',

            name: '',
            addPaymentStatus: 0,
            addPaymentStatusError: '',

            paymentMethods: [],
            paymentMethodsLoadStatus: 0,
            paymentMethodSelected: {},

            selectedPlan: '',
        }
    },

    mounted(){
        this.includeStripe('js.stripe.com/v3/', function(){
            this.configureStripe();
        }.bind(this) );

        this.loadIntent();

        this.loadPaymentMethods();
    },

    methods: {
        /*
            Includes Stripe.js dynamically
        */
        includeStripe( URL, callback ){
            var documentTag = document, tag = 'script',
                object = documentTag.createElement(tag),
                scriptTag = documentTag.getElementsByTagName(tag)[0];
            object.src = '//' + URL;
            if (callback) { object.addEventListener('load', function (e) { callback(null, e); }, false); }
            scriptTag.parentNode.insertBefore(object, scriptTag);
        },

        /*
            Configures Stripe by setting up the elements and 
            creating the card element.
        */
        configureStripe(){
            this.stripe = Stripe( this.stripeAPIToken );

            this.elements = this.stripe.elements();
            this.card = this.elements.create('card');

            this.card.mount('#card-element');
        },

        /*
            Loads the payment intent key for the user to pay.
        */
        loadIntent(){
            axios.get('/api/v1/user/setup-intent')
                .then( function( response ){
                    this.intentToken = response.data;
                }.bind(this));
        },

        /*
            Uses the intent to submit a payment method
            to Stripe. Upon success, we save the payment
            method to our system to be used.
        */
        submitPaymentMethod(){
            this.addPaymentStatus = 1;

            this.stripe.confirmCardSetup(
                this.intentToken.client_secret, {
                    payment_method: {
                        card: this.card,
                        billing_details: {
                            name: this.name
                        }
                    }
                }
            ).then(function(result) {
                if (result.error) {
                    this.addPaymentStatus = 3;
                    this.addPaymentStatusError = result.error.message;
                } else {
                    this.savePaymentMethod( result.setupIntent.payment_method );
                    this.addPaymentStatus = 2;
                    this.card.clear();
                    this.name = '';
                }
            }.bind(this));
        },

        /*
            Saves the payment method for the user and
            re-loads the payment methods.
        */
        savePaymentMethod( method ){
            axios.post('/api/v1/user/payments', {
                payment_method: method
            }).then( function(){
                this.loadPaymentMethods();
            }.bind(this));
        },

        /*
            Loads all of the payment methods for the
            user.
        */
        loadPaymentMethods(){
            this.paymentMethodsLoadStatus = 1;

            axios.get('/api/v1/user/payment-methods')
                .then( function( response ){
                    this.paymentMethods = response.data;

                    this.paymentMethodsLoadStatus = 2;
                    // this.setDefaultPaymentMethod();
                }.bind(this));
        },

        removePaymentMethod( paymentID ){
            axios.post('/api/v1/user/remove-payment', {
                id: paymentID
            }).then( function( response ){
                this.loadPaymentMethods();
            }.bind(this));
        },

        updateSubscription(){
            axios.put('/api/v1/user/subscription', {
                plan: this.selectedPlan,
                payment: this.paymentMethodSelected
            }).then( function( response ){
                console.log( response );
            }.bind(this));
        },
    }
}

Our api.php should look like:


use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::group(['prefix' => 'v1', 'middleware' => 'auth:api'], function(){
    Route::get('/user/setup-intent', 'API\[email protected]');
    Route::put('/user/subscription', 'API\[email protected]');
    Route::post('/user/payments', 'API\[email protected]');
    Route::get('/user/payment-methods', 'API\[email protected]');
    Route::post('/user/remove-payment', 'API\[email protected]');
});

And finally our UserController.php should look like:


namespace App\Http\Controllers\API;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * Creates an intent for payment so we can capture the payment
     * method for the user. 
     * 
     * @param Request $request The request data from the user.
     */
    public function getSetupIntent( Request $request ){
        return $request->user()->createSetupIntent();
    }

    /**
     * Updates a subscription for the user
     * 
     * @param Request $request The request containing subscription update info.
     */
    public function updateSubscription( Request $request ){
        $user = $request->user();
        $planID = $request->get('plan');
        $paymentID = $request->get('payment');

        if( !$user->subscribed('Super Notes') ){
            $user->newSubscription( 'Super Notes', $planID )
                 ->create( $paymentID );
        }else{
            $user->subscription('Super Notes')->swap( $planID );
        }
        
        return response()->json([
            'subscription_updated' => true
        ]);
    }

    /**
     * Adds a payment method to the current user. 
     * 
     * @param Request $request The request data from the user.
     */
    public function postPaymentMethods( Request $request ){
        $user = $request->user();
        $paymentMethodID = $request->get('payment_method');

        if( $user->stripe_id == null ){
            $user->createAsStripeCustomer();
        }

        $user->addPaymentMethod( $paymentMethodID );
        $user->updateDefaultPaymentMethod( $paymentMethodID );
        
        return response()->json( null, 204 );        
    }

    /**
     * Returns the payment methods the user has saved
     * 
     * @param Request $request The request data from the user.
     */
    public function getPaymentMethods( Request $request ){
        $user = $request->user();

        $methods = array();

        if( $user->hasPaymentMethod() ){
            foreach( $user->paymentMethods() as $method ){
                array_push( $methods, [
                    'id' => $method->id,
                    'brand' => $method->card->brand,
                    'last_four' => $method->card->last4,
                    'exp_month' => $method->card->exp_month,
                    'exp_year' => $method->card->exp_year,
                ] );
            }
        }

        return response()->json( $methods );
    }

    /**
     * Removes a payment method for the current user.
     * 
     * @param Request $request The request data from the user.
     */
    public function removePaymentMethod( Request $request ){
        $user = $request->user();
        $paymentMethodID = $request->get('id');

        $paymentMethods = $user->paymentMethods();

        foreach( $paymentMethods as $method ){
            if( $method->id == $paymentMethodID ){
                $method->delete();
                break;
            }
        }

        return response()->json( null, 204 );
    }
}

If you have any questions or need any help, feel free to reach out in the comment section below or on Twitter @danpastori.

About the Author
Builder, creator, and maker. Dan Pastori has over 10 years experience as a full stack developer. When you aren't finding Dan working on building a farm using Arduinos, catch him at the beach or hiking in the National Parks. Follow me on Twitter
More posts in this course
Using Laravel Cashier with VueJS SPA and Laravel Passport API See The Entire Course