rabol/laravel-simplesubscription-stripe

Get up and running with subscriptions in your Laravel app in minutes instead of days

1.1.1 2023-09-08 06:22 UTC

This package is auto-updated.

Last update: 2024-04-27 10:33:51 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

This package is not Laravel Cashier, Laravel Cashier is much more advanced and have several other features. If you want to get up and running with subscriptions and Stripe payments for your Laravel app quickly, then this package is for you.

It only contains 1 migration and a helper class.

Setting up Stripe is out of scope for this package

Installation

You can install the package via composer:

composer require rabol/laravel-simplesubscription-stripe

You can publish and run the migrations with:

php artisan vendor:publish --provider="Rabol\LaravelSimpleSubscriptionStripe\LaravelSimpleSubscriptionStripeServiceProvider" --tag="laravel-simplesubscription-stripe-migrations"
php artisan migrate

You can publish the config file with:

php artisan vendor:publish --provider="Rabol\LaravelSimpleSubscriptionStripe\LaravelSimpleSubscriptionStripeServiceProvider" --tag="laravel-simplesubscription-stripe-config"

This is the contents of the published config file:

return [
    'stripe_key' => env('STRIPE_KEY'),
    'stripe_secret' => env('STRIPE_SECRET'),
    'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), // Web hook secret
    'stripe_webhook_tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300) // max time diff in webhook signature
];

Usage

Here is a simple example of how to use this package.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Rabol\LaravelSimpleSubscriptionStripe\LaravelSimpleSubscriptionStripe;
use Auth;

class StripeController extends Controller
{
    public function index()
    {
        return view('stripe.index')
            ->with('stripePrices', LaravelSimpleSubscriptionStripe::stripe()->prices->all());
    }

    public function gotoStripeCustomerPortal(Request $request)
    {
        return redirect(LaravelSimpleSubscriptionStripe::gotoStripeCustomerPortal(Auth::user(), route('stripe.index')));
    }

    public function checkout(Request $request)
    {
        $user = Auth::user();

        if (!$user->stripe_id) {
            $options = [
                    'email' => $user->email,
                    'name' => $user->name,
                    'address' => [
                    'city' => $user->city ?? '',
                    'line1' => $user->adr_line_1 ?? '',
                    'line2' => $user->adr_line_2 ?? '',
                    'postal_code' => $user->postal_code ?? '',
                    'country' => $user->country ?? '',
                    'state' => $user->state ?? '',
                ]];

            // If the user should be Tax Exempt, and the information to the options array
            LaravelSimpleSubscriptionStripe::createAsStripeCustomer($options);
        }

        $priceId = $request->input('priceId');
        $session = LaravelSimpleSubscriptionStripe::createCheckoutSession([
            'allow_promotion_codes' => true,
            'success_url' => config('app.url') . '/stripe/success/{CHECKOUT_SESSION_ID}',
            'cancel_url' => config('app.url') . '/stripe/canceled',
            'customer' => $user->stripe_id,
            'customer_update' => [
                'name' => 'auto',
                'address' => 'auto',
            ],
            'payment_method_types' => [
                'card'
            ],
            'mode' => 'subscription',
            'tax_id_collection' => [
                'enabled' => true
            ],
            'line_items' => [[
                'price' => $priceId,
                // For metered billing, do not pass quantity
                'quantity' => 1,
            ]],
            'subscription_data' => [
                'metadata' => ['name' => 'Advanced'],
                //'default_tax_rates' => ['txr_xxxxxxxx'] // get the tax id from the Stripe dashboard
            ]
        ]);

        return redirect()->to($session->url);
    }

    public function cancled(Request $request)
    {
        return view('stripe.cancled')
            ->with('request', $request);
    }

    public function success(Request $request, string $session_id)
    {
        $session = LaravelSimpleSubscriptionStripe::stripe()->checkout->sessions->retrieve($session_id);
        $customer = LaravelSimpleSubscriptionStripe::stripe()->customers->retrieve($session->customer);

        return view('stripe.success')
            ->with('session', $session)
            ->with('customer', $customer);
    }
}

If you have a customer that should be Tax exempt add something like this:

'tax_id_data' => [
    [
        'type' => 'eu_vat',
        'value' => 'DK12345678'
    ]
    ],
    'tax_exempt' => 'exempt'

to the $options when creating the customer in Stripe and remember to add a valid tax_rate in the subscription data

View files

Index - /resources/views/stripe/index.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card shadow-sm">
                    <div class="card-header bg-warning">
                        <div>
                            <div class="row">
                                <div class="col-4">
                                    {{ __('Simple subsctiption Stripe') }}
                                </div>
                                @if(auth()->user()->stripe_id)
                                <div class="col-8 text-right">
                                    <form method="POST" action="{{ route('stripe.customer_portal') }}">
                                        @csrf
                                        <button class="btn btn-sm btn-primary" type="submit">Billing portal</button>
                                    </form>
                                </div>
                                @endif
                            </div>
                        </div>
                    </div>

                    <div class="card-body">
                        <table class="table">
                            <thead>
                                <th>Stripe Price Id</th>
                                <th>Name</th>
                                <th>Price</th>
                                <th>Actions</th>
                            </thead>
                            <tbody>
                            @foreach($stripePrices as $stripePrice)
                                <tr>
                                    <td>{{$stripePrice->id}}</td>
                                    <td>{{Rabol\LaravelSimpleSubscriptionStripe\LaravelSimpleSubscriptionStripe::stripe()->products->retrieve($stripePrice->product)->name}}</td>
                                    <td>
                                        @php
                                            $nf = new \NumberFormatter('es_ES', \NumberFormatter::CURRENCY);
                                            $value = $nf->formatCurrency(($stripePrice->unit_amount / 100), 'EUR');
                                        @endphp
                                        {{ $value}}
                                    </td>
                                    <td>
                                        <form action="{{ route('stripe.checkout') }}" method="POST">
                                        @csrf
                                            <input type="hidden" name="priceId" value="{{ $stripePrice->id }}" />
                                            <button class="btn btn-sm btn-primary" type="submit">Checkout</button>
                                        </form>
                                    </td>
                                </tr>
                            @endforeach
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Cancled - /resources/views/stripe/canceled.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Simple subsctiption Stripe') }}</div>

                    <div class="card-body">
                        Sorry to see that you canceled the checkout
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Success - /resources/views/stripe/success.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Simple subsctiption Stripe') }}</div>

                    <div class="card-body">
                        <div class="row">

                        </div>
                        Thanks, {{$customer->name}} enjoy your subscription.
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

web.php - /routes/web.php

Route::prefix('stripe')
    ->as('stripe.')
    ->middleware(['auth'])
    ->group(function () {
        Route::get('index', [StripeController::class, 'index'])->name('index');
        Route::post('customer_portal', [StripeController::class,'gotoStripeCustomerPortal'])->name('customer_portal');
        Route::post('checkout', [StripeController::class, 'checkout'])->name('checkout');

        Route::get('cancled', [StripeController::class, 'canceled'])->name('canceled');
        Route::get('success/{session_id}', [StripeController::class, 'success'])->name('success');
});

Handling Stripe callbacks - webhooks

To handle webhooks, create a new controller and extend it from Rabol\LaravelSimpleSubscriptionStripe\Http\Controllers\WebhookController and create a method for each event that you would like to handle.

The method should be the studly case name of the event prefixed with handle and postfixed with Event like this:

handleCustomerSubscriptionCreatedEvent($event)

Example:

<?php

namespace App\Http\Controllers;


use App\Models\User;
use Rabol\LaravelSimpleSubscriptionStripe\Http\Controllers\WebhookController;

class StripeWebhookController extends WebhookController
{
    public function handleCustomerSubscriptionCreatedEvent($event)
    {

        $subscription = $event->data->object;
        $user = User::where('stripe_id',$subscription->customer)->first();

        // provision the subscription in the app
        
        return Response('All ok', 200);
    }
}

Please be aware that Stripe cannot guarantee that the events arrives at your endpoint in the correct order. One way to handle this is to create Jobs that will be executed when an event occur and the job should then be able to handle 'retry' in case a 'updated' event arrives before a 'created event' arrive.

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.