I have a form that sends a request to Stripe, in order for my users to add a payment method:
<form id="card-form">
<label>Card details</label>
<div id="card-element"></div>
<button wire:target="addPaymentMethod" wire:loading.class="hidden" class="button round" id="card-button">Add payment card</button>
<!-- Loading spinner -->
<svg wire:target="addPaymentMethod" wire:loading class="inline-block animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</form>
The (stripped down) JavaScript looks like below:
const stripe = Stripe('stripe-public-key');
const elements = stripe.elements();
const form = document.getElementById('card-form')
const cardButton = document.getElementById('card-button');
const cardElement = elements.create('card');
cardElement.mount('#card-element');
form.addEventListener('submit', async (e) => {
e.preventDefault();
cardButton.disabled = true;
const {
setupIntent,
error
} = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardNumber,
billing_details: {
name: 'Oliver'
}
}
}
);
if (error) {
// Display error.message to the user...
cardButton.disabled = false
console.log(error.message)
} else {
// The card has been verified successfully...
@this.addPaymentMethod(setupIntent.payment_method)
}
});
If the response in the async method above is successful, it will trigger the below method in the Livewire component:
public function addPaymentMethod($paymentMethod)
{
//Create the payment method in Stripe...
auth()->user()->addPaymentMethod($paymentMethod);
//Other validation etc...
}
The output of all of this is captured in the below gif. As you can see here, the user enters the credit card details and click on the "Add payment card" and is redirected to another page.
The problem is, that the loading state starts for a while, but disappears before the actual request/redirect is done.
How can I show the loading spinner until the actual request to Stripe is completed?
It depends on what else happens in the addPaymentMethod
as to why your loading state is hiding early. To give you more control over that timing I'd probably defer the showing/hiding of your loading state to the javascript layer. Here's a couple of ways to approach it:
One of the nice features of Livewire is that your component methods, when called via javascript, return a promise when they've completed. So if you need to hide the loading state you can do this here:
// The card has been verified successfully...
@this.addPaymentMethod(setupIntent.payment_method)
.then(result => {
// Hide loading state
})
Another way to go would be to emit an event from the addPaymentMethod
that you could listen to in your components frontend.
public function addPaymentMethod($paymentMethod)
{
// Your add payment code runs...
// Emit an event to the frontend (with optional data)
$this->dispatchBrowserEvent('add-payment-method-complete', ['someData' => $value]);
}
You can listen for this event in the frontend very simply using AlpineJs. Here's a brief form example using Alpine to manage the loading state:
<form
// Init AlpineJs with the x-data attribute
x-data="{
isLoading: false
}"
// Listen the forms submit event and change
// our "isLoading" flag to true
x-on:submit="isLoading = true">
// Listen for the event emitted from Livewire to the window
// and reset the isLoading flag to false
<svg x-on:add-payment-method-complete.window="isLoading = false" />
</form>