StripeInput

Greetings, I would like to create a StripeInput field for the Filament Form Builder. I would like to create a Payment Intent and then use it later to perform payments on the customer. I am currently stuck at the submission process, because the field should tackle into the form submission and after Filament validation do it's magic. Any help is appreciated, and also I would love to know more about the correct way to implement custom fields because I have a lot of ideas that I would like to share as plugins, both for standalone usage on the Filament Form and on the Filament Admin Builder.
14 Replies
Dan Harrin
Dan Harrin2y ago
essentially the state of the field in alpine needs to be entangled and then the state of the field stores the payment intent ID and that is accessible to PHP is there a way to get the nessecary ID for payment purely on the frontend without the backend call?
digitall_it
digitall_it2y ago
[edited] Still figuring details out. So far, I entangled the state of the field by using [edited] hidden input field. [edited] When changing the input value from the javascript, I had to resort to dispatchEvent an "input" event on the input field so that alpine updates the entangled data. Seems there's also some operations that need to be done on submit, I am still figuring out [edited]. It could be helpful to me to understand if it is possible to pass some object to the instance of the javascript class StripeInput (preferred) or the global instance of Livewire or alpine.js (less preferable) so I don't have to rely on the entanglement system via a proxy input field. Sorry, I edited the message to shorten it to not tire you with unnecessary details.
digitall_it
digitall_it2y ago
Right now I am receiving the ID of the intent, but I cannot pass it back to the Filament Form during the submission, the entangling doesn't work anymore and even livewire global events don't get passed.
Dan Harrin
Dan Harrin2y ago
can you send the code where you are entangling?
digitall_it
digitall_it2y ago
<x-dynamic-component
:component="$getFieldWrapperView()"
:bind="$getBind()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-action="$getHintAction()"
:hint-color="$getHintColor()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
id="{{ $getId() }}"
x-data="new window.StripeInput(
'{{ $getId() }}',
'{{ $getPublishableKey() }}',
'{{ $getIntentSecret() }}'
)" wire:ignore>
<input
id="{{ $getId() }}-card-boundfield"
type="text"
x-data="{}"
class="hidden"
readonly
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" />
<div id="{{ $getId() }}-card-element" class="filament-input StripeElement"></div>
<div id="{{ $getId() }}-card-errors" role="alert" class="text-red-500"></div>
</div>
</x-dynamic-component>
<x-dynamic-component
:component="$getFieldWrapperView()"
:bind="$getBind()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-action="$getHintAction()"
:hint-color="$getHintColor()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
id="{{ $getId() }}"
x-data="new window.StripeInput(
'{{ $getId() }}',
'{{ $getPublishableKey() }}',
'{{ $getIntentSecret() }}'
)" wire:ignore>
<input
id="{{ $getId() }}-card-boundfield"
type="text"
x-data="{}"
class="hidden"
readonly
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" />
<div id="{{ $getId() }}-card-element" class="filament-input StripeElement"></div>
<div id="{{ $getId() }}-card-errors" role="alert" class="text-red-500"></div>
</div>
</x-dynamic-component>
as you see I am entangling input with {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" and this works, but it is not elegant. the input is used as an hidden vessel to share information between the server and the client. unfortunately, when you press the submitAction of the Wizard, the entangling stops one idea I had is this. Remove the submitAction and replace it with a custom action that dispatches an event, this event is catched on the frontend that starts the card verification process. Once the card verification process is finished, an event on the frontend is dispatched that is catched by the backend on the form and the submission is performed. However this flow seems a bit fragile and I need to investigate some steps in it. The problem is also on publishing this component, it can be a difficult to understand workflow (albeit if you are incorporating credit card processing, you have to expect some minor things to change). At the moment the system retrieves the card SetupIntent ID, so it seems like the Stripe part is done! Also, if I had just to use this on my form, that uses the credit card field in a step, I could tackle into the next step logic and work my magic from there, but again it would not make it general purpose. I need to be sure the whole process is async, because the card verification process can show some UI depending on the card (for example, if it triggers the Strong Customer Authentication). you can see I have those setters and getters on the javascript class so I can make this.state = {...this.state, valid: false, error: event.error.message}; and than I can
set state(value) {
try {
this.boundField.value = JSON.stringify(value);
console.log('set state', value);
this.boundField.dispatchEvent(new Event('input', {bubbles: true}));
} catch (e) {
console.error("Failed to set state:", e);
}
}
set state(value) {
try {
this.boundField.value = JSON.stringify(value);
console.log('set state', value);
this.boundField.dispatchEvent(new Event('input', {bubbles: true}));
} catch (e) {
console.error("Failed to set state:", e);
}
}
digitall_it
digitall_it2y ago
in this screenshot a very simplified output with just one step. How über cool is that?
digitall_it
digitall_it2y ago
The checkbox on the top says "I don't have the credit card with me, I'll insert it later". It will send you an email and you can get back later to insert your credit card data for registration. As we are talking about a Filament Form, the sky is the limit and the credit card becomes a field just like any other! Unfortunately my specific use case is a bit more complicated because you don't directly pay, instead you register the card for future usage (just like you do on Amazon). But as releasing this as a plugin we can figure out the different modes. Also notice that I am using fake data and working on unsecured localhost via Sail, as I am using a test key.
handleSubmit = async (event) => {
console.log('handleSubmit', event);
event.preventDefault();

if (!this.state.stripeSetupIntentId) {
console.log('handleSubmit: no stripeSetupIntentId, calling confirmCardSetup');
event.stopPropagation();
await this.confirmCardSetup();
this.form.querySelector('button[type="submit"]').click();
}
}
handleSubmit = async (event) => {
console.log('handleSubmit', event);
event.preventDefault();

if (!this.state.stripeSetupIntentId) {
console.log('handleSubmit: no stripeSetupIntentId, calling confirmCardSetup');
event.stopPropagation();
await this.confirmCardSetup();
this.form.querySelector('button[type="submit"]').click();
}
}
ok now I feel like an hack. I managed to simply stop the propagation, then doing my magic, then clicking on the submit button again. Is this cheating? Can it be done better? I feel dirty. the good news is that this double submit workflow also handles the SCA (3D Secure) because it makes it async by using await in the confirmCardSetup. ah, I miss Angular so much... and I hate Angular so much!! also I would like to point out:
async confirmCardSetup() {
try {
const {setupIntent, error} = await this.stripe.confirmCardSetup(this.clientSecret, {
payment_method: {
card: this.stripeCardElement,
},
});

if (error) {
console.log("Stripe error:", error);
this.cardErrors.textContent = error.message;
this.state = {...this.state, valid: false, error: error.message};
} else {
console.log("Stripe Setup Intent:", setupIntent);
this.state = {...this.state, valid: true, stripeSetupIntentId: setupIntent.id, error: null};
window.livewire.emit('stripePaymentSetupIntentAssigned', {stripeSetupIntentId: setupIntent.id});
}
} catch (error) {
console.error('Error in confirmCardSetup:', error);
this.cardErrors.textContent = 'Si è verificato un errore durante la conferma del metodo di pagamento. Riprova.';
this.state = {...this.state, valid: false, error: 'Errore durante la conferma del metodo di pagamento'};
}
}
async confirmCardSetup() {
try {
const {setupIntent, error} = await this.stripe.confirmCardSetup(this.clientSecret, {
payment_method: {
card: this.stripeCardElement,
},
});

if (error) {
console.log("Stripe error:", error);
this.cardErrors.textContent = error.message;
this.state = {...this.state, valid: false, error: error.message};
} else {
console.log("Stripe Setup Intent:", setupIntent);
this.state = {...this.state, valid: true, stripeSetupIntentId: setupIntent.id, error: null};
window.livewire.emit('stripePaymentSetupIntentAssigned', {stripeSetupIntentId: setupIntent.id});
}
} catch (error) {
console.error('Error in confirmCardSetup:', error);
this.cardErrors.textContent = 'Si è verificato un errore durante la conferma del metodo di pagamento. Riprova.';
this.state = {...this.state, valid: false, error: 'Errore durante la conferma del metodo di pagamento'};
}
}
seems like window.livewire.emit is not catched during submit, I would have preferred to have some extension points in place, but I may simply have made something wrong. this.state = {...this.state, as I said is the code that makes the entangling and also doesn't work. Reading the documentation it seems like there is a phase before submission and after validation, but I think I can only access it server side. Even if I could dispatch an event at that point, the server uses code that is sync, while the flow is async and the server would not "await" for the frontend to answer. Sorry to bother you but I think I need to document all my mental process. It appears evident I am not using the full potential of alpine.js.
Dan Harrin
Dan Harrin2y ago
when i say entangle, i mean entangle in alpine $wire.entangle() you dont need a hidden input then
digitall_it
digitall_it2y ago
That’s exactly the point that I am missing I’ll look that up and report. I also made an example input field to perform a minimal use case debug The idea is that I can create a JavaScript class instance and pass some sort of reference to the entanglement so I can use that to communicate, but it’s obvious I need to study alpine.js more.
<script>
class ExampleInput {
state;

constructor(state) {
this.state = state;
console.log('constructor, state:', state);
setTimeout(this.test.bind(this), 5000);
}

test() {
this.state = 'executed test, changed state';
console.log('test, state:', this.state);
}
}
</script>

<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-action="$getHintAction()"
:hint-color="$getHintColor()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div id="$getId()" x-data="new ExampleInput($wire.entangle('{{ $getStatePath() }}').defer)">
</div>
</x-dynamic-component>
<script>
class ExampleInput {
state;

constructor(state) {
this.state = state;
console.log('constructor, state:', state);
setTimeout(this.test.bind(this), 5000);
}

test() {
this.state = 'executed test, changed state';
console.log('test, state:', this.state);
}
}
</script>

<x-dynamic-component
:component="$getFieldWrapperView()"
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-action="$getHintAction()"
:hint-color="$getHintColor()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div id="$getId()" x-data="new ExampleInput($wire.entangle('{{ $getStatePath() }}').defer)">
</div>
</x-dynamic-component>
in this minimal use case, I am passing the state in the instance. Then I am changing the state programmatically after 5 seconds. Should that reflect on the server side? If not, what I am not understanding? (please ignore the double root in component, this is a pseudocode example)
Dan Harrin
Dan Harrin2y ago
i dont know why you are using class ExampleInput that is not standard Alpine and is probably breaking it use Alpine.data() check the alpine docs
digitall_it
digitall_it2y ago
ExampleInput is very simplified, and of course it would not need a separate javascript class to hold its logic. But, coming back to StripeInput, there's a lot more involved there
digitall_it
digitall_it2y ago
So I used the hidden input to bind the state of the object. Also, I've checked out alpine.js "Data Binding" chapter here https://laravel-livewire.com/docs/2.x/properties#data-binding
Livewire
Properties | Livewire
A full-stack framework for Laravel that takes the pain out of building dynamic UIs.
digitall_it
digitall_it2y ago
and it looks like it binds to any element that dispatches an input event, so it actually needs that element to perform its entangling. Am I missing out something? I am so sorry for tiring you with those questions, I wanted to learn more about Filament Form. I fear that I couldn't model the behavior I need just inlining all the javascript so I used a class. By the way, thank you very much for pointing me out to Alpine.data(). I'll check that out!
Want results from more Discord servers?
Add your server