Laravel 7 — Create a subscription system using Cashier & Stripe

Zeba Rahman
fabcoding
Published in
6 min readOct 1, 2020

The latest versions of Laravel and Cashier have brought many changes and made it significantly easier to build features in our web applications. In this tutorial, we will build a simple subscription system using Stripe.

1. Install the Cashier package

Run the following command.

composer require laravel/cashier

This will install migrations for adding required columns to the users table, and also create a subscriptions table. So let us run the migrations.

php artisan migrate

Optionally, you can publish the migration files to your project database directory along with other migrations.

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

We will also require stripe-php package later for retrieving our subsriptions from our stripe account, so let us install it also.

composer require stripe/stripe-php

2. Modify the User model

User.php

Add the Billable trait to the User model.

<?php
namespace App;
...
use Laravel\Cashier\Billable;
class User extends Authenticatable {
use Billable;
...
}

3. Configure Stripe

Go to Stripe.com. Create an account and go to your Dashboard. There are 2 steps to be followed here

3.1. Create subscription plans

First, Here we need to create a Product for each of the subscription plans we want to offer. Set a name and a price for each.

Dashboard -> Products

For this example, I have created 2 plans, Basic and Premium, priced at £ 10.00 and £ 50.00 respectively.

3.2. Set the API keys

Grab the API keys from Stripe.

Dashboard -> Developers -> API Keys

Copy the Publishable Key and the Secret Key and add them in the .env file. Also, add the currency in the file, same as the one you selected in the Stripe dashboard.

project/.env file

STRIPE_KEY=your publishable key here
STRIPE_SECRET=your secret here
CASHIER_CURRENCY=gbp

Also in the services file,

config/sevices.php

Add the following array

'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],

Now we can start working on the main feature.

4. The controller

Run the command to create a new controller

php artisan make:controller SubscriptionController

Go to the created file

app/Http/Controllers/SubscriptionController.php

Add the necessary imports on the top.

<?phpnamespace App\Http\Controllers;require_once('../vendor/autoload.php');use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Cashier\Cashier;
use \Stripe\Stripe;
class SubscriptionController extends Controller {}

Now write the functions inside the class.

public function __construct() {
$this->middleware('auth');
}
public function retrievePlans() {
$key = \config('services.stripe.secret');
$stripe = new \Stripe\StripeClient($key);
$plansraw = $stripe->plans->all();
$plans = $plansraw->data;

foreach($plans as $plan) {
$prod = $stripe->products->retrieve(
$plan->product,[]
);
$plan->product = $prod;
}
return $plans;
}
public function showSubscription() { $plans = $this->retrievePlans();
$user = Auth::user();

return view('seller.pages.subscribe', [
'user'=>$user,
'intent' => $user->createSetupIntent(),
'plans' => $plans
]);
}
public function processSubscription(Request $request)
{
$user = Auth::user();
$paymentMethod = $request->input('payment_method');

$user->createOrGetStripeCustomer();
$user->addPaymentMethod($paymentMethod);
$plan = $request->input('plan');
try {
$user->newSubscription('default', $plan)->create($paymentMethod, [
'email' => $user->email
]);
} catch (\Exception $e) {
return back()->withErrors(['message' => 'Error creating subscription. ' . $e->getMessage()]);
}

return redirect('dashboard');
}

Now we have the controller for returning the list of stripe plans to our subscription view.

6. The routes

routes/web.php

Route::get('/subscribe', 'SubscriptionController@showSubscription');
Route::post('/subscribe', 'SubscriptionController@processSubscription');
// welcome page only for subscribed users
Route::get('/welcome', 'SubscriptionController@showWelcome')->middleware('subscribed');

The first 2 routes are the ones we are working with. The next, welcome route, has a middleware, we will be discussing that in the last step of this tutorial, so don’t pay attention to it now.

7. The views

resources/views/subscribe.blade.php

Here, I will show only the relevant parts of the view, because your theme, scaffolding, and view would be entirely different.

Add the styles required for the Stripe element (shown for entering card details), at the top.

<style>
.StripeElement {
background-color: white;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid transparent;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
</style>

Now create the form. Here I am showing a view with a list of plans as radio selection option. You can choose a better way to display and choose it. Just make sure the value is submitted correctly in the form submission.

<form action="/seller/subscribe" method="POST" id="subscribe-form">
<div class="form-group">
<div class="row">
@foreach($plans as $plan)
<div class="col-md-4"> <div class="subscription-option">
<input type="radio" id="plan-silver" name="plan" value='{{$plan->id}}'>
<label for="plan-silver">
<span class="plan-price">{{$plan->currency}}{{$plan->amount/100}}<small> /{{$plan->interval}}</small></span>
<span class="plan-name">{{$plan->product->name}}</span>
</label>
</div>
</div> @endforeach </div>
</div>
<input id="card-holder-name" type="text"><label for="card-holder-name">Card Holder Name</label> @csrf
<div class="form-row">
<label for="card-element">Credit or debit card</label>
<div id="card-element" class="form-control">
</div> <!-- Used to display form errors. -->
<div id="card-errors" role="alert"></div>
</div>
<div class="stripe-errors"></div>
@if (count($errors) > 0)
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
{{ $error }}<br>
@endforeach
</div>
@endif
<div class="form-group text-center">
<button id="card-button" data-secret="{{ $intent->client_secret }}" class="btn btn-lg btn-success btn-block">SUBMIT</button>
</div>
</form>

Immediately below the closing tag of the FORM element, add the following javascript.

<script src="https://js.stripe.com/v3/"></script><script>    var stripe = Stripe('{{ env('STRIPE_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', {hidePostalCode: true,
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 = '';
}
});
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
console.log("attempting");
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: card,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = error.message;
} else { paymentMethodHandler(setupIntent.payment_method);
}
});
function paymentMethodHandler(payment_method) {
var form = document.getElementById('subscribe-form');
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'payment_method');
hiddenInput.setAttribute('value', payment_method);
form.appendChild(hiddenInput);
form.submit();
}
</script>

Awesome! Now you can navigate to the /seller/subscribe route and use test credentials of Stripe payment and test it out! It should work if you didn’t mess up.

Checking the subscription status

Now you might want to redirect the user to subscribe page directly if they are not already subscribed and to a dashboard of they are already subscribed. Or you may want to show a button and information in the profile/dashboard according to subscription status. You can use the following snippets for checking.

Pay attention to the name of subscription ‘default’ when performing such actions. All the methods return true/false.

Auth::user()->subscribed('default');  //Check if user is subscribed
Auth::user()->subscription('main')->onGracePeriod(); //cancelled but current subscription has not ende
Auth::user()->onPlan('bronze'); //check which plan.
Auth::user()->subscription('default')->cancelled(); //user earlier had a sibscription but cancelled (and no longer on grace period)

Protecting routes

We defined a route above for a ‘welcome’ page only for subscribed users, with a middleware named Subscribed. Let us work on that now.

Some pages you may want to display only if the user is subscribed, otherwise, it should not display at all; the route should not work for the non-subscribed users. We can achieve this using a middleware, in 4 simple steps.

First, create the middleware.

php artisan make:middleware Subscribed

Second, to the newly created file and replace the code.

app/Http/Middleware/Subscribed.php

<?php
namespace App\Http\Middleware;
use Closure;
class Subscribed { public function handle($request, Closure $next) {
if ($request->user() and ! $request->user()->subscribed('default'))
return redirect('subscribe');
return $next($request);
}
}

Third, go the Kernel file and add the middleware.

app/Http/Kernel.php

Add the middleware to the routeMiddleware array.

protected $routeMiddleware = [
...
'subscribed' => \App\Http\Middleware\Subscribed::class,
];

Fourth, just add the middleware to the routes you want to protect

routes/web.php

Route::get('/welcome', 'Seller\SubscriptionController@showWelcome')->middleware('subscribed');

Or, if you wish to use this for several routes, wrap them in a group like this:

Route::group(['middleware' => ['role:seller']], function () {
Route::get('/welcome', 'Seller\SubscriptionController@showWelcome');
...
});

If this helped you, share, clap, and leave a comment!

Originally published on Fabcoding

--

--