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 hereCASHIER_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!
…
Leave A Comment