Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here
Premium Members Only
Join to unlock this tutorial and all of our courses.
Tutorial Premium Tutorial

Filament: Stripe One-Time Payment Form with Elements

November 21, 2023
25 min read

Tutorial last revisioned on July 02, 2024 with Filament v3

Filament is great for admin panels, but what if you want to use it as an e-shop with payments? In this tutorial, we will show how to integrate Stripe one-time checkout into Filament.

Filament Stripe Payment Element

Prepare for quite a long "ride" because there's a lot of work to implement it properly, with all the JS/PHP/Livewire elements, validation, and webhooks.


Table of Contents

  1. Data Preparation: Models/Migrations/Factories
  2. Filament Product Resource
  3. Custom Filament Pages: Checkout and Payment Status
  4. Back-end: Stripe PHP Library and Payment Intent
  5. Checkout Page: Stripe.js, Form and Stripe Elements
  6. Handle the Submit Event
  7. Show Payment Status
  8. Post-payment Events: Order Approval and Webhooks

Ready? Let's dive in!


1. Data Preparation

Our goals in this section:

  • Create Product and Order Model
  • Add relationships
  • Populate products table
  • Create a ProductResource list and view pages
  • Add the Buy product to view page

That is how our final pages should look like.

List Products

View Product

1.1. Migrations, Factories and Models

First, let's create Models; only the Product will have a factory.

php artisan make:model Product -mf
php artisan make:model Order -m

Then, update the migrations.

database/migrations/XXXXXX_create_products_table.php

Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->bigInteger('price');
$table->timestamps();
});

database/migrations/XXXXXX_create_orders_table.php

use App\Models\Product;
use App\Models\User;
 
// ...
 
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Product::class);
$table->foreignIdFor(User::class);
$table->bigInteger('amount');
$table->timestamps();
});

In ProductFactory, we define column data. Notice that we store the price in cents.

database/factories/ProductFactory.php

public function definition(): array
{
return [
'name' => fake()->words(3, asText: true),
'price' => rand(999, 9999),
];
}

app/Models/User.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
// ...
 
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}

And then add relationships into Models.

app/Models/Product.php

use Illuminate\Database\Eloquent\Relations\HasMany;
 
// ...
 
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}

app/Models/Order.php

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
// ...
 
protected $fillable = [
'product_id',
'user_id',
'amount',
];
 
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

1.2. Database Seeder

Now let's update DatabaseSeeder.

database/seeders/DatabaseSeeder.php

use App\Models\Product;
use App\Models\User;
 
public function run(): void
{
User::factory()->create([
'name' => 'Admin',
'email' => '[email protected]',
]);
 
Product::factory(100)->create();
}

And seed the database.

php artisan migrate:fresh --seed

2. Filament Product Resource

Generate ProductResource and view pages using the Artisan command. Filament doesn't create a view page by default, so we must add the --view flag when creating a resource.

php artisan make:filament-resource Product --view

Implementation of the resource file:

app/Filament/Resources/ProductResource.php

namespace App\Filament\Resources;
 
use App\Filament\Resources\ProductResource\Pages;
use App\Models\Product;
use Filament\Infolists\Components\Actions;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use NumberFormatter;
 
class ProductResource extends Resource
{
protected static ?string $model = Product::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
TextEntry::make('name'),
TextEntry::make('price')
->formatStateUsing(function ($state) {
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY);
 
return $formatter->formatCurrency($state / 100, 'eur');
}),
Actions::make([
Action::make('Buy product')
->url('/'),
]),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name'),
TextColumn::make('price')
->formatStateUsing(function ($state) {
$formatter = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY);
 
return $formatter->formatCurrency($state / 100, 'eur');
}),
])
->actions([
Tables\Actions\ViewAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListProducts::route('/'),
'view' => Pages\ViewProduct::route('/{record}'),
];
}
}

Filament's infolists can use...

Premium Members Only

This advanced tutorial is available exclusively to Laravel Daily Premium members.

Premium membership includes:

Access to all premium tutorials
Video and Text Courses
Private Discord Channel

Comments & Discussion

IM
Ismail Mahmoud ✓ Link copied!

Hello everyone, im living in country which unfortunately is'nt supported by stripe. So will this tutorial works for me (just for testing)?

M
Modestas ✓ Link copied!

You should ask Stripe support as we are not sure.

A
Arnaud ✓ Link copied!

Awesome ! Thank you

WC
WILLY CHANDRA NEGARA ✓ Link copied!

I got an error

Property [$stripe] not found on component: [app.filament.resources.product-resource.pages.checkout]

    protected function getStripeCustomer(User $user): Customer

    {

        if ($user->stripe_customer_id !== null) {

            return $this->stripe->customers->retrieve($user->stripe_customer_id);

        }



        $customer = $this->stripe->customers->create([

            'name' => $user->name,

            'email' => $user->email,

        ]);



        $user->update(['stripe_customer_id' => $customer->id]);



        return $customer;

    }
M
Modestas ✓ Link copied!

Did you add:

protected StripeClient $stripe;

To your file?

WC
WILLY CHANDRA NEGARA ✓ Link copied!

I apologize i have a typo on the $stirpe. I also have another question, I learned that we must store a price in decimal right ? And yeah i know this is just a tutorial and make it simple, but i tried to change the price and amount to be decimal so far so good untill the payment step i got an error saying something like "amount" is not an integer. Mind explaining for me ?

DL
David Lun ✓ Link copied!

You can store price in integer or decimal. You don't really need this change if you're concerned about data type used.

Never store price in float or double.

M
Mondayx5 ✓ Link copied!

Hi, Thank you for the tutorial could you please also provide the full working code. I follow the guide but got this error Invalid value for elements(): clientSecret should be a client secret of the form ${id}secret${secret}. Y

J
Joe ✓ Link copied!

The Stripe Elements script code in section 5.3 is missing a closing }); The correct code is:

    <script>
        document.addEventListener("DOMContentLoaded", function(event) {
            const stripe = Stripe("{{ config('services.stripe.key') }}", { apiVersion: '2023-10-16' });

            const elements = stripe.elements({ clientSecret: '{{ $clientSecret }}' });
            const paymentElementOptions = { layout: "tabs" };
            const paymentElement = elements.create("payment", paymentElementOptions);
            paymentElement.mount("#payment-element");
        });
    </script>
E
Evan ✓ Link copied!

Would it be possible to have the checkout form pop up in a modal, rather than a separate page? I'm having trouble figuring out how to call the view from a modal, while first calling the Checkout class.

ND
Nils Domin ✓ Link copied!

Thanks for this great tutorial!

The links to the "Practical Laravel Queues on Live Server" and "Queues in Laravel" courses in section "8.2 Webhooks" have the wrong urls. They direct to "laraveldaily.test", but should direct to "laraveldaily.com".

M
Modestas ✓ Link copied!

Thank you for letting us know, fixed!

ND
Nils Domin ✓ Link copied!

You're welcome! :-)

ND
Nils Domin ✓ Link copied!

How can I send an invoice (or receipt) to a customer after payment success?

GI
Gaetan Inserra ✓ Link copied!

Changed this :

return $this->stripe->customers->retrieve($user->account->stripe_customer_id; 

into :

$stripe_customer_id = $this->stripe->customers->retrieve($user->account->stripe_customer_id);
if ($stripe_customer_id->deleted == false) return $stripe_customer_id;

juste in case customer is deleted from stripe but not from your users table.

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.