Skip to content

Compra, carrinho, frete e pagamentos ​

Regras comuns ​

  • Hooks podem aparecer em estruturas visuais diferentes. Não são necessários nomes de classes do Aura, fontes, cores, presets ou settings.
  • data-* comercial vem do contexto Liquid do servidor. Escape valores dinâmicos. data-cart-endpoint, data-shipping-endpoint, data-checkout-endpoint e data-cart-url devem usar caminhos da mesma origem com storefront_url, preservando o namespace da prévia.
  • Use botões com type explícito, labels, inputs nativos e mensagens com role="status"/aria-live="polite". Botões fora de submissões devem ter type="button".
  • Mensagens são atribuídas como texto, nunca HTML. data-state nas mensagens indica info, loading, success, error ou vazio; o CSS decide sua aparência. Controles de requisição recebem disabled e aria-busy.
  • Não esconda erros do backend nem reimplemente headers, cookies, fetch, limites comerciais, recuperação, polling ou idempotência no tema. A sessão é mantida pelo servidor e pelo navegador; não há tokens de autenticação do painel no runtime.
  • O runtime não oferece mini-cart nem wishlist. A conta do comprador usa os hooks documentados na seção Conta do comprador V3. O contador de carrinho é compartilhado; um drawer visual pode ser criado pelo tema. Busca, categorias, filtros e paginação usam os formulários GET e URLs fornecidos pelo servidor, sem outro controlador comercial no navegador.

Produto, opções e quantidade ​

Raiz: [data-product-detail]. Atributos comerciais: data-product-id, data-requires-selection, data-price-visible, data-product-has-price; para inclusão, data-cart-endpoint="{{ '/api/cart/items' | storefront_url }}" e data-cart-url="{{ '/carrinho' | storefront_url }}".

HookContrato
[data-product-quantity]Input numérico com min, value e max quando houver limite. Obrigatório para compra.
[data-product-quantity-error]Mensagem de validação da quantidade; necessária para validação imediata.
[data-add-to-cart], [data-cart-message]Botão e mensagem necessários para inclusão.
[data-option-group]Em produto variável, um grupo por dimensão; data-group-index único na raiz.
[data-option-id]Botão dentro do grupo; data-option-id e data-option-name; data-option-image opcional.
[data-option-selection]Texto opcional da opção selecionada no grupo.
[data-product-variant]Um elemento por variante do DTO, com data-variant-id, data-option-ids separados por vírgula, data-in-stock, data-purchasable, data-minimum-quantity, data-maximum-quantity quando não nulo, data-price-amount/data-price-display apenas quando o preço é público. Pode ficar oculto.
[data-product-status], [data-product-selection-message], [data-product-price]Obrigatórios na seleção variável: disponibilidade, erro de combinação e preço.
[data-product-price-prefix], [data-product-compare-price]Opcionais; ocultados quando a variante tem preço público.
[data-product-low-stock]Aviso opcional dentro da raiz; texto e estilo livres. Renderizar hidden quando product.lowStock não for verdadeiro e manter o elemento para futuras seleções. Usar role="status"/aria-live="polite".
[data-gallery-media]Container da mídia principal do produto. Necessário para o vídeo: o runtime substitui seus filhos pelo player e os devolve depois.
[data-gallery-video]Botão com data-gallery-video="{{ product.video.embedUrl }}" e data-gallery-video-title opcional. Ao clicar, o runtime insere um <iframe> do YouTube em [data-gallery-media], marca is-active/aria-pressed e desmarca os [data-gallery-image]. Clicar em uma imagem ou escolher uma opção restaura a mídia original. Somente URLs youtube.com/embed ou youtube-nocookie.com/embed são aceitas.

Para o alerta de estoque baixo, cada [data-product-variant] declara o booleano data-low-stock, usando variant.lowStock ou uma condição Liquid sobre variant.stock com o limite visual escolhido pelo tema. Ausência do atributo equivale a falso. O runtime alterna apenas hidden em todos os avisos da raiz, sem calcular estoque nem alterar texto. Seleções incompletas/ambíguas, variantes esgotadas, sem compra ou sem preço público ocultam o aviso. Tema sem o hook continua funcionando. O alerta é nativo do Node, sem aplicativo ou chave de ativação. Sua presença no Liquid controla a exibição. Os limites e a diferença em relação ao legado estão no manual canônico.

O runtime resolve combinações, mantém data-selected-variant-id e data-selected-variant-has-price na raiz, ajusta quantidade mínima/máxima e bloqueia compra sem seleção, preço ou quantidade válida. Opções impossíveis recebem disabled/aria-disabled; opções sem compra recebem is-unavailable; a escolhida recebe is-selected e aria-pressed. O status recebe data-state="selection|unavailable|available|order". Nenhum preço é recalculado somando valores de variantes.

Evento storefront:product-option-change na raiz, com bubbling e detail: { imageUrl }, informa uma escolha de opção. Serve para atualizar a galeria sem acoplar a lógica comercial à apresentação. O Aura preserva sua escolha manual de foto, thumbnails e zoom no JS visual. O tema pode ignorar esse evento e usar sua própria apresentação de mídia.

Quick add, linhas e contador de carrinho ​

Quick add: raiz [data-product-card] com data-product-id, data-minimum-quantity, data-cart-endpoint="{{ '/api/cart/items' | storefront_url }}"; filhos obrigatórios [data-quick-add] e [data-quick-add-message]. Ofereça quick add apenas para produtos simples compráveis conforme o DTO; variantes são escolhidas no detalhe. O runtime envia o mínimo e atualiza [data-cart-count] pelo quantityTotal retornado (não pelo número de linhas); zero oculta o contador.

Linha: [data-cart-line] com data-line-key e data-cart-endpoint="{{ '/api/cart/items/' | storefront_url }}". [data-cart-quantity] é um input com limites do DTO; [data-cart-remove] é o botão de remoção. Os dois são opcionais individualmente; inclua [data-cart-message] para apresentar falhas. PATCH e DELETE recarregam a página após sucesso, para reobter preços e totais do servidor. A inclusão pelo detalhe navega para data-cart-url.

Há um bloqueio compartilhado por documento para mutações, com restauração dos controles e aria-busy. data-cart-was-disabled é transitório/interno e não deve ser estilizado ou alterado. Não há coordenação entre abas: o carrinho atual usa cookie stateless e essa limitação anterior permanece. Nenhum evento de carrinho adicional é necessário para os componentes atuais.

Cupom ​

Raiz form[data-cart-coupon], método POST e action/data-coupon-endpoint apontando para {{ '/api/cart/coupon' | storefront_url }}. Filhos: input [data-coupon-code] com maxlength 30, botão submit [data-coupon-apply], botão opcional [data-coupon-remove] e mensagem [data-coupon-message] com aria-live. O runtime envia somente código, serializa a mutação com as demais operações e recarrega após sucesso. Prévia continua bloqueada antes da rede. Dados de cupom e desconto vêm do servidor; Liquid só formata e escapa.

No checkout, a raiz recebe data-coupon-applied, data-coupon-code, data-coupon-amount e data-coupon-free-shipping. A quantia é somente a do cupom: use cart.coupon.amount quando presente, com fallback cart.discount.amount nas revisões antigas. data-coupon-eligible mantém a expectativa de um cupom elegível que perdeu para uma promoção maior (amount zero, applied false). O runtime envia expectedCoupon para detectar alterações; esses valores não autorizam desconto.

A raiz também recebe data-discount-type (tipo de cart.promotion, desconto_cupom, desconto_frete_gratis ou vazio) e data-discount-amount (cart.discount.amount em centavos). Eles formam expectedDiscount: { type, amount }, com type null quando vazio. O servidor revalida preço, regras e sessão. Resumo atualizado usa [data-checkout-subtotal], [data-checkout-coupon-row] (região do desconto vencedor, inclusive automático), [data-checkout-discount-label], [data-checkout-coupon-code] e [data-checkout-discount]. O rótulo vem de cart.promotion.label ou “Desconto do cupom”; só mostre código se applied true. promotion_changed retorna o carrinho para revisão e mantém a tentativa. Revisões antigas sem expectativa recebem uma revisão quando existe promoção automática; atualizar o tema por nova publicação apresenta o contrato completo. Erros recuperáveis com cart atualizado reapresentam o resumo sem apagar dados pessoais, invalidam o frete selecionado e bloqueiam envio quando o cupom ficou inválido. Inclua link para revisar/remover no carrinho. Não gerar outra tentativa para contornar conflito de idempotência.

CEP, frete e totais ​

Raiz de cálculo: form[data-shipping-calculator], com data-shipping-endpoint="{{ '/api/cart/shipping-quotes' | storefront_url }}".

Obrigatórios: input [data-shipping-postal-code], botão submit [data-shipping-submit], mensagem [data-shipping-message] e container [data-shipping-options]. O CEP é validado como oito dígitos e formatado ao sair do campo, sem movimentar o cursor durante a digitação. CEP inválido recebe aria-invalid e foco.

Resumo opcional (um por página): [data-cart-summary] com data-subtotal-amount inteiro em centavos, data-currency e data-shipping-required="true|false". Filhos opcionais [data-cart-shipping] e [data-cart-total] mostram frete e total estimado. Para cupom, acrescente data-total-amount="{{ cart.total.amount }}": é o total de produtos já descontado, calculado pelo servidor. data-subtotal-amount continua sendo bruto. A cotação devolve o total final por modalidade; o runtime apenas o apresenta.

A plataforma gera labels [data-shipping-option="id"] com radio [data-shipping-option-input], conteúdo [data-shipping-option-content] (nome e prazo), e preço em b. A seleção recebe is-selected e radio checked. O CSS pode reordenar esses elementos e estilizar os hooks. Essa estrutura é funcional e neutra; não contém classes do Aura. Na próxima cotação o conteúdo do container é substituído.

As modalidades personalizadas ativas do painel usam os mesmos controles de seleção: nome, valor e deliveryDescription vêm da cotação. O servidor aplica limites de compra/peso, CEP, UF/região, capital/interior e adicionais cadastrados; o tema não calcula essas regras. Uma entrega manual elegível independe das medidas e da origem exigidas por transportadoras automáticas. A barra de frete grátis não habilita nem desabilita essas modalidades.

Editar CEP ou alterar/remover uma linha limpa a seleção e invalida a estimativa. Requisições antigas são abortadas e respostas atrasadas são ignoradas. O checkout recota antes de criar pedido. Não há persistência de seleção de frete do carrinho nem eventos de frete adicionais no contrato atual.

Checkout ​

Raiz: [data-checkout] com data-checkout-attempt opaco fornecido pelo servidor, data-checkout-endpoint="{{ '/api/checkout/orders' | storefront_url }}" e data-shipping-required="true|false".

Obrigatórios: form[data-checkout-form], [data-checkout-submit], [data-checkout-message], [data-checkout-total] com data-subtotal="{{ cart.total.amount }}" em centavos (produtos após desconto vencedor) e [data-checkout-shipping-value]. O formulário fornece inputs com name: name, email, phone, document, postalCode, street, number, complement, neighborhood, city, state. Preserve os atributos HTML de obrigatoriedade e limites do contrato da página.

Quando requer frete, inclua [data-checkout-quote] com data-shipping-endpoint e o container [data-checkout-shipping]. O botão finalizar começa desabilitado e é liberado pela seleção. O total final de cada modalidade é recebido do servidor, em centavos BRL. Quando não há frete, a plataforma envia no-shipping e valor zero.

O runtime monta o payload, envia tentativa opaca e valor esperado de frete, bloqueia submissões concorrentes e navega para confirmação pelo token retornado. shipping_changed exige recotar e selecionar novamente. Repetições manuais usam a mesma tentativa de checkout do servidor; não há geração local de outra tentativa nem retry automático. Não existem eventos adicionais de checkout neste contrato.

Permissões comerciais de pessoa física/jurídica ​

O servidor fornece shop.customerPolicy.individual (CPF) e .company (CNPJ), conforme as flags de bloqueio administradas no painel. O Liquid deve oferecer somente os tipos habilitados no cadastro/perfil e identificar o documento aceito no checkout. Ambos falsos impedem novos cadastros/compras. Mantenha os hooks e nomes personType / document: a proteção é revalidada pelo backend, inclusive para visitantes e contas existentes. Uma recusa person_type_not_allowed usa os canais de erro dos formulários existentes, sem requisições adicionais no tema.

Pagamentos: Pix e cartão ​

Raiz: [data-payment-pix] com data-payment-order="{{ confirmation.orderId }}", apenas numa confirmação autorizada pelo servidor. Este ID é uma proteção de contexto, não uma credencial e não substitui a sessão autorizada.

Obrigatórios:

HookElemento/comportamento
[data-payment-form]Formulário de seleção, inicialmente hidden.
[data-payment-options]Fieldset ou container dentro do formulário. Recebe labels [data-payment-option] e radios gerados. Pode conter legend fixo; só opções geradas são removidas.
[data-payment-message]Mensagem de estado/erro fora do formulário, para continuar visível durante a cobrança.
[data-payment-start]Botão submit, inicialmente disabled.
[data-payment-details]Container fora do formulário, inicialmente hidden.
[data-payment-code]Input ou textarea readonly dentro dos detalhes.

Opcionais: título [data-payment-order-status="<orderId>"] (pode ficar fora da raiz Pix; recebe Pedido <orderId> <orderStatusLabel> do DTO atual), imagem [data-payment-qr], botão [data-payment-check], botão [data-payment-copy], texto [data-payment-expiration] e texto [data-payment-total]. Botões opcionais usam type="button". Sem QR, o Pix Copia e Cola continua funcionando. Sem botão copiar, o comprador pode selecionar o texto manualmente.

O hook opcional [data-payment-card] define a posição do formulário de cartão. Use um id único, inicialmente hidden e fora do formulário de seleção. O provedor selecionado define o formulário nesse container: Brick para Mercado Pago, formulário com tokenização direta para Pagar.me ou formulário com cartão criptografado pelo SDK oficial para PagSeguro transparente (PagBank). Se o hook estiver ausente, o runtime cria o container dentro da raiz de pagamento ao selecionar cartão, inclusive em revisões já instaladas; se faltar apenas o id, gera um. A ausência desse hook não esconde cartão disponibilizado pela API. Habilitação, credenciais e regras comerciais continuam sendo validadas pelo backend.

A seleção de cartão Mercado Pago carrega https://sdk.mercadopago.com/js/v2 e monta o Card Payment Brick com a Public Key e limites vindos da API autorizada. O parcelamento segue o limite de até 12 parcelas do checkout, combinado com o teto e o valor mínimo da parcela cadastrados no admin. Um teto maior no admin (por exemplo, 24) não oculta o cartão: a API oferece até 12, sem alterar o cadastro. Dados brutos de cartão permanecem nos campos do provedor. O runtime envia somente bandeira, parcelas, token transitório e identificação de dispositivo quando disponível. A seleção/preparação vincula o método ao pedido; valor e elegibilidade continuam sendo calculados pelo servidor. Montagem/desmontagem são serializadas: troca de opção ou saída da página invalida callbacks antigos. Erros do SDK não são logados com seus payloads. Preview não carrega SDK nem consulta pagamentos.

No Pagar.me, o runtime monta campos acessíveis para número, nome, validade, CVV e parcelas sem juros. Envia dados de cartão diretamente a https://api.pagar.me/core/v5/tokens?appId=<publicKey>, sem Authorization; só token, bandeira e parcelas seguem para o Node. O backend converte o token no cofre do Pagar.me e cobra pelo identificador do cartão; o tema não recebe esse identificador nem oferece reuso de cartões salvos. Os campos são limpos após tentativa ou saída da página, sem armazenamento local nem logs dos dados. O domínio precisa estar habilitado para tokenização no Pagar.me. Não existe desafio 3DS interativo neste formulário. Prévia não monta formulário nem tokeniza.

No PagSeguro transparente, o runtime monta campos acessíveis para número, nome, validade, CVV e parcelas sem juros, carrega https://assets.pagseguro.com.br/checkout-sdk-js/rc/dist/browser/pagseguro.min.js e chama PagSeguro.encryptCard com a chave pública da loja vinda da API autorizada. Só o cartão criptografado, o nome do titular e as parcelas seguem para o Node; os campos são limpos após cada tentativa. Não há débito nem 3DS. Prévia não monta o formulário nem criptografa.

Boleto Pagar.me, PagSeguro transparente e PagHiper usam as mesmas opções, preparação, retomada e polling do runtime. O hook opcional [data-payment-boleto], inicialmente hidden e fora do formulário de seleção, posiciona linha digitável, botão de cópia, link e vencimento. Se ausente, o runtime cria o container. showBoleto e boleto: { line, url } no DTO autorizam sua exibição; não interpretar estados financeiros no tema. A confirmação financeira continua vindo da API, nunca da emissão do boleto. A plataforma filtra os links retornados pelo provedor e mantém cada emissão vinculada à tentativa do pedido. Os estilos .payment-pagarme-form, .payment-pagseguro-form e [data-payment-boleto] pertencem ao CSS do tema; os temas oficiais já os incluem. Publicar nova revisão para atualizar CSS.

PagHiper usa o cadastro de boleto por e-mail do painel existente. Não oferece Pix ou cartão nesse contrato. O servidor confirma o pagamento pelo retorno autenticado do provedor; o polling da página lê o estado persistido. Se a emissão ficar incerta, a interface bloqueia outra cobrança e orienta conferência. O tema não deve criar links de boleto, reenviar emissão nem interpretar notificações.

A Carteira PicPay usa method: wallet, com preparação, envio único e acompanhamento pelo runtime. O hook opcional [data-payment-wallet], inicialmente hidden e fora do formulário, posiciona o link Pagar no PicPay e a validade. Se ausente, o runtime cria o container. showWallet e wallet: { url } autorizam a exibição. O link abre somente o checkout HTTPS validado do PicPay em outra aba; não é Pix Copia e Cola. Confirmação, expiração, revisão e retomada vêm do backend. Instruções são removidas quando deixam de ser exibíveis. Nenhum token PicPay chega ao navegador. O CSS de [data-payment-wallet] pertence ao tema; os temas oficiais incluem estilos do link e foco, aplicáveis às lojas por nova revisão. Prévia não consulta nem cria pagamentos. A plataforma controla também wallet/prepare e wallet/start; o tema não chama essas rotas por conta própria.

As formas manuais cadastradas no painel (Pix/depósito e pagamento personalizado) usam o mesmo seletor. O personalizado usa o título cadastrado no painel, como “Pagar na entrega”, inclusive quando existem várias opções personalizadas. Ao selecionar, o runtime apresenta as instruções e o valor antes da confirmação. Ao confirmar, chama manual/select e registra a escolha aguardando conferência. O personalizado não exige chave Pix, conta bancária ou credenciais de gateway. As opções respeitam ativação, exibição, tipo de pessoa e limites do cadastro. O hook opcional [data-payment-manual] permite escolher a posição dessas instruções; se ausente, o runtime cria o container dentro da raiz de pagamento, inclusive em revisões já instaladas. Os dados textuais são inseridos como texto; o HTML das instruções é sanitizado no servidor para parágrafos, listas e ênfase, sem atributos. Trocar para uma opção online ou recarregar as opções oculta as instruções da seleção anterior. A confirmação recupera o título e as instruções registrados na escolha, mesmo que o cadastro seja alterado depois.

Os adicionais percentual e fixo do cadastro manual aceitam valores negativos como descontos. O percentual incide sobre os produtos após o cupom, sem frete; o desconto combinado fica limitado a esse saldo. O valor apresentado e persistido é calculado no servidor. O desconto Pix específico de gateways não se aplica às formas manuais.

A escolha manual fica aguardando conferência pelo lojista no painel existente. Não cria cobrança em gateway, não confirma pagamento, não baixa estoque e não agenda polling financeiro. Recarregar a confirmação recupera a escolha persistida e consulta a situação atual do pedido. Instruções deixam de ser apresentadas quando o pedido é aprovado, cancelado ou fica indisponível. A interface mostra indisponibilidade temporária quando opções cadastradas falham na validação técnica; o servidor registra o motivo por loja/associação, sem credenciais.

A plataforma controla card/prepare, card/start, /api/payments/options, pix/prepare, pix/start, pix/current, pix/check e pix/qr, headers X-LojaVirtual-Storefront/X-Payment-Order, cookies da mesma origem e crypto.randomUUID() usado na mesma tentativa de prepare/start. O contexto do pedido também é validado em options.current.

Reload consulta opções/tentativa existente. Poll consulta somente o estado local a cada dez segundos quando shouldPoll é verdadeiro; não cria cobrança nem consulta o provider. Consulta manual chama check com intervalo mínimo de quinze segundos. Após resposta perdida, consulta current sem repetir criação; se também perder essa consulta, mantém bloqueio até recuperação ou reload. A disponibilidade de uma nova tentativa vem de blocksNewPayment, não de um mapa de estados do tema.

Quando a tentativa permite novo pagamento, a seleção anterior é descartada e o formulário permanece oculto/bloqueado até receber novas opções. Isso também vale para retorno pelo bfcache. Após a recarga, é obrigatório selecionar uma opção novamente; falha na consulta mantém o bloqueio. O evento de estado continua refletindo blocksNewPayment do backend, sem autorizar reutilizar opções antigas durante essa atualização.

data-payment-state na raiz reflete o estado recebido. statusMessage vem do backend; showPix e presença do código controlam os detalhes. O formulário/controles são bloqueados por tentativa existente, execução ou resultado incerto. Código/QR são retirados quando deixam de ser exibíveis. Falha no QR orienta a usar Copia e Cola; falha no clipboard seleciona o texto e informa o erro. Pagehide cancela timers; retorno pelo bfcache reconsulta opções sem criar Pix.

Evento storefront:payment-state-change, com bubbling, tem detail: { state, blocksNewPayment, showPix, shouldPoll }. Use apenas para efeitos visuais. Não altere flags, não calcule estados financeiros e não inicie outra cobrança por esse evento. Não inclui código Pix ou credenciais.

Exemplo mínimo sem JS do tema:

liquid
<section data-payment-pix data-payment-order="{{ confirmation.orderId }}">
  <p data-payment-message role="status" aria-live="polite"></p>
  <form data-payment-form hidden>
    <fieldset data-payment-options><legend>Pagamento</legend></fieldset>
    <button data-payment-start type="submit" disabled>Gerar Pix</button>
  </form>
  <div data-payment-manual hidden></div>
  <div data-payment-card hidden></div>
  <div data-payment-boleto hidden></div>
  <div data-payment-wallet hidden></div>
  <div data-payment-details hidden>
    <label>Código Pix <textarea data-payment-code readonly></textarea></label>
    <button data-payment-copy type="button">Copiar</button>
    <button data-payment-check type="button">Consultar pagamento</button>
  </div>
</section>

Metas e créditos no checkout ​

[data-purchase-benefits] é a região opcional de metas no carrinho/checkout. O Liquid renderiza a apresentação inicial; uma nova cotação atualiza os dados no carrinho e checkout, mesmo sem mudança de total, com texto escapado e valores calculados pelo servidor. A região fica oculta quando não há benefícios; uma resposta sem benefícios remove os anúncios anteriores. Use aria-live="polite", títulos e progress nativo acessível. Os temas oficiais usam .purchase-benefit; toda aparência permanece no CSS de cada revisão.

A raiz [data-checkout] declara data-expected-gift-ids com os goalId de cart.benefits.gifts separados por vírgula (vazio quando não há brindes). O runtime envia expectedGiftIds; mudanças exigem nova revisão do carrinho. O servidor determina os produtos: enviar outro ID não concede um brinde.

Dentro de [data-checkout-form], um input opcional [data-checkout-credits] usa name="credits", type="number", min="0", step="0.01", value="0" e max em reais a partir de checkout.creditsMaximum. Só o input usa reais; creditsAmount no POST usa centavos. [data-checkout-credits-row] e [data-checkout-credits-value] apresentam a dedução e o total acompanha a escolha. A validação local é auxílio de interface; a autorização ocorre no servidor. Não salvar saldo, fazer débito ou gerar cashback em JavaScript do tema. Depois de uma recotação, o total reaplica a escolha de créditos. Se a ativação ou o mínimo mudarem, a escolha permanece preenchida para correção, mas deixa de ser deduzida. Total comercial ausente é exibido como indisponível, nunca zero.

credits_unavailable mantém o preenchimento para corrigir o valor. Mudança de brindes exige revisar/reabrir o carrinho, preservando a tentativa existente até uma decisão explícita. A repetição após resposta perdida mantém o mesmo UUID, o mesmo valor de créditos e os mesmos IDs esperados. Não criar nova tentativa automaticamente. A prévia bloqueia mutações antes de acessar a rede.

Modalidades de frete podem conter deliveryDescription em vez de prazo numérico. O runtime apresenta o texto cadastrado escapado, sem inventar uma data de entrega.

Temas V3 · Liquid estrutura, CSS desenha, JSON compõe.