Hi all,
For many of my subscription or payment-based projects, we use Stripe Elements to securely collect payment information.
While I was able to create a working version of a registration + payment form using Livewire and Alpine, I’m interested to know what the best practices are or some ways that you guys have implement Stripe Elements.
The tricky part is the order of events - submit the form, validate the data, communicate with Stripe via JS, send the stripe token back to Livewire, attempt payment, etc.
Here is an example registration form:
<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-red-500 @enderror"
id="name" name="name"/>
@error('name')
<p class="text-red-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-red-500 @enderror"
id="email" name="email"/>
@error('email')
<p class="text-red-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-red-500 @enderror"
id="password" name="password"/>
@error('password')
<p class="text-red-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-red-500 @enderror"
id="password_confirmation" name="password_confirmation"/>
@error('password_confirmation')
<p class="text-red-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 </span>
</label>
@error('terms_and_conditions')
<p class="text-red-500 text-xs italic mt-4">{{ $message }}</p>
@enderror
</div>
<div>
<button type="submit">
Register
</button>
</div>
</form>
Next, our 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-red-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') }}');
// Create an instance of Elements.
var elements = stripe.elements();
// Custom styling can be passed to options when creating 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 = '';
}
});
// Handle form submission.
function generateStripeToken() {
stripe.createToken(card).then(function (result) {
if (result.error) {
// Inform the user if there was an error.
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// Send the token to your server.
@this.set('stripe_token', result.token.id);
}
});
}
</script>
@endpush
Finally, our 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 will listen to and attempt
// to send 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;
}
}
- Does this seem like the correct approach to you? Are there are changes you would make?
- Is the “flow” correct, as far as the order of steps being taken, for validation, communication with Stripe JS, etc.?
I’d love to hear any thoughts you guys have.
Thanks,
Cory