← Voltar ao blog

Como adicionar uma etapa customizada no checkout do Magento 2

Rodrigo Biazzi

O checkout Luma tem duas etapas: Shipping e Review & Payments. Às vezes o negócio precisa de uma terceira, por exemplo para confirmar a idade, escolher uma janela de entrega ou coletar dados B2B antes do pagamento. O Magento suporta isso pelo step navigator, um pequeno model JavaScript no qual toda etapa do checkout se registra.

Este artigo mostra como adicionar uma etapa no Magento Open Source e no Adobe Commerce 2.4.8 com o checkout Luma. Ele cobre o componente JavaScript, o template, o layout, a ordem das etapas e a parte que os tutoriais costumam pular: como validar e persistir o que a etapa coleta.

Pense duas vezes antes de adicionar uma etapa

Cada etapa extra é mais uma tela em que o cliente pode desistir. Antes de construir uma, veja se o dado cabe como um campo dentro de uma etapa existente. Um único campo na etapa de shipping dá menos código e menos atrito. Veja adicionar um campo customizado ao checkout para essa abordagem. Uma etapa nova faz sentido quando o conteúdo é grande, tem regras próprias ou precisa ser concluído antes de as opções de envio ou de pagamento poderem ser calculadas.

Como o step navigator funciona

Magento_Checkout/js/model/step-navigator mantém uma lista de etapas registradas. Cada etapa fornece a ele:

  • um código, usado como hash da URL, por exemplo #shipping
  • um alias, que pode ser null
  • um título para a barra de progresso
  • um observable do Knockout que controla a visibilidade
  • um callback de navigate, chamado quando o cliente volta à etapa pela barra de progresso ou pelo hash
  • uma ordem de classificação

O navigator ordena as etapas por essa ordem, mostra uma de cada vez e avança quando uma etapa chama stepNavigator.next(). Os componentes de shipping e de pagamento do core se registram do mesmo jeito. Leia Magento_Checkout/view/frontend/web/js/view/shipping.js e payment.js na sua versão para ver a ordem que eles passam e depois escolha um valor que posicione a sua etapa onde você quer.

O componente da etapa

Crie view/frontend/web/js/view/delivery-slot-step.js no seu módulo:

define([
    'ko',
    'uiComponent',
    'underscore',
    'Magento_Checkout/js/model/step-navigator'
], function (ko, Component, _, stepNavigator) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_DeliverySlot/delivery-slot-step'
        },

        isVisible: ko.observable(true),
        selectedSlot: ko.observable(''),

        initialize: function () {
            this._super();

            stepNavigator.registerStep(
                'delivery_slot',
                null,
                'Delivery Slot',
                this.isVisible,
                _.bind(this.navigate, this),
                15
            );

            return this;
        },

        navigate: function () {
            this.isVisible(true);
        },

        navigateToNextStep: function () {
            if (!this.selectedSlot()) {
                return;
            }

            stepNavigator.next();
        }
    });
});

O título passado para registerStep é uma string simples aqui. Em um módulo real, envolva com $t de mage/translate para que possa ser traduzido. A ordem 15 é o valor que a própria documentação do Magento usa para uma etapa entre as duas padrão. Confirme com os valores do core na sua versão, como descrito acima.

O template

Crie view/frontend/web/template/delivery-slot-step.html. O elemento externo precisa ter o código da etapa como id e um binding fadeVisible no observable que você registrou:

<li id="delivery_slot" data-bind="fadeVisible: isVisible">
    <div class="step-title" data-bind="i18n: 'Delivery Slot'" data-role="title"></div>
    <div id="checkout-step-delivery-slot" class="step-content" data-role="content">
        <form data-bind="submit: navigateToNextStep" novalidate="novalidate">
            <select data-bind="value: selectedSlot">
                <option value="" data-bind="i18n: 'Choose a slot'"></option>
                <option value="morning" data-bind="i18n: 'Morning'"></option>
                <option value="afternoon" data-bind="i18n: 'Afternoon'"></option>
            </select>
            <div class="actions-toolbar">
                <div class="primary">
                    <button data-role="opc-continue" type="submit" class="button action continue primary">
                        <span data-bind="i18n: 'Next'"></span>
                    </button>
                </div>
            </div>
        </form>
    </div>
</li>

Em um projeto real, os slots viriam do servidor, seja por window.checkoutConfig com um config provider, seja de um endpoint REST se dependerem do endereço. As opções fixas no código estão aqui só para manter o exemplo curto.

Adicionando a etapa ao layout

O componente precisa ser declarado como filho de steps em view/frontend/layout/checkout_index_index.xml:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="delivery-slot-step" xsi:type="array">
                                            <item name="component" xsi:type="string">Vendor_DeliverySlot/js/view/delivery-slot-step</item>
                                            <item name="sortOrder" xsi:type="string">2</item>
                                            <item name="children" xsi:type="array"/>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Existem duas ordens diferentes. A do layout decide a ordem em que os componentes filhos são renderizados no DOM. A passada para registerStep decide a ordem na barra de progresso e na navegação. Mantenha as duas consistentes, senão a barra de progresso e o conteúdo da página vão discordar.

Depois de adicionar os arquivos, rode bin/magento cache:clean layout full_page. Em production mode você também precisa fazer o deploy do conteúdo estático de novo para que o novo JS e o template sejam publicados. Com developer mode e pub/static/frontend limpo, um hard refresh basta. O artigo sobre UI components e noções básicas de Knockout explica como esses layouts em JSON viram componentes.

Colocando a etapa antes

Se a sua etapa precisa vir antes de Shipping, há um problema extra. O componente de shipping se define como visível ao inicializar, então no carregamento da página as duas etapas podem aparecer. A correção documentada pelo Magento é um mixin em Magento_Checkout/js/view/shipping que define visible como false no initialize quando o hash atual não é #shipping e volta para true no navigate. Teste o carregamento da página com e sem hash na URL e teste o botão voltar do navegador.

Validando e salvando o que a etapa coleta

Uma etapa que só vive em JavaScript não basta. O cliente pode recarregar, abrir o checkout em outra aba ou chamar a API diretamente. Três regras mantêm o dado confiável:

  1. Envie ao servidor antes de avançar. Em navigateToNextStep, chame seu próprio endpoint REST, espere o sucesso e então chame stepNavigator.next(). Mostre a mensagem de erro se a chamada falhar. Use Magento_Checkout/js/model/full-screen-loader durante a request.
  2. Guarde na quote. O endpoint deve salvar o valor na quote do carrinho atual, como coluna ou em uma tabela relacionada. Use /V1/carts/mine/... para clientes e uma rota de guest com o masked cart id para guests. Criando um endpoint REST customizado cobre o webapi.xml e a ACL para os dois.
  3. Valide de novo quando o pedido for feito. Um plugin ou observer na criação do pedido deve verificar se a quote tem um valor válido e recusar o pedido se não tiver. A validação em JavaScript é para o cliente. A validação no servidor é para o negócio.

Para restaurar a etapa depois de um reload, devolva o valor salvo junto com o checkout config e use-o como valor inicial do observable.

Depurando uma etapa que não aparece

Quando nada aparece, trabalhe de fora para dentro. Primeiro confira o console do navegador por erros do RequireJS, que geralmente indicam um caminho errado no valor de component ou template. Depois confira se a mudança de layout foi aplicada: no console, require('uiRegistry').get('checkout.steps.delivery-slot-step') deve retornar o seu componente. Se retornar undefined, o layout XML não foi mesclado, muitas vezes por um erro de digitação no aninhamento ou por um cache que não foi limpo. Se o componente existe mas a barra de progresso não tem o item novo, o registerStep não foi chamado, então confira o método initialize.

O que quebra etapas customizadas

  • Carrinhos virtuais. Se o carrinho só tem produtos virtuais, a etapa de shipping não é mostrada. Garanta que a sua etapa ainda faça sentido, ou esconda-a nesse caso verificando quote.isVirtual() de Magento_Checkout/js/model/quote.
  • Checkouts de uma etapa de terceiros. Extensões que substituem o layout do checkout geralmente ignoram etapas extras. Confirme com o fornecedor antes de construir.
  • Hyvä Checkout. Ele é construído em Magewire e tem uma forma própria de adicionar etapas, então nada deste código se aplica lá.
  • JS minificado ou em bundle. Se a sua etapa não aparece em produção mas funciona local, confira se o arquivo está incluído no conteúdo estático publicado e em qualquer configuração de bundling.

FAQ

Posso adicionar mais de uma etapa customizada?

Pode. Cada etapa se registra com seu próprio código, observable e ordem. Mantenha o número baixo, porque cada etapa adiciona atrito ao checkout.

Por que minha nova etapa aparece junto com a etapa de shipping?

Geralmente a etapa está colocada antes de Shipping e o componente de shipping ainda se define como visível ao carregar. Adicione um mixin na view de shipping que a esconda até o navigator chegar a ela.

Onde devo salvar os dados de uma etapa customizada?

Na quote, por meio do seu próprio endpoint REST chamado antes de ir para a próxima etapa. Copie para o pedido na criação e valide de novo no servidor.

Isso funciona com o Hyvä Checkout?

Não. O Hyvä Checkout não usa o step navigator do Luma nem templates Knockout. Ele tem sua própria configuração de etapas.

Atendo como freelancer em Magento e Adobe Commerce, incluindo customização de checkout. Se precisar de ajuda com isso, fale comigo.

Artigos relacionados