How to Use Stripe Elements With Livewire AlpineJS and Without It For Payment Processing in Laravel App?

We all know that most of us use Stripe Element to gather information about the transactions on our platforms securely or even while developing projects for others we have to test it. Here we will learn how to implement Stripe form submission using Livewire and Alpine and which allows users to register as well.

By the end of this tutorial you’ll come to know how to implement Stripe Elements completely on top of Laravel using Livewire and AlpineJS.

While doing this we have to take care of each event such as submitting the form, then validating the data and communicating that data with Stripe using JS and then sending it back to Livewire. Then the whole process starts.

Let’s review the bellow example for registration form first:

<form wire:submit.prevent="register">
    <div>
        <label for="name">
            Name
        </label>

        <input wire:model.lazy="name" type="text"
                class="w-full form-input @error('name') border-green-500 @enderror"
                id="name" name="name"/>

        @error('name')
        <p class="text-black-500 text-xs italic mt-4">{{ $message }}</p>
        @enderror
    </div>
    <div>
        <label for="email">
            Email Address
        </label>

        <input wire:model.lazy="email" type="email"
                class="w-full form-input @error('email')border-green-500 @enderror"
                id="email" name="email"/>

        @error('email')
        <p class="text-black-500 text-xs italic mt-4">{{ $message }}</p>
        @enderror
    </div>
 <div>
        <label for="password">
            Password
        </label>

        <input wire:model.lazy="password" type="password"
                class="w-full form-input @error('password')border-green-500 @enderror"
                id="password" name="password"/>

        @error('password')
        <p class="text-black-500 text-xs italic mt-4">{{ $message }}</p>
        @enderror
    </div>
    <div>
        <label for="password_confirmation">
            Confirm Password
        </label>

        <input wire:model.lazy="password_confirmation" type="password"
                class="w-full form-input @error('password_confirmation')border-green-500 @enderror"
                id="password_confirmation" name="password_confirmation"/>

        @error('password_confirmation')
        <p class="text-black-500 text-xs italic mt-4">{{ $message }}</p>
        @enderror
    </div>
    <div wire:ignore>
        <x-stripe-elements />
    </div>
    <div>
        <label>
            <input type="checkbox"
                    wire:model="terms_and_conditions">
            <span>I agree to the&nbsp;</span>
        </label>

        @error('terms_and_conditions')
        <p class="text-black-500 text-xs italic mt-4">{{ $message }}</p>
        @enderror
    </div>
<div>
        <button type="submit">
            Register
        </button>
    </div>
</form>

Next, consideration will be the Stripe Elements blade component:

<div x-data x-on:generate-stripe-token.window="generateStripeToken()">
    <label for="card">
        Credit Card
    </label>
    <div id="card-element">
        <!-- A Stripe Element will be inserted here. -->
    </div>
</div>
<div class="mt-2 text-sm text-black-500" id="card-errors">

</div>
@push('scripts')
    <script src="https://js.stripe.com/v3/"></script>
    <script type="text/javascript">
        // Create a Stripe client.
        var stripe = Stripe('{{ config('services.stripe.publishable_key') }}');

        // Let’s add an instance for  Elements
        var elements = stripe.elements();

        // We can customized styling for passing to options while adding an Element
       
         var style = ////// replaced this content for demo

        // Create an instance of the card Element.
        var card = elements.create('card', {style: style});

        // Add an instance of the card Element into the `card-element` <div>.
        card.mount('#card-element');

        // Handle real-time validation errors from the card Element. 
        card.addEventListener('change', function (event) {
            var displayError = document.getElementById('card-errors');
            if (event.error) {
                displayError.textContent = event.error.message;
            } else {
                displayError.textContent = '';
            }
   });

        // Now handling the submission of form
        function generateStripeToken() {
            stripe.createToken(card).then(function (result) {
                if (result.error) {
                    // Update user about the potential error that has happened
                    var errorElement = document.getElementById('card-errors');
                    errorElement.textContent = result.error.message;
                } else {
                    // Move the token ID to the server 
                    @this.set('stripe_token', result.token.id);
                }
            });
        }
    </script>
@endpush

At the end, the Livewire register component class:

<?php

namespace App\Http\Livewire\Auth;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Arr;
use Livewire\Component;

class Register extends Component
{
    public $name = '';
    public $email = '';
    public $password = '';
    public $password_confirmation = '';
    public $terms_and_conditions = false;
    public $stripe_token = null;

    protected $listeners = ['setStripeToken'];

    public function render()
    {
        return view('livewire.auth.register');
    }
protected function validationRules()
    {
        return [
            'terms_and_conditions' => 'accepted',
            'name' => ['required', 'string', 'max:255', 'min:2'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }

    public function updated($field)
    {
        $this->validateOnly($field, Arr::except($this->validationRules(), 'password'));
    }

    public function register()
    {
        $this->validate($this->validationRules());

        // Fire event that Alpine listens and tries 
        // to move the card data to Stripe. If successful will set 
        // $stripe_token in Livewire component
        $this->dispatchBrowserEvent('generate-stripe-token');

        if (! $this->stripe_token) {
            // Early exit here?
        }

        // Create a DB transaction, create user, attempt Stripe subscription
        DB::beginTransaction();

        $user = User::create([
            'name' => $this->name,
            'email' => $this->email,
            'password' => Hash::make($this->password),
        ]);
   try {
            
            $user->newSubscription('main', 'main')->create($this->stripe_token);
            DB::commit();

            event(new Registered($user));

            $this->guard()->login($user);
            return redirect()->route('dashboard');
            
        } catch (\Exception $e) {

            DB::rollback();
            // Send error back to user

        }
    }

    public function setStripeToken($stripeToken)
    {
        $this->stripe_token = $stripeToken;
    }
}

However if you are more of a Laravel Cashier kind of guy you, you can do that with Stripe Elements and Livewire. You’d notice component Stripe Elements that uses Alpine for Stripe token value to be passed to Livewire.
This is so far a good way to do it like for this code example we have added above Stripe Elements + Livewire/AlpineJS. Like I said earlier if you are Laravel Cashier kind of guy you can add that for ‘click event’ instead of Alpine. You can read detailed guide about Laravel Cashier here.

Here is another code code example for Stripe Elements with the use of Laravel Cashier instead of AlpineJS

public $paymentmethod;

public function updatedPaymentmethod()
    {
        if (filled($this->model->stripe_id)) {
            $this->add_or_update_payment_method();
        } else {
            // notify user error
        }
    }

    public function add_or_update_payment_method()
    {
        try {
            if (!$this->model->hasPaymentMethod()) {
                $this->model->addPaymentMethod($this->paymentmethod);
                $this->model->updateDefaultPaymentMethod($this->paymentmethod);
            } else {
                $this->model->updateDefaultPaymentMethod($this->paymentmethod);
            }
            // notify success
        } catch (\Exception $e) {
            // error handling
        }
    }
public function render()
    {
        return view('livewire.subscriptions.card', [
            'intent' => auth()->user()->createSetupIntent(),
        ]);
    }

Here is the script for card.blade.php

<div>
        <div>{{$error}}</div>
        <label class="block">
            <span>Card holder name</span>
            <input id="card-holder-name" placeholder="Jon Jones">
        </label>

        <label class="block">
            <span>Your card</span>
            <!-- Stripe Elements Placeholder -->
            <div wire:ignore id="card-element"></div>
            <div id="error-wrapper"></div>
        </label>

        <button id="card-button" data-secret="{{ $intent->client_secret }}">
            Register card
        </button>
 </div>

@push('scripts')
    <script src="https://js.stripe.com/v3/"></script>
    <script>

            const stripe = Stripe('{{config('services.stripe.key')}}')
            const elements = stripe.elements()
            const cardElement = elements.create('card')

            cardElement.mount('#card-element')

            const cardHolderName = document.getElementById('card-holder-name')
            const cardButton = document.getElementById('card-button')
            const clientSecret = cardButton.dataset.secret

            cardButton.addEventListener('click', async (e) => {
                const {setupIntent, error} = await stripe.confirmCardSetup(clientSecret, {
                    payment_method: {
                        card: cardElement, billing_details: {name: cardHolderName.value},
                    },
                })

                if (error) {
                    let errorWrapper = document.getElementById('error-wrapper')
                    errorWrapper.textContent = error.error
                    console.info(error)
                } else {
                    @this.set('paymentmethod', setupIntent.payment_method)
                }
            })
    </script>
@endpush

Please note that if you have stripe-elements(card input) render issue make sure to check the call @stack(‘scripts’) in the main app.