Skip to content

Introduction

We've implemented a significant change compared to other checkout solutions, particularly Luma. Instead of leaving the final responsibility for placing an order to the payment methods, we've opted to remove this requirement. This decision was driven by our commitment to providing merchants with maximum flexibility in designing their checkout process. With this change, merchants are no longer constrained to having the payment method at the last step, along with a "place order" button.

This adjustment has offered us two main advantages. Firstly, it grants merchants the freedom to position payment methods at any stage of the checkout process, be it the initial step or subsequent ones. Secondly, it empowers us to assume control of the order placement workflow, enabling a more precise orchestration of actions and providing developers with a streamlined API.

Central to this API is the "Place Order Service" (POS-API), an optional feature tailored to each payment method. Merchants have the choice to utilize their unique service or fallback onto our default option. In essence, the POS manages the final steps of order placement, including redirecting the customer to the order success page and any subsequent actions required.

Why use it?

The POS provides complete control over post-frontend validation processes once an order is ready to be created. It enables dynamic decision-making regarding order placement, destination routing, customer redirection, and exception handling. Additionally, since version 1.1.13, we've integrated our Evaluation API. This enhancement allows you to issue specific instructions to the frontend before or after order placement.

How does it work?

During the checkout process, most of the default components work to collect all necessary information to process the quote and potentially convert it into an order. Towards the end of the checkout, the primary navigation button, sometimes a "step to" button, automatically transitions to a "place order" button.

The primary navigation is part of a wrapping component called Main, located at \Hyva\Checkout\Magewire\Main. This component manages various tasks, including navigation actions and order placement via a "POS Processor," designated for @internal use and not intended for external use.

The processor handles the placement of the order based on the payment method specified on the quote. It consults the \Hyva\Checkout\Model\Magewire\Payment\PlaceOrderServiceProvider to obtain the corresponding POS, with a default fallback to \Hyva\Checkout\Model\Magewire\Payment\DefaultPlaceOrderService if none is found.

Ultimately, based on the provided POS, the processor attempts to place the order, assigns the final order ID, and optionally manages any exceptions as specified by the POS.

Maintain consistency in how you handle order placement

We've encountered numerous situations where a PSP or a payment method autonomously converted the quote into an order. However, we do not recommend this practice due to the extensive flexibility we've built into our checkout product, which relies on specific rules to function properly. In other words, if this conversion occurs, the checkout process may not behave as expected.

This caution is warranted because unlike the Luma checkout, payment methods are not always presented in the final step. If the quote is converted into an order midway through the checkout process, crucial information intended for subsequent steps will not be processed or visible to the customer.

Example scenario

Consider a scenario where an order has been successfully placed, but the customer needs to undergo a 3DS security workflow displayed in a modal before being redirected. This would create the following scenario:

  1. Custom Place Order Service tailored to our payment method
  2. Ability to initiate order placement
  3. Control over redirecting customers to the order success page
  4. Execution of a frontend JavaScript Promise to display a modal
  5. Waiting for customer completion of the 3DS workflow (status: awaiting 3DS)
  6. Optional display of an error dialog if issues arise
  7. Redirection of the customer to the order success page upon completion
Some optional considerations beyond scope

Additional considerations, though not currently in scope, include implementing a cronjob to periodically check for pending orders stuck in "awaiting 3DS" status for, say, over an hour. If found, customers would receive an email, offering them the option to complete their transaction through a custom route. This route would offer the same 3DS functionality minus the modal popup. It could reside within the customer account page or be accessed via a custom route, requiring a valid hash in conjunction with the email address and/or order ID.

3DS authentication

As previously noted, if no custom payment method has been mapped, payment processing will be managed by the default place order service. You can locate the default place order service at \Hyva\Checkout\Model\Magewire\Payment\DefaultPlaceOrderService.

Where does 3DS Authentication stand for?

3D Secure (3DS) authentication is an additional security measure used in online e-commerce checkouts to prevent fraud. When a customer enters their payment details and submits their order, the 3DS system prompts the cardholder's bank to verify the customer's identity. This verification can involve entering a password, receiving a one-time password (OTP) via SMS or email, or using biometric methods like fingerprint or facial recognition.

If the authentication is successful, the transaction proceeds and the payment is processed. If it fails, the transaction is declined. 3DS significantly reduces the risk of fraud, enhances customer trust, and often shifts liability for fraudulent transactions from the merchant to the issuing bank.

1. Create a new service

We strongly recommend extending your service from our abstraction class. This approach allows you to implement only the necessary changes without concerns about additional functionality.

In this example, we've assumed that a payment method with the code 'foo' already exists and has a valid renderer. Further details on creating a custom payment method can be found here.

<?php

class FooPlaceOrderService extends \Hyva\Checkout\Model\Magewire\Payment\AbstractPlaceOrderService
{
    public function canRedirect(): bool
    {
        return false; // To prevent automatic redirection after the order is placed during checkout.
    }

    public function evaluateCompletion(EvaluationResultFactory $resultFactory, ?int $orderId = null): EvaluationResultInterface
    {
        // Incorporate a navigation task to be executed immediately after all validations are successfully completed.
        $redirect = $resultFactory->createRedirect('checkout/onepage/success');

        if ($orderId === null) {
            return $redirect;
        }

        // Initially, trigger the 3DS validation Promise on the frontend.
        $validate = $resultFactory->createValidation('foo-authentication');
        // For demonstration purposes, we still redirect the customer to the success page when the authentication fails (optional).
        $validate->withFailureResult($redirect);

        // Create a navigation task that serves as a wrapper Promise to execute the redirect once the validation is complete.
        $navigationTask = $resultFactory->createNavigationTask('foo-redirect', $redirect);
        // The 'executeAfter' method is only required before version 1.1.18.
        $navigationTask->executeAfter(true);

        return $resultFactory->createBatch()
            ->push($validate)
            ->push($navigationTask);
    }
}

2. Map the new service

<!-- File: etc/frontend/di.xml -->

<type name="Hyva\Checkout\Model\Magewire\Payment\PlaceOrderServiceProvider">
    <arguments>
        <argument name="placeOrderServiceList" xsi:type="array">
            <!-- This item name should correspond to the payment method code. -->
            <item name="foo" xsi:type="object">
                My\Example\Model\Payment\PlaceOrderService\FooPlaceOrderService
            </item>
        </argument>
    </arguments>
</type>

3. Authentication modal

To complete the example, we will display a modal containing some 3DS authentication features. Additionally, we'll attach a new validation to the framework that will be automatically triggered when the order is placed. This validation was added during step one.

For the modal, we're going to use a regular Hyvä modal.

Modals should not be placed within the payment method template.

Please note that the HTML for the modal dialog cannot be located within the payment method template. This is because the payment method may not always reside where the "Place Order" button is located. To circumvent this issue, we recommend placing it at the bottom of the DOM outside of the Main component.

<!-- File: view/frontend/layout/hyva_checkout_index_index.xml -->

<referenceContainer name="hyva.checkout.init-validation.after">
    <!-- Inject a block that will automatically render within a container specifically designated for validations. -->
    <block name="modals.three-ds.authentication"
           template="My_Example::modals/three-ds/authentication.phtml"
    />
</referenceContainer>

Now let's create a basic Hyvä modal.

<div>
    <div x-data="initFooAuthentication()" x-init="initialize()">
        <div x-cloak x-spread="overlay()" x-bind="overlay()" class="fixed inset-0 flex items-center justify-center text-left bg-black bg-opacity-50">
            <div x-ref="dialog" role="dialog" aria-labelledby="foo_three-ds_authentication_dialog-label" class="inline-block max-w-2xl mx-4 max-h-screen overflow-auto bg-white shadow-xl rounded-lg p-6 text-gray-700">
                <div class="w-full font-medium text-gray-700 not-required">
                    <label for="authentication_code">
                        Authentication Code
                    </label>

                    <div class="flex items-center gap-4">
                        <input type="text" x-model="code" id="authentication_code" class="form-input"/>
                    </div>
                </div>

                <div class="flex gap-y-2 md:gap-x-2 mt-6 w-full">
                    <button type="button" class="btn btn-primary" x-on:click="ok">
                        Confirm Payment
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>

And the corresponding Alpine authentication component.

<script>
    /*
     * Component is designed to be compatible with both Alpine v2.x and v3.x.
     */
    function initFooAuthentication() {
        return Object.assign(
            {
                code: null
            },

            hyva.modal(),

            {
                initialize() {
                    window.addEventListener('foo:authenticate:show', event => {
                        this.show()
                            .then(result => {
                                this.$dispatch('foo:authenticate:confirm', {
                                    result: result && this.code === '1234'
                                })
                        })
                    })
                }
            }
        )
    }
</script>

In the end, we must integrate this with a new validator that can be injected. This can be accomplished either within the same PHTML file where the Alpine component resides or in a new one.

<script>
    function initFooAuthentication() {...}

    // Wait for the Evaluation frontend to complete its initialization.
    window.addEventListener('checkout:init:evaluation', () => {

        // The validator name "foo-authentication" corresponds to the name specified in the FooPlaceOrderService::evaluateCompletion method.
        hyvaCheckout.evaluation.registerValidator('foo-authentication', element => {
            return new Promise((resolve, reject) => {

                // Dispatch an event to display the authentication dialog.
                window.dispatchEvent(
                    new Event('foo:authenticate:show')
                )

                // Wait for confirmation from the modal on whether its result is true or false.
                window.addEventListener('foo:authenticate:confirm', event => {
                    event.detail.result ? resolve() : reject()
                })
            })
        })
    })
</script>