Skip to main content

Yuno Frontend / SDK Integration

Summary

Yuno is the only gateway in Farfalla that operates entirely in-page: the card form is rendered inside the checkout without redirecting the user to an external site. This is achieved with the Yuno JS SDK, controlled from an Alpine store that communicates with a Livewire component acting as a bridge to the backend.

The integration has three layers: the SDK loader (a Blade view that injects the Yuno script), a global Alpine store that handles one-off payments, and a helpers module for subscription and enrollment flows. Everything is protected by the feature flag yuno_checkout_enabled: if the tenant has it disabled, the SDK is not loaded and the checkout falls back to the other gateways.

The SDK exposes three mounting modes depending on the flow: a full checkout with a payment method selector for one-off payments, a simplified card-only form for subscriptions without a trial, and an enrollment mode that validates the card without a charge (used for trials and payment method changes).

SDK Loading

The SDK is loaded via a defer script pointing to the Yuno CDN. The loader emits custom events on window to notify when the SDK has finished loading or failed. This allows the Alpine store to wait for the SDK to be ready before initializing it, without polling globals. The public key is not injected in the loader: it travels from the backend to the frontend together with each checkout session.

The loader is included in two views with a functional Yuno flow: checkout v2 and the My Account page (for the payment method change modal). A third view, the classic cart, also includes the loader, but Yuno does not work end-to-end there: the classic views do not render the container where the SDK needs to mount, nor a Yuno button. There is code in the classic cart Livewire component that dispatches the Yuno event, but it is unreachable from the UI.

Alpine Store and SDK Singleton

The yunoCheckout store is registered globally when Alpine initializes on storefront pages. It holds the checkout state (idle, loading, mounted, processing, completed, error, cancelled), UI flags, tokens and session data received from the backend, and a map of error messages translated from PHP.

State transitions are natural: it starts at idle, passes through loading and mounted while the SDK initializes, reaches processing when the user submits, and ends at completed. From any intermediate state it can fall to error (SDK failure, decline, network error) or cancelled (when the user explicitly cancels or the SDK reports cancellation).

To avoid mounting the SDK more than once on the same page, the component keeps a singleton of the active instance outside the store. If a new session arrives with a different token than the one already mounted, the frontend unmounts and remounts. This typically happens when the user changes currency during checkout.

The SDK loading wait combines polling and the loader's custom events, with a 10-second timeout. If the SDK does not respond within that time, the flow falls to error and the user sees a "Try again" button.

Communication Between Livewire, Alpine, and the SDK

The flow always starts from the Livewire component. The backend requests a session from Yuno and dispatches a window event that the Alpine store listens to. Alpine initializes the SDK, mounts the form, and registers the callbacks. When the SDK invokes a callback (typically when obtaining the token or finishing 3DS), Alpine calls back into Livewire, which executes the corresponding backend operation: create the payment, verify the result, create the subscription, etc.

The window events connecting the layers are:

EventWhen firedPayload
yunoCheckoutReadyOne-off paymentTokens and session data
yunoCheckoutReadyRecurringSubscription (trial or non-trial)Same as above + trial flag
yunoCheckoutCancelledUser cancels
yunoCheckoutLockedLock/unlock the parent checkoutBoolean lock state

Why Alpine resolves the Livewire component dynamically

The Alpine store is global, but the SDK callbacks need to invoke methods on the specific Livewire component instance that initiated the checkout. Yuno mounts in two different components — the checkout v2 Livewire and the My Account modal — which share a similar API but are different classes. To avoid coupling to a specific case, the store walks up the DOM from the container where the SDK mounted until it finds the Livewire component that contains it. Since that container always lives inside the correct Livewire, the lookup always returns the instance that started the flow.

One-off Checkout Flow

When the user clicks "Pay", the Yuno form Livewire component starts the flow. First it checks the checkout lock and returns if a payment is already in progress. It also returns without doing anything if the cart is not purchasable.

The Livewire component handles several paths depending on context. If the user applied a 100% coupon, there is nothing to charge and a "free checkout" is processed without going through Yuno. If the payment method selected in the cart is not Yuno — for example Stripe or MercadoPago — the Yuno form delegates to the main checkout to perform the redirect to the corresponding gateway. Only when the method is Yuno does the flow described here begin.

The first step is requesting a checkout session from the Yuno API, which returns a session token and the public key. If customer creation in Yuno fails at that step, the flow aborts before touching the SDK: the error is logged to the yuno_api channel and shown to the user in the form.

With the session created, Livewire locks the checkout and dispatches an event that the Alpine store listens to. The lock prevents the user from modifying the cart — items, currency, shipping, billing — while the payment is in progress. The coupon is outside the lock: the user can apply or remove coupons even with an active flow. The lock propagates from the Yuno form to the parent checkout component, and from there to all cart inputs via Blade props.

The UI enforces the lock passively: when active, the remove-item button is hidden, the currency selector is not rendered at all, the edit shipping and billing buttons are disabled with a non-clickable style, and the "back" button is replaced with an inert icon. The lock is released when the flow ends, whether by success, error, or cancellation.

Alpine receives the event, waits for the SDK to be ready, and mounts the form in the container exposed by the Blade view with wire:ignore so that Livewire does not touch it during re-renders. When the user fills in the card and submits, the SDK passes an OTT to the frontend. Alpine forwards the token to Livewire, which posts to the Yuno payments endpoint.

The response can take three paths. If Yuno approves the payment and returns a redirect URL, the frontend redirects immediately to the backend receivePayment, which finalizes the user's plan. If Yuno indicates that an additional step is needed — typically 3DS — Alpine tells the SDK to continue the flow and the SDK shows the challenge inside the same iframe. If the payment was declined, failed, or rejected without additional action, the frontend shows an error toast and the user can retry; the "Try again" button resets the state to idle without reloading the page.

After the 3DS challenge, the SDK invokes a callback with the final result. If it arrives as successful, the frontend asks the backend to verify the payment and return the redirect URL; any other state is treated as an error and shown to the user.

Enrollment Flows

Yuno exposes an "enrollment" mounting mode that validates card data without generating a charge. This is used in two flows: trial (subscription that does not charge until the trial period ends) and payment method change in My Account.

Trial

For plans with a free trial, the backend returns a customer session instead of a checkout session, and marks the flow as a trial. The frontend recognizes the flag and mounts the SDK in enrollment mode, where the user enters the card and the SDK validates it without charging. When enrollment succeeds, the SDK returns a vaulted token. The frontend sends it to the backend, which creates the subscription in Yuno marked as "do not charge initially" so that Yuno waits until the trial ends before triggering the first charge. If enrollment returns without a token or with an error, the flow falls to error and the subscription is not created.

Subscription without trial (workaround to get 3DS in subscriptions)

The Yuno API for creating subscriptions does not support 3DS on the first charge. Since 3DS is required for compliance and to reduce chargebacks, that API is not used directly. Instead, an alternate two-step flow is implemented:

  1. The first cycle is charged as a one-off payment using the SDK Payment Lite, which supports 3DS in-page. The backend sends this payment with vault_on_success: true so that, when the charge is confirmed, Yuno stores the card and returns a vaulted token. (Additionally it sends a stored_credentials block with reason: SUBSCRIPTION, usage: FIRST that Yuno propagates down the payment chain; the exact downstream effect depends on Yuno and the issuing bank, so it is not documented as causality.)
  2. When the one-off is confirmed — including the 3DS challenge if applicable — the backend retrieves the vaulted token from the payment and only then calls the Yuno subscription API, creating the subscription with that token as the payment method. Subsequent recurring charges are triggered automatically by Yuno using that vaulted token, without user intervention.

From the user's perspective the flow looks like a single checkout: the SDK mounts the simplified card-only form — without a payment method selector — the user pays, goes through 3DS if required, and the subscription becomes active. The internal two-step split is invisible.

For trials, the problem does not apply because there is no first charge, so the Yuno enrollment API is used directly (see previous section).

Payment method change in My Account

From My Account the user can replace the card of an active subscription without cancelling or recreating it. The flow opens a modal that reuses the same SDK enrollment used for trials.

When the modal mounts, the backend generates an enrollment session against the Yuno API with the user's customer. The frontend uses that session to initialize the SDK in enrollment mode. When the user confirms the card, the SDK returns a new vaulted token to the frontend, which sends it to the backend to replace the token associated with the subscription. If enrollment fails — invalid card, bank rejection, etc. — the modal shows the error and the subscription remains intact with the previous payment method.

SDK Events We Handle

CallbackContextBehavior
onLoadingAllToggles the store state between loading and mounted
yunoPaymentMethodSelectedOne-offIf it is a card with no saved method, auto-triggers submit
yunoCreatePaymentOne-off and non-trial subscriptionPasses the OTT to the backend to create the payment
yunoPaymentResultOne-off post-3DS and non-trial subscriptionOne-off: asks the backend to verify and redirect. Subscription: asks to create the subscription
yunoEnrollmentStatusTrial and card changeIf a token arrived, triggers subscription creation or payment method replacement. Without token or error: modal shows error
yunoErrorAllUser cancellation: resets state. Other errors: shows error

There is no explicit "render finished" callback: the signal arrives through onLoading turning off.

Session Types and Public Key

The SDK requires a different session token depending on the flow. Enrollments (trial and card change) use a customer session, which represents the user in Yuno and allows listing and saving payment methods. Standard payments (one-off and non-trial subscription) use a checkout session, tied to a specific transaction.

In all cases, the backend generates the token and sends it to the frontend together with the gateway's public key, configured in the tenant settings.

3DS / SCA

Yuno handles 3D Secure transparently without an external redirect: the challenge is shown inside the same iframe that is already mounted. The flow is: the backend creates the payment, Yuno responds indicating that additional action is needed, the frontend tells the SDK to continue, the SDK manages the challenge, and on completion invokes the callback with the result.

3DS is available in flows where there is a charge: the full SDK (mountCheckout) used for one-off, and Payment Lite (mountCheckoutLite) used for the first charge in the subscription workaround. Enrollment Lite (mountEnrollmentLite) does not do 3DS because it generates no charge — it is card enrollment only. The Yuno subscription creation API also does not support it, which is why for non-trial subscriptions the workaround described above is used: the first cycle is charged as a one-off (with 3DS via Payment Lite) and the subscription is created only afterwards using the resulting vaulted token.

The payment id is persisted in a window variable because the post-challenge callback is a new SDK invocation that does not have access to the previous closure.

Differences With Other Gateways

FeatureStripeMercadoPagoPayUYuno
Integration typeRedirect to external checkoutRedirect to external checkoutDynamic form submitIn-page SDK
Card captureStripe pageMP pagePayU pageEmbedded in Farfalla
3DSHandled by StripeHandled by MPHandled by PayUIn-page with continuePayment
Enrollment without chargeNot applicableNot applicableNot applicableYes, used for trials
Card changeNot implementedNot implementedNot applicableIn-page modal

The key difference is that Yuno does not take the user off the page, which requires the Alpine ↔ Livewire ↔ SDK communication layer described in this document.

UI Edge Cases

  • SDK does not load: after the 10-second timeout, the store falls to error and the user sees a retry button. Typical causes are network issues, ad blockers, or a Yuno CDN outage.
  • Feature flag disabled: the loader is not rendered, the Yuno component is not mounted, and the checkout shows the standard button.
  • Customer creation in Yuno fails: covered in the one-off flow — the flow aborts before reaching the SDK, with the error logged and shown to the user.
  • Session changes mid-flow (for example, currency change): the frontend detects the token change, unmounts the SDK, and remounts.
  • Double click on "Pay": the button is disabled while the flow is in progress. The lock check at the start of the Livewire action is an additional guard against re-entry.
  • Cart modifications during payment: the global lock described in the main flow hides or disables controls for items, currency, shipping, and billing while a payment is in progress. The coupon is outside the lock.
  • Enrollment without token: if the SDK reports successful enrollment but does not return the vaulted token, it is treated as an error and the subscription is not created.
  • Multiple checkout instances on the same page: the SDK stores the payment id in global window variables to survive the post-3DS callback. Two coexisting instances would collide, though this is an unlikely scenario.

File References

FileRole
resources/views/modules/yuno-sdk-library.blade.phpSDK loader
resources/js/yuno-libraries.jsHelpers for subscription and enrollment
resources/js/alpine/alpine-components/yuno-checkout-v2-component.jsAlpine store for one-off
resources/js/alpine/alpine-storefront.jsStore registration
resources/views/livewire/checkout-v2/yuno-payment-form.blade.phpYuno form view in checkout
resources/views/livewire/checkout-v2/checkout-v2.blade.phpMain checkout view
resources/views/livewire/my-account/change-yuno-payment-method.blade.phpCard change modal
app/Http/Livewire/CheckoutV2/YunoPaymentForm.phpCheckout form logic
app/Http/Livewire/CheckoutV2/CheckoutV2.phpMain checkout logic
app/Http/Livewire/MyAccount/ChangeYunoPaymentMethod.phpCard change modal logic
app/Domains/Commerce/Gateways/Yuno/YunoService.phpMain gateway service
app/Domains/Commerce/Gateways/Yuno/YunoRecurringPaymentService.phpSubscriptions and enrollment
app/Domains/Commerce/Gateways/Yuno/YunoOneOffPaymentService.phpOne-off payments
app/Domains/Commerce/Gateways/Yuno/YunoBaseService.phpBase, connector, currency conversion
routes/web.phpreceivePayment route
X

Graph View