How to integrate stripe subscription in laravel?

Learn how to integrate Stripe subscriptions into your Laravel application with this comprehensive guide. Step-by-step instructions cover everything from setting up your Stripe account to managing subscriptions and payments within your Laravel framework, ensuring a seamless and efficient integration process.


We hope you already have an account along with your publishable and secret keys. If you don't, please visit https://dashboard.stripe.com to create or log into your account. You will find your credentials on the dashboard.


We'll be utilizing the stripe package for this process. To install the stripe package, please execute the provided command.

composer require stripe/stripe-php
Step 1 : Plan Module

First, we need to create the plan module. To do this, let's generate the model, migration, and controller. You can accomplish all of this with a single command.

php artisan make:model Plan -mcr

In this step, we will add fields to our plan table. To manage the fields of the plan table, go to 'database/migrations'. In the migrations folder, you will see multiple files; open the file called '_create_plans_table.php' and copy the schema below.

Schema::create('plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('duration')->nullable();
    $table->float('price');
    $table->tinyInteger('status')->default(1);
    $table->string('plan_id')->nullable();
    $table->string('product_id')->nullable();
    $table->timestamps();
});

After editing the schema, run the migration command. This will create tables for all pending migrations.

php artisan migrate

Now, let's add a resource route to the 'web.php' file.

use App\Http\Controllers\PlanController;


Route::resource('plan', PlanController::class);

Let's move to the 'PlanController' and begin working on the 'index' function that has already been created in the resource controller.

use App\Models\Plan;


public function index()
{
    $plans = Plan::all();
    return view('plan.index',compact('plans'));
}

Now let's create blade file to display fetched data in table, So create a file in 'resources/views/plan/index.blade.php'


@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="text-end pb-3">
                    <a href="{{ route('plan.create') }}" class="btn btn-success">Create</a>
                </div>
                <div class="card">
                    <div class="card-header"><h4>{{ __('Plan List') }}</h4></div>
                    <div class="card-body">
                        <table class="table table-bordered">
                            <thead>
                            <tr>
                                <th>#</th>
                                <th>Name</th>
                                <th>Duration</th>
                                <th>Price</th>
                                <th>Status</th>
                                <th class="text-end">Actions</th>
                            </tr>
                            </thead>
                            <tbody>
                            @foreach($plans as $key => $plan)
                                <tr>
                                    <td>{{ $key + 1 }}</td>
                                    <td>{{ $plan->name }}</td>
                                    <td>{{ $plan->duration }}</td>
                                    <td>{{ $plan->price }}</td>
                                    <td>{{ $plan['status'] ? 'Active' : 'Inactive' }}</td>
                                    <td class="text-end">
                                        <div class="btn-group" role="group" aria-label="Action Buttons">
                                            <a href="{{ route('plan.edit',$plan->id) }}" class="btn btn-sm btn-warning">Edit</a>
                                            <form action="{{ route('plan.destroy',$plan->id) }}" method="POST" onSubmit="if(!confirm('Are you sure? This action cannot be undone!')){return false;}">
                                                @csrf
                                                @method('DELETE')
                                                <button class="btn btn-sm btn-danger" type="submit">Delete</button>
                                            </form>
                                        </div>
                                    </td>
                                </tr>
                            @endforeach
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Next, return to the 'PlanController' and update the 'create' function.

public function create()
{
    return view('plan.create');
}

Create a file and form in 'resources/views/plan/create.blade.php'


@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                @if (\Session::has('success'))
                    <div class="alert alert-success" role="alert">
                        {!! \Session::get('success') !!}
                    </div>
                @endif
                @if (\Session::has('error'))
                    <div class="alert alert-danger" role="alert">
                        {!! \Session::get('error') !!}
                    </div>
                @endif
                <div class="pb-3">
                    <a href="{{ route('plan.index') }}" class="btn btn-secondary">Back</a>
                </div>
                <form action="{{ route('plan.store') }}" method="POST" class="card" enctype="multipart/form-data">
                    @csrf
                    <div class="card-header"><h4>{{ __('Create Plan') }}</h4></div>
                    <div class="card-body">
                        <div class="mb-3">
                            <label for="name">Name</label>
                            <input type="text" class="form-control" name="name" id="name" placeholder="Enter Name" value="{{ old('name') }}">
                        </div>
                        <div class="mb-3">
                            <label for="price">Price</label>
                            <input type="number" class="form-control" name="price" id="price" placeholder="Enter Price" value="{{ old('name') }}">
                        </div>
                        <div class="mb-3">
                            <label for="duration">Duration</label>
                            <select name="duration" class="form-select">
                                <option value="">Select Duration</option>
                                <option value="Trial" {{ old('duration') == 'Trial' ? 'selected' : '' }}>Trial</option>
                                <option value="Monthly" {{ old('duration') == 'Monthly' ? 'selected' : '' }}>Monthly</option>
                                <option value="Quarterly" {{ old('duration') == 'Quarterly' ? 'selected' : '' }}>Quarterly</option>
                                <option value="Yearly" {{ old('duration') == 'Yearly' ? 'selected' : '' }}>Yearly</option>
                            </select>
                        </div>
                        <div class="form-check">
                            <input type="checkbox" name="status" class="form-check-input" id="status" {{ old('status') ? 'checked' : '' }}>
                            <label class="form-check-label" for="status">Status</label>
                        </div>
                    </div>
                    <div class="card-footer text-end">
                        <button class="btn btn-primary" type="submit">Save</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
@endsection

Return to the 'PlanController' and update the 'store' function.

public function store(Request $request)
{
    // Validate the incoming request data
    $validator = \Validator::make(
        $request->all(), [
            'name' => 'required',
            'price' => 'required',
            'duration' => 'required',
        ]
    );

    // Check if validation fails
    if ($validator->fails()) {
        $messages = $validator->getMessageBag();
        return redirect()->back()->withInput()->with('error', $messages->first());
    }

    // Create a new Plan instance and set its attributes
    $plan = new Plan();
    $plan->name = $request->name;
    $plan->price = $request->price;
    $plan->duration = $request->duration;
    $plan->status = $request->status == 'on' ? 1 : 0;
    $plan->save();

    // These are possible intervals and interval counts
    // interval : day, month, week, year
    // interval_count : year = 3, months = 36, weeks = 156 | maximum time limit for a subscription
    // Quarterly: interval_count = 3 , interval = month

    // If the duration is not 'Trial', create a Stripe plan
    if ($request->duration != 'Trial')
    {
        $stripe_secret_key = env('STRIPE_SECRET_KEY');
        $stripe = new \Stripe\StripeClient($stripe_secret_key);

        // Determine the interval and interval count based on the plan's duration
        switch ($plan->duration)
        {
            case "Monthly":
                $interval = 'month';
                $interval_count = '1';
                break;
            case "Quarterly":
                $interval = 'month';
                $interval_count = '3';
                break;
            case "Yearly":
                $interval = 'year';
                $interval_count = '1';
                break;
            default:
                $interval = 'month';
                $interval_count = '1';
        }

        // Create a new Stripe plan
        $response = $stripe->plans->create([
            'amount' => $request->price * 100,
            'currency' => env('CURRENCY_CODE'),
            'interval' => $interval,
            'interval_count' => $interval_count,
            'product' => ['name' => $request->name],
        ]);

        // Update the Plan instance with the Stripe plan ID and product ID
        $plan->plan_id = $response->id;
        $plan->product_id = $response->product;
        $plan->update();
    }

    // Redirect back with a success message
    return redirect()->back()->with('success', __('Plan Created'));
}

This function handles the creation of a new subscription plan, including validation of input data, saving the plan to the database, and interacting with the Stripe API to create a corresponding plan if the duration is not 'Trial'.


Note : Ensure that you have properly mapped your keys. I've stored my keys in the '.env' file like this.

STRIPE_PUBLISHABLE_KEY='pk_test_**************'
STRIPE_SECRET_KEY='sk_test_***********'
CURRENCY_CODE='USD'

Return to 'PlanController' and proceed to work on the 'edit' and 'update' functions.

public function edit(Plan $plan)
{
    return view('plan.edit',compact('plan'));
}

Create a file and form in 'resources/views/plan/edit.blade.php'

Note : Updating a plan is not possible if it is already being used in a subscription. Therefore, allow updates only if the subscription has not been purchased. While it is possible to check the database or check through the API, the best approach would be to disallow updates once the plan is created. 

@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                @if (\Session::has('success'))
                    <div class="alert alert-success" role="alert">
                        {!! \Session::get('success') !!}
                    </div>
                @endif
                @if (\Session::has('error'))
                    <div class="alert alert-danger" role="alert">
                        {!! \Session::get('error') !!}
                    </div>
                @endif
                <div class="pb-3">
                    <a href="{{ route('plan.index') }}" class="btn btn-secondary">Back</a>
                </div>
                <form action="{{ route('plan.update',$plan->id) }}" method="POST" class="card" enctype="multipart/form-data">
                    @csrf
                    @method('PUT')
                    <div class="card-header"><h4>{{ __('Edit Plan') }}</h4></div>
                    <div class="card-body">
                        <div class="mb-3">
                            <label for="name">Name</label>
                            <input type="text" class="form-control" name="name" id="name" placeholder="Enter Name" value="{{ $plan->name }}">
                        </div>
                        <div class="form-check">
                            <input type="checkbox" name="status" class="form-check-input" id="status" {{ $plan->status ? 'checked' : '' }}>
                            <label class="form-check-label" for="status">Status</label>
                        </div>
                    </div>
                    <div class="card-footer text-end">
                        <button class="btn btn-primary" type="submit">Update</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
@endsection

Now, let's update the record based on the ID. Return to the 'PlanController' and its 'update' function.

public function update(Request $request, Plan $plan)
{
    // Validate the incoming request data
    $validator = \Validator::make(
        $request->all(), [
            'name' => 'required',
        ]
    );

    // Check if validation fails
    if ($validator->fails()) {
        $messages = $validator->getMessageBag();
        return redirect()->back()->withInput()->with('error', $messages->first());
    }

    $plan->name = $request->name;
    $plan->status = $request->status == 'on' ? 1 : 0;
    $plan->update();

    // Redirect back with a success message
    return redirect()->back()->with('success', __('Plan Updated'));
}

Now we're on the final step of plan module, which is deletion. Open the 'PlanController' and scroll to the bottom to find the 'destroy' function.

public function destroy(Plan $plan)
{
    $plan->delete();
    return redirect()->back()->with('success', __('Plan Deleted'));
}
Step 2 : Setup User, Cashier Stripe and Plan Purchase

We will be using the Cashier Stripe package to create users as customers in Stripe. To install the package, please run the following command.

composer require laravel/cashier

After installing the package, publish Cashier's migrations using the vendor:publish Artisan command:

php artisan vendor:publish --tag="cashier-migrations"

Then, Run the migration command

php artisan migrate

After running the migration, open 'app/Models/User.php' and add 'Billable' to the user model.

use Laravel\Cashier\Billable;


class User extends Authenticatable
{
    use Billable;

Cashier also requires Stripe's secret keys and publishable keys. However, there's a chance our variables might not match Cashier's variables. Therefore, we need to publish the Cashier config file. This will allow us to map our keys in real-time and avoid issues, even if we retrieve keys dynamically. To publish the Cashier config file, run the command below.

php artisan vendor:publish --tag="cashier-config"

With Cashier and User set up, we can now move forward with the plan purchase process. Let's start by creating the 'PlanPurchase' controller.

php artisan make:controller PlanPurchaseController

Next, we will create a model and migration to save the details of purchased plans.

php artisan make:model PurchasedPlan -m

Next, we will add fields to our 'PurchasedPlan' table. To manage the fields, navigate to the 'database/migrations' directory. Inside the migrations folder, you will find multiple files; open the one named '_create_purchased_plans_table.php' and copy the schema provided below.

Schema::create('purchased_plans', function (Blueprint $table) {
    $table->id();
    $table->string('order_id');
    $table->integer('user_id');
    $table->integer('plan_id');
    $table->string('name');
    $table->float('price')->default(0.00);
    $table->string('duration');
    $table->timestamp('valid_till')->nullable();
    $table->tinyInteger('is_active')->default(1);
    $table->string('subscription_id')->nullable();
    $table->string('stripe_plan_id')->nullable();
    $table->string('product_id')->nullable();
    $table->timestamps();
});

Open 'app/Models/PurchasedPlan.php' and set it up like this.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class PurchasedPlan extends Model
{
    protected $casts = ['valid_till' => 'datetime'];

    public function plan()
    {
        return $this->hasOne(Plan::class, 'id', 'plan_id');
    }

    public function user()
    {
        return $this->hasOne(User::class, 'id', 'user_id');
    }
}
Step 3 : Plan Purchase

Now we will make list, payment and free plan purchase functions in 'PlanPurchaseController', So first lets add some routes in web.php

use App\Http\Controllers\PlanPurchaseController;


// Plan list
Route::get('purchase-plan', [PlanPurchaseController::class, 'index'])->name('purchase-plan');
// Purchasing free plan / trial plan
Route::post('ajax/purchase-free-plan', [PlanPurchaseController::class, 'freePlan'])->name('purchase-free-plan');
// Purchasing real plan / paid plan
Route::post('ajax/initiate-payment', [PlanPurchaseController::class, 'initiatePayment'])->name('initiate-payment');
// Resume plan if its paused or inactive
Route::get('resume-subscription', [PlanPurchaseController::class, 'resumeSubscription'])->name('resume-subscription');

Open 'PlanPurchaseController' and begin working on the 'index' function.

use App\Models\Plan;


public function index()
{
    $user = auth()->user();
    $plans = Plan::where('status',1)->orderBy('price')->get();
    $active_plan_id = 0;
    return view('plan-purchase.index', compact('plans','active_plan_id','user'));
}

Now create a blade file to display available plans, So create a file in 'resources/views/plan-purchase/index.blade.php'


@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row">
            <div class="col-12">
                <h4 class="text-center pb-2">Purchase Plan</h4>
            </div>
            @foreach($plans as $plan)
                <div class="col-4 mt-3">
                    <div class="card">
                        <div class="card-header">
                            <h5 class="text-center">{{ $plan->name }}</h5>
                        </div>
                        <div class="card-body text-center">
                            @if($plan->price > 0)
                                <b>${{ $plan->price }}</b>
                            @else
                                <b>Free Trial</b>
                            @endif
                        </div>
                        <div class="card-footer text-center">
                            @if($plan->price > 0)
                                <button type="button" class="btn btn-danger btn-sm plan-btn" data-id="{{ $plan->plan_id }}">Select</button>
                            @else
                                <button type="button" class="btn btn-danger btn-sm trial-btn" data-id="{{ $plan->id }}">Select</button>
                            @endif
                        </div>
                    </div>
                </div>
            @endforeach
        </div>
    </div>
@endsection

Let's create a custom helper function to manage our 'stripe setup intent' operation.


Since we'll be using this function multiple times, I recommend creating a custom helper file/function. If you're unsure how to create a custom helper, refer to our post on the topic for guidance.

<?php


if (!function_exists('stripe_setup_intent'))
{
    function stripe_setup_intent()
    {
        config(
            [
                'cashier.key' => env('STRIPE_PUBLISHABLE_KEY'),
                'cashier.secret' => env('STRIPE_SECRET_KEY'),
                'cashier.currency' => env('CURRENCY_CODE'),
            ]
        );
        $user = auth()->user();
        $key = $user->createSetupIntent()->client_secret;
        return $key;
    }
}

Let's create a payment form. We don't need to do much because 'stripe.js' handles most of the heavy lifting. We only need to provide the name on the card and the plan ID. We'll use a Bootstrap model for this. First, let's add the required CDN to the bottom of the Blade file.

// Jquery CDN
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
// Stripe CDN
<script src="https://js.stripe.com/v3/"></script>

Add model just before end of body tag.

<div class="modal fade" role="dialog" id="payment-card-modal">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <form method="POST" action="{{ route('initiate-payment') }}" enctype="multipart/form-data" id="payment-form">
                @csrf
                <input type="hidden" id="plan-id" name="plan_id">
                <div class="modal-header">
                    <h5 class="modal-title">Add Card Details</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div class="w-100">
                        <div class="d-flex flex-column mb-7 fv-row mt-3">
                            <label class="d-flex align-items-center fs-6 fw-semibold form-label mb-2">
                                <span class="required">Name On Card</span>
                            </label>
                            <input type="text" class="form-control" placeholder="" id="cardholder-name" name="name_on_card" value="" />
                        </div>
                        <div class="d-flex flex-column mb-7 fv-row stripe-card">
                            <div id="card-element"></div>
                            <div id="card-errors pt-3 text-danger" role="alert"></div>
                        </div>
                    </div>
                </div>
                <div class="modal-footer flex-center mt-4">
                    <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
                    <button type="button" class="btn btn-danger rounded" id="payment-btn">Pay</button>
                </div>
            </form>
        </div>
    </div>
</div>

Don't forget to add css for stripe element

.StripeElement{
    border: 1px solid #d0d4d9 !important;
    padding-top: 12px;
    padding-bottom: 12px;
    padding-right: 10px;
    padding-left: 10px;
    border-radius: 5px;
    background-color: #ffffff;
    margin-top: 30px;
}

Model Preview : 


Let's move on to the JavaScript section. First, we'll add the script for the free purchase.

$(document).on('click', '.trial-btn', function()
{
    const progressHtml = '<span class="indicator-progress d-block">Processing...<span class="spinner-border spinner-border-sm align-middle ms-2"></span></span>';
    var $this = $(this);
    $this.prop('disabled',true);
    $this.html(progressHtml);

    var id = $this.data('id');
    var formValues = {
        plan_id:id
    };

    if (confirm("Are you sure you want to buy free plan?") == false)
    {
        $this.html('Select');
        $this.prop('disabled',false);
        return false;
    }
    purchaseFreePlan(formValues);
});

function purchaseFreePlan(formData)
{
    var URL = "{{ route('purchase-free-plan') }}";
    $.ajax({
        url: URL,
        dataType: "json",
        data: formData,
        type: "POST",
        cache: false,
        success: function (response) {
            if (response.status == 'success')
            {
                // success show message and reload page
                if(confirm(response.message))
                {
                    window.location.reload();
                }
            }
            else
            {
                //show error message
                $('.trial-btn').html('Select');
                $('.trial-btn').prop('disabled',false);
                alert(response.message);
            }
        },
        error: function (response)
        {
            //show error message
            $('.trial-btn').html('Select');
            $('.trial-btn').prop('disabled',false);
            alert(response.responseJSON.message);
        }
    });
}

The JavaScript section for the free plan is complete. Now, navigate to the 'PlanPurchaseController' and create a function named 'freePlan'.

use App\Models\PurchasedPlan;
use Carbon\Carbon;
use Illuminate\Http\Request;


public function freePlan(Request $request)
{
    $user = auth()->user();
    $plan = Plan::find($request->plan_id);

    $order_id = time().rand(1,9999);

    $purchasedPlan = new PurchasedPlan();

    $purchasedPlan->order_id = $order_id;
    $purchasedPlan->plan_id = $plan->id;
    $purchasedPlan->user_id = $user->id;
    $purchasedPlan->name = $plan->name;

    $purchasedPlan->price = $plan->price;
    $purchasedPlan->valid_till = Carbon::now()->addMonth();

    $purchasedPlan->duration = $plan->duration;
    $purchasedPlan->is_active = 1;
    $purchasedPlan->subscription_id = 'Trial';
    $purchasedPlan->save();

    return response()->json(['status' => 'success', 'message' => 'TrialPlan Purchased Successfully!']);
}

That's it for the free plan purchase. Now, let's move on to the main task: subscribing with Stripe. Since we're in the controller, let's create a function named 'initiatePayment'.

public function initiatePayment(Request $request)
{
    config(
        [
            'cashier.key' => env('STRIPE_PUBLISHABLE_KEY'),
            'cashier.secret' => env('STRIPE_SECRET_KEY'),
            'cashier.currency' => env('CURRENCY_CODE'),
        ]
    );

    $response = auth()->user()->newSubscription('cashier', $request->plan_id)->create($request->paymentMethod);

    $user = auth()->user();
    $user_id = $user->id;

    $order_id = time().rand(1,9999);

    $plan = Plan::where('plan_id',$request->plan_id)->first();

    switch ($plan->duration)
    {
        case "Monthly":
            $validTill = Carbon::now()->addMonth();
            break;
        case "Quarterly":
            $validTill = Carbon::now()->addMonths(3);
            break;
        case "Yearly":
            $validTill = Carbon::now()->addYear();
            break;
        default:
            $validTill = Carbon::now()->addMonth();
    }

    $purchasedPlan = new PurchasedPlan();

    $purchasedPlan->order_id = $order_id;
    $purchasedPlan->plan_id = $plan->id;
    $purchasedPlan->user_id = $user_id;
    $purchasedPlan->name = $plan->name;

    $purchasedPlan->price = $plan->price;
    $purchasedPlan->duration = $plan->duration;
    $purchasedPlan->valid_till = $validTill;

    $purchasedPlan->is_active = 1;
    $purchasedPlan->subscription_id = $response->stripe_id;
    $purchasedPlan->stripe_plan_id = $response->stripe_price;
    $purchasedPlan->product_id = $response->product_id;

    $purchasedPlan->save();

    return redirect()->back()->with('success',$plan->name . ' plan purchased successfully!');
}

Return to the Blade file, and let's add the subscription script.

// Script to show model
$(document).on('click','.plan-btn',function()
{
    $('#plan-id').val($(this).data('id'));
    $('#payment-card-modal').modal('show');
});
$(document).ready(function()
{
    var stripe = Stripe('{{ env('STRIPE_PUBLISHABLE_KEY') }}');
    var elements = stripe.elements();
    var style = {
        base: {
            color: '#32325d',
            fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
            fontSmoothing: 'antialiased',
            fontSize: '16px',
            '::placeholder': {
                color: '#aab7c4'
            }
        },
        invalid: {
            color: '#fa755a',
            iconColor: '#fa755a'
        }
    };
    var card = elements.create('card', {style: style});

    card.mount('#card-element');

    card.addEventListener('change', function(event) {
        var displayError = document.getElementById('card-errors');
        if (event.error) {
            displayError.textContent = event.error.message;
        } else {
            displayError.textContent = '';
        }
    });

    // Handle form submission.
    var cardHolderName = document.getElementById('cardholder-name');
    var clientSecret = '{{ stripe_setup_intent() }}';

    var paymentBtn = document.getElementById('payment-btn');

    paymentBtn.addEventListener('click', async function(event)
    {
        event.preventDefault();

        const progressHtml = '<span class="indicator-progress d-block">Processing...<span class="spinner-border spinner-border-sm align-middle ms-2"></span></span>';
        var $this = $('#payment-btn');
        $this.prop('disabled',true);
        $this.html(progressHtml);

        const { setupIntent, error } = await stripe.confirmCardSetup(
            clientSecret, {
                payment_method: {
                    card,
                    billing_details: { name: cardHolderName.value }
                }
            }
        );

        if (error)
        {
            // Inform the user if there was an error.
            var errorElement = document.getElementById('card-errors');
            errorElement.textContent = error.message;

            $this.prop('disabled',false);
            $this.html('Pay');
        }
        else
        {
            // Send the token to your server.
            // console.log(paymentMethod);
            stripeTokenHandler(setupIntent);
        }
    });

    // Submit the form with the token ID.
    function stripeTokenHandler(setupIntent)
    {
        // Insert the token ID into the form so it gets submitted to the server
        var form = document.getElementById('payment-form');
        var hiddenInput = document.createElement('input');
        hiddenInput.setAttribute('type', 'hidden');
        hiddenInput.setAttribute('name', 'paymentMethod');
        hiddenInput.setAttribute('value', setupIntent.payment_method);
        form.appendChild(hiddenInput);

        // Submit the form
        form.submit();
    }
});

The plan purchasing process is now complete. You can now proceed with purchasing a plan.


Use this credentials for testing purpose : 

Name : Test

Card Number : 4111 1111 1111 11111

Month / Year : Any future date

CVV : 123

Zip : 12345


Now, let's proceed to manage periodic subscription payments using middleware. To get started, please execute the provided command.

php artisan make:middleware VerifySubscriptionPayment

Now a middleware named 'VerifySubscriptionPayment.php' will be generated in : 'app/Http/Middleware/VerifySubscriptionPayment.php'


Note: Before we start working on the 'VerifySubscriptionPayment' middleware, ensure that you add a 'csrf' bypass for routes starting with '/ajax/'. To do this, open 'app/Http/Middleware/VerifyCsrfToken.php'.

class VerifyCsrfToken extends Middleware
{
    protected $except = [
        '*/ajax/*'
    ];
}

Now we can begin working on the 'VerifySubscriptionPayment' middleware. This middleware will handle all possible scenarios, including new subscriptions, plan expiry, the expiry of free/trial plans, and paused/cancled plans.

<?php

namespace App\Http\Middleware;

use App\Models\Plan;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class VerifySubscriptionPayment
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = auth()->user();

        if($user)
        {
            // I am using eloquent relationship of belongsTo, To define a many-to-one relationship between two models. Checkout last step
            if (!empty($user->plan->valid_till))
            {
                if ($user->plan->valid_till->isPast())
                {
                    // Plan expired need to verify through stripe api
                    $subscriptionId = $user->plan->subscription_id;
                    if(!empty($subscriptionId) && $subscriptionId != 'Trail')
                    {
                        // Plan was purchased before, Check plan status
                        $stripe_secret_key = env('STRIPE_SECRET_KEY');
                        $stripe = new \Stripe\StripeClient($stripe_secret_key);
                        $stripeResponse = $stripe->subscriptions->retrieve($subscriptionId, []);
                    }

                    if(isset($stripeResponse->status))
                    {
                        if ($stripeResponse->status == 'active')
                        {
                            // Plan is active, So update current valid till date and continue
                            $plan = $user->plan;
                            $plan->is_active = 1;
                            $plan->valid_till = Carbon::parse($stripeResponse->current_period_end);
                            $plan->update();

                            return $next($request);
                        }
                        elseif ($stripeResponse->status == 'paused')
                        {
                            $plan = $user->plan;
                            $plan->is_active = 0;
                            $plan->update();

                            // Subscription is paused, Show message and button to resume it.
                            return new \Illuminate\Http\Response(view('plan-purchase.resume', compact('user')));
                        }
                        elseif ($stripeResponse->status == 'canceled' || $stripeResponse->status == 'cancel')
                        {
                            // Plan is Canceled / Over, Purchase new plan / New Subscription
                            // Exclude free trial plan
                            $plans = Plan::where('status', 1)->where('price', '>', 0)->get();
                            return new \Illuminate\Http\Response(view('plan-purchase.index',compact('plans')));
                        }
                        else
                        {
                            // Buy new plan if all conditions fail
                            // Exclude free trial plan
                            $plans = Plan::where('status', 1)->where('price', '>', 0)->get();
                            return new \Illuminate\Http\Response(view('plan-purchase.index',compact('plans')));
                        }
                    }
                    else
                    {
                        // Free plan is over buy actual plan,
                        // Exclude free trial plan
                        $plans = Plan::where('status',1)->where('price','>',0)->get();
                        return new \Illuminate\Http\Response(view('plan-purchase.index',compact('plans')));
                    }
                }
                else
                {
                    // Plan is active, So continue
                    return $next($request);
                }
            }
            else
            {
                // No plan purchased / New user show all plans including free trial
                $plans = Plan::where('status',1)->get();
                return new \Illuminate\Http\Response(view('plan-purchase.index',compact('plans')));
            }
        }
        else
        {
            // User is not logged in
            return $next($request);
        }
    }
}

We now need to create a Blade file to handle resume plan requests or display a pending confirmation message. Create a file in 'resources/views/plan-purchase/resume.blade.php'.


If payment is pending or paused, We will show a payment link.  


If payment is completed but waiting for conformation then we need to show 'waiting for conformation' message, It happens rarely where you have to confirm payment manually from stripe side.


@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        @if($user->plan->is_active == 2)
            <div class="col-12">
                <h4 class="text-center pb-2">Payment Completed</h4>
                <div class="row">
                    <div class="col-4"></div>
                    <div class="col-4">
                        <div class="card">
                            <div class="card-body text-center">
                                <p>Payment has been completed, Waiting for conformation.</p>
                            </div>
                        </div>
                    </div>
                    <div class="col-4"></div>
                </div>
            </div>
        @else
            <div class="col-12 text-center">
                <h4 class="text-center pb-2">Subscription is Paused</h4>
                <div class="row">
                    <div class="col-4"></div>
                    <div class="col-4">
                        <div class="card">
                            <div class="card-body text-center">
                                <p>To start using our service again, Please resume subscription.</p>
                            </div>
                            <div class="card-footer">
                                <a href="{{ route('resume-subscription') }}" id="pay-now" class="btn btn-danger btn-sm">Pay Now</a>
                            </div>
                        </div>
                    </div>
                    <div class="col-4"></div>
                </div>
            </div>
        @endif
    </div>
</div>
@endsection

When the user clicks on the 'Pay Now' button, we'll initiate a subscription resume request. Stripe will then notify the user with a payment link.

Step 4 : Reminder

We can set up an optional process to notify users about their upcoming payments. To achieve this, we'll use a cron job with task scheduling.


To create a task please execute the provided command.

php artisan make:command PaymentReminder

Now a file named PaymentReminder.php will be generated in : 'app/Console/Commands/PaymentReminder.php'


Note  :  I am using eloquent relationship of belongsTo, To define a many-to-one relationship between two models, To add eloquent relationship open user model. 

<?php

namespace App\Console\Commands;

use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;

class PaymentReminder extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:payment-reminder';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = "Check if user's subscription is active or not.";

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $users = User::all();
        foreach($users as $user)
        {
            if(!empty($user->plan->valid_till))
            {
                if ($user->plan->valid_till->isPast())
                {
                    // PLAN EXPIRED
                    $plan = $user->plan;
                    $plan->is_active = 0;
                    $plan->update();

                    // Notify : Plan has expired
                }
                else
                {
                    $now = Carbon::now();
                    $daysLeft = $user->plan->valid_till->diff($now)->days;

                    if ($daysLeft == 3)
                    {
                        // Notify : Plan expires in 2 days
                    }

                    if ($daysLeft == 1)
                    {
                        // Notify : Plan expires tomorrow
                    }

                    if ($daysLeft == 0)
                    {
                        // Notify : Plan expires today
                    }
                }
            }
        }

        return 1;
    }
}

1. Open the 'app/Console/Kernel.php'. This file contains the schedule method, where you can define your scheduled tasks.

2. Suppose you want to run a task every day at midnight. Add the following code inside the schedule method :


To ensure your project works with the correct time, make sure to set the proper timezone. By default, the timezone is UTC. To change the timezone, open the 'config/app.php' file.

protected function schedule(Schedule $schedule): void
{
    $schedule->command('app:payment-reminder')->dailyAt('00:00');
}

Prior to configuring the cron job, you can manually test your custom task using the following command :

php artisan app:payment-reminder

To configure a cron job on your server, you'll need to add the following command :

0 0 * * * cd /path/to/your/project && php artisan schedule:run >> /dev/null 2>&1
Step 5 : Revision

Due to the length and complexity of this post, we missed a few details along the way. We'll address these in our final revision step.

1. Eloquent relationship in User model

2. Assign middleware

// User Model 
public function plan()
{
    return $this->belongsTo(PurchasedPlan::class,  'id','user_id')->where('is_active',1)->orderBy('id','DESC');
}

To assign middleware open 'app/Http/Middleware/Kernel.php' 

use App\Http\Middleware\VerifySubscriptionPayment;


protected $middlewareAliases = [
    'verify_subscription' => VerifySubscriptionPayment::class,
// Usage of middleware | web.php
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home')->middleware('verify_subscription');