Posted on under Laravel by Owen Conti.
Here's a quick overview of how you can add Stripe Checkout and Stripe Customer Portal to your Laravel application via Laravel Cashier.
This guide assumes you have a Stripe dev account setup and access to your Stripe Key and Secret. You should also have your products and pricing configure in Stripe. You will need at least one Price ID.
First, you need to figure out which entity in your application will be considered the billable entity. For example, if you have an application with teams where each team signs up and pays for a number of seats, then your
Team
model will be your billable entity. However, if your application has users which sign up and pay for themselves, then your
User
model will be your billable entity.
We need to setup some environment variables for Laravel Cashier to pass along to stripe. You only need to set
CASHIER_MODEL
if your billable model is not
App\Models\User
.
1CASHIER_MODEL=App\Models\Team2STRIPE_KEY=XXXXXXX3STRIPE_SECRET=XXXXXXX
As of Feb 9, 2020, the released version of Laravel Cashier supports Stripe checkout. You can install Laravel Cashier with:
1composer require laravel/cashier
On the frontend, we will be using the Stripe JS SDK, so make sure to include that on your page somewhere:
1<script src="https://js.stripe.com/v3/" defer></script>
I recommend publishing Cashier's migrations into your local migrations directory, so that you have full control over them:
1php artisan vendor:publish --tag="cashier-migrations"
If your billable model is not
User
, make sure to change the table in the
CreateCustomerColumns
migration we just published to the table that corresponds to your billable model.
1<?php 2 3// I changed 'users' to 'teams' 4 5Schema::table('teams', function (Blueprint $table) { 6 $table->string('stripe_id')->nullable()->index(); 7 $table->string('card_brand')->nullable(); 8 $table->string('card_last_four', 4)->nullable(); 9 $table->timestamp('trial_ends_at')->nullable();10});
I also had to change the
CreateSubscriptionsTable
migration to reference my billable entity's table:
1<?php 2 3class CreateSubscriptionsTable extends Migration 4{ 5 /** 6 * Run the migrations. 7 * 8 * @return void 9 */10 public function up()11 {12 Schema::create('subscriptions', function (Blueprint $table) {13 $table->bigIncrements('id');14 $table->unsignedBigInteger('team_id'); // I changed this from `user_id`15 $table->string('name');16 $table->string('stripe_id');17 $table->string('stripe_status');18 $table->string('stripe_plan')->nullable();19 $table->integer('quantity')->nullable();20 $table->timestamp('trial_ends_at')->nullable();21 $table->timestamp('ends_at')->nullable();22 $table->timestamps();23 24 $table->index(['team_id', 'stripe_status']); // I changed this from `user_id`25 });26 }27}
Since we published Cashier's migrations, we should also tell Cashier not to run its default migrations. Add this to your
AppServiceProvider.register
method:
1<?php 2 3use Laravel\Cashier\Cashier; 4 5/** 6 * Register any application services. 7 * 8 * @return void 9 */10public function register()11{12 Cashier::ignoreMigrations();13}
Next, we need to configure our billable model. In my case, the billable model is
Team
. Add the
Billable
trait to your model:
1<?php 2 3namespace App\Models; 4 5use Laravel\Cashier\Billable; 6 7class Team extends Model 8{ 9 use Billable;10}
If your user is logged in but doesn't have an active subscription, we need to redirect them to a page asking them to subscribe. The following examples will be specific to Inertia, but the concepts can be used on any Laravel stack.
We're going to add a middleware which we will use to confirm the user has an active subscription. You can pass the name of the subscription into
subscribed()
.
1<?php 2 3namespace App\Http\Middleware; 4 5use Closure; 6 7class BillingMiddleware 8{ 9 public function handle($request, Closure $next)10 {11 $user = $request->user();12 13 if ($user && !$user->currentTeam->subscribed('default')) {14 return redirect('subscription');15 }16 17 return $next($request);18 }19}
We then need to add the new middleware to our HTTP Kernel:
1<?php2 3protected $routeMiddleware = [4 // ...5 'billing' => BillingMiddleware::class6];
Create the controller which will start a new Stripe Checkout session, and then return the Checkout Session ID to the UI.
1<?php 2 3namespace App\Http\Controllers\Teams; 4 5use App\Http\Controllers\Controller; 6use Illuminate\Http\Request; 7use Inertia\Inertia; 8 9class ManageSubscriptionController extends Controller10{11 public function __invoke(Request $request)12 {13 $checkout = $request->user()->currentTeam->newSubscription('default', config('stripe.price_id'))->checkout();14 15 return Inertia::render('Teams/ManageSubscription', [16 'stripeKey' => config('cashier.key'),17 'sessionId' => $checkout->id18 ]);19 }20}
You'll notice in the above example that I am referencing
config('stripe.price_id')
. This comes from the Stripe Dashboard where you configure your products and pricing. I'll leave it up to you to figure out how you want to determine this value. Most people store their plans/pricing in a Laravel config file and then pull them from there based on what the user selected from your UI.
Don't forget to add the route for the above controller:
1<?php2 3Route::get('/subscription', ManageSubscriptionController::class)->name('subscription');
Note that the name of the route above matches the name of the route we are redirecting to in the
BillingMiddleware
.
Here's my full Inertia (Vue) component to render the subscription page. This probably won't be copy/pastable, but hopefully it can guide you in the right direction.
1<template> 2 <page :title="title"> 3 <div class="py-10 mx-auto max-w-7xl sm:px-6 lg:px-8"> 4 <div class="text-center"> 5 <h2 class="text-2xl font-bold"> 6 Hold up! You need an active subscription first. 7 </h2> 8 <jet-button class="mt-4" @click.native="checkout"> 9 Head to the checkout page10 </jet-button>11 </div>12 </div>13 </page>14</template>15 16<script>17import Page from '../../Layouts/Page';18import AppLayout from '../../Layouts/AppLayout';19import JetButton from '../../Jetstream/Button';20export default {21 layout: AppLayout,22 components: {23 Page,24 JetButton,25 },26 props: {27 stripeKey: {28 type: String,29 required: true,30 },31 sessionId: {32 type: String,33 required: true,34 },35 },36 computed: {37 title() {38 return 'Manage Subscription';39 },40 },41 methods: {42 checkout() {43 window44 .Stripe(this.stripeKey)45 .redirectToCheckout({46 sessionId: this.sessionId,47 })48 .then(function (result) {49 console.error('result', result);50 });51 },52 },53};54</script>
The key to the above component is the
checkout
method which we call via a button (user input). The
checkout
method is called with our public Stripe Key (passed from the backend), and the
sessionId
which is the Stripe Checkout Session ID (also passed from the backend).
From here, Laravel Cashier will take care of updating your database tables with the accurate subscription info, all via webhooks. You will need to make sure you've configured webhooks on the Stripe Dashboard though. The endpoint that Laravel Cashier automatically registers is:
/stripe/webhook
.
The Billing Portal is the easiest part of this whole process. All you need to do is register a new endpoint which redirects to the billing portal.
1<?php 2 3namespace App\Http\Controllers\Teams; 4 5use App\Http\Controllers\Controller; 6use Illuminate\Http\Request; 7 8class BillingPortalController extends Controller 9{10 public function __invoke(Request $request)11 {12 return $request->user()->currentTeam->redirectToBillingPortal();13 }14}
And then define the route:
1<?php2 3Route::get('/billing-portal', BillingPortalController::class);
And then link to that new route from wherever you want in your application. In my Jetstream application, I've added it to the dropdown menu:
1<jet-dropdown-link href="/billing-portal" :external="true">2 Billing Portal3</jet-dropdown-link>
There are a bunch of steps involved, but mostly its following the Laravel Cashier documentation. You can check out the Laravel docs repo on GitHub to see the documentation changes made for the Billing Portal: https://github.com/laravel/docs/compare/stripe-checkout...master
Hopefully you found this article useful! If you did, share it on X!
Found an issue with the article? Submit your edits against the repository.