Trouble with the Wizard

This may be more of a Livewire, but maybe someone here can point me in the right direction. I am trying to use Stripe Elements on the second step of a Wizard. But when you click Next to get to step 2, the component gets re-rendered, and some Javascript needs to run again to reinitialize the Stripe elements. From the Stripe docs, I register their script and put the necessary JS code in a method called initElements which lives in the stripe-elements.js file. I am able to manually run the initElements method from the console, and then the Stripe Elements do appear. I'm wondering: Is there a way to prevent certain steps in a wizard from re-rendering when the next action is performed? Is there a better/different way of approaching this? What is the best way to call a javascript function after the component has been rendered? I did try listening for the next-wizard-step event in my page's class with the following code, but the Stripe Elements still wouldn't show
#[On('next-wizard-step')]
public function initStripe() {
$this->js('initElements()');
}
#[On('next-wizard-step')]
public function initStripe() {
$this->js('initElements()');
}
I am registering the JS like this:
FilamentAsset::register([
Js::make('stripe', 'https://js.stripe.com/basil/stripe.js'),
Js::make('stripe-elements', __DIR__ . '/../../resources/js/stripe-elements.js'),
]);
FilamentAsset::register([
Js::make('stripe', 'https://js.stripe.com/basil/stripe.js'),
Js::make('stripe-elements', __DIR__ . '/../../resources/js/stripe-elements.js'),
]);
This is the schema for my wizard:
Wizard::make([
Step::make('Order')
->icon(Heroicon::ShoppingBag)
->description('Review your order')
->schema([
View::make('filament.clusters.webstore.pages.review-order.order'),
]),
Step::make('Payment')
->schema([
View::make('filament.clusters.webstore.pages.review-order.payment'),
]),
Step::make('Confirm')
->schema([
View::make('filament.clusters.webstore.pages.review-order.confirm'),
]),
])
->submitAction(view('filament.clusters.webstore.pages.review-order.submit-button'))
]);
Wizard::make([
Step::make('Order')
->icon(Heroicon::ShoppingBag)
->description('Review your order')
->schema([
View::make('filament.clusters.webstore.pages.review-order.order'),
]),
Step::make('Payment')
->schema([
View::make('filament.clusters.webstore.pages.review-order.payment'),
]),
Step::make('Confirm')
->schema([
View::make('filament.clusters.webstore.pages.review-order.confirm'),
]),
])
->submitAction(view('filament.clusters.webstore.pages.review-order.submit-button'))
]);
1 Reply
kschwab
kschwabOP2mo ago
The previous issue was able to be resolved by simply adding wire:ignore to the root element I wanted Livewire to.. ignore for re-rendering. 🤦 Simple enough. Now I'm stuck trying to figure out what is the right way of calling an Alpine function when the Next action button is clicked, but before requesting the next step? On step two, I am collecting the billing information and when Next step is clicked, the information is sent to Stripe via an ajax request. I don't want the wizard to proceed to the next step until that request succeeds. Since the click listener for the Next action is tied to the button's parent div (vendor/filament/schemas/resources/views/components/wizard.blade.php:172), I'm having a hard time figuring out how to do this. Below is the code I currently have, but it doesn't feel right, and while it does solve this scenario, the return '$dispatch(\'confirm-stripe\', $event)'; line causes the following error when submitting on the final step Filament\Actions\Exceptions\ActionNotResolvableException: An action tried to resolve without a name.. If I comment out that line, it submits, but then the Stripe ajax call is obviously never made.
Wizard::make([
Step::make('Order')
->schema([
View::make('filament.clusters.webstore.pages.review-order.order'),
]),
Step::make('Billing')
->schema([
View::make('filament.clusters.webstore.pages.review-order.billing'),
]),
Step::make('Confirm')
->schema([
View::make('filament.clusters.webstore.pages.review-order.confirm'),
]),
])
->nextAction(function(Action $action, Wizard $component) {
$action
->alpineClickHandler(function () use ($component) {
if ($component->getCurrentStepIndex() === 1) {
return '$dispatch(\'confirm-stripe\', $event)';
}

return null;
});
})
->submitAction(view('filament.clusters.webstore.pages.review-order.submit-button'))
]);
Wizard::make([
Step::make('Order')
->schema([
View::make('filament.clusters.webstore.pages.review-order.order'),
]),
Step::make('Billing')
->schema([
View::make('filament.clusters.webstore.pages.review-order.billing'),
]),
Step::make('Confirm')
->schema([
View::make('filament.clusters.webstore.pages.review-order.confirm'),
]),
])
->nextAction(function(Action $action, Wizard $component) {
$action
->alpineClickHandler(function () use ($component) {
if ($component->getCurrentStepIndex() === 1) {
return '$dispatch(\'confirm-stripe\', $event)';
}

return null;
});
})
->submitAction(view('filament.clusters.webstore.pages.review-order.submit-button'))
]);
billing.blade
<x-filament-panels::page wire:ignore>
<form id="payment-form" x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('stripe-elements') }}"
x-data="stripeElementsComponent({
publicKey: @js(config('services.stripe.key')),
stripeConfirmationToken: $wire.{{ $applyStateBindingModifiers("\$entangle('{$this->stripeConfirmationToken}')") }},
stripeCustomerSession: @js($this->stripeCustomerSession),
})" @init-stripe.window="initStripe($event.detail)"
@confirm-stripe.window="confirmStripe($event.detail) && requestNextStep()">
<div id="payment-element">
<!-- Elements will create form stripeElements here -->
</div>
<div id="error-message">
<!-- Display error message to your customers here -->
</div>
</form>
</x-filament-panels::page>

@assets
<script src="https://js.stripe.com/basil/stripe.js"></script>
@endassets
<x-filament-panels::page wire:ignore>
<form id="payment-form" x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('stripe-elements') }}"
x-data="stripeElementsComponent({
publicKey: @js(config('services.stripe.key')),
stripeConfirmationToken: $wire.{{ $applyStateBindingModifiers("\$entangle('{$this->stripeConfirmationToken}')") }},
stripeCustomerSession: @js($this->stripeCustomerSession),
})" @init-stripe.window="initStripe($event.detail)"
@confirm-stripe.window="confirmStripe($event.detail) && requestNextStep()">
<div id="payment-element">
<!-- Elements will create form stripeElements here -->
</div>
<div id="error-message">
<!-- Display error message to your customers here -->
</div>
</form>
</x-filament-panels::page>

@assets
<script src="https://js.stripe.com/basil/stripe.js"></script>
@endassets
stripe-elements
export default function stripeElementsComponent({
publicKey,
stripeConfirmationToken,
stripeCustomerSession = null,
elementsContainer = '#payment-element',
errorContainer = '#error-message',
}) {
return {
stripe: null,
stripeElements: null,
stripeConfirmationToken,
initStripe(amount) {
...
},
handleError(error, submitBtn) {
const messageContainer = document.querySelector(errorContainer);
messageContainer.textContent = error.message;
submitBtn.disabled = false;
},
async confirmStripe(event) {
event.stopPropagation();
const submitBtn = event.target;

if (submitBtn.disabled) {
return false;
}

submitBtn.disabled = true;

const {
error: submitError
} = await this.stripeElements.submit();

if (submitError) {
handleError(submitError, submitBtn);
return false;
}

// Create the ConfirmationToken using the details collected by the Payment Element
const {
error,
confirmationToken
} = await this.stripe.createConfirmationToken({
elements: this.stripeElements,
params: {
payment_method_data: {
billing_details: {
name: '{{ auth()->user()->fullName }}',
email: '{{ auth()->user()->email }}',
},
},
}
});

if (error) {
handleError(error, submitBtn);
return false;
}

this.stripeConfirmationToken = confirmationToken;
return true;
}
}
}
export default function stripeElementsComponent({
publicKey,
stripeConfirmationToken,
stripeCustomerSession = null,
elementsContainer = '#payment-element',
errorContainer = '#error-message',
}) {
return {
stripe: null,
stripeElements: null,
stripeConfirmationToken,
initStripe(amount) {
...
},
handleError(error, submitBtn) {
const messageContainer = document.querySelector(errorContainer);
messageContainer.textContent = error.message;
submitBtn.disabled = false;
},
async confirmStripe(event) {
event.stopPropagation();
const submitBtn = event.target;

if (submitBtn.disabled) {
return false;
}

submitBtn.disabled = true;

const {
error: submitError
} = await this.stripeElements.submit();

if (submitError) {
handleError(submitError, submitBtn);
return false;
}

// Create the ConfirmationToken using the details collected by the Payment Element
const {
error,
confirmationToken
} = await this.stripe.createConfirmationToken({
elements: this.stripeElements,
params: {
payment_method_data: {
billing_details: {
name: '{{ auth()->user()->fullName }}',
email: '{{ auth()->user()->email }}',
},
},
}
});

if (error) {
handleError(error, submitBtn);
return false;
}

this.stripeConfirmationToken = confirmationToken;
return true;
}
}
}

Did you find this page helpful?