Como construir um checkout de uma página em React e Next.js que aguenta recusas e preços alterados
Um formulário de checkout é fácil. Recusas, 3-D Secure, preços que mudam antes do Pagar e pagamentos que nunca respondem não são. Um checkout de uma página em React e Next.js.
Preet Suthar · · 6 min de leitura
- React
- Next.js
- E-commerce
- Payments
A maioria dos tutoriais de checkout termina num formulário e num botão Pagar que chama a Stripe. Essa parte funciona de primeira. O que faz perder pedidos é tudo em volta: o cartão que é recusado, o banco que pede uma verificação 3-D Secure, o frete que muda com o CEP, a requisição que nunca volta e o cliente que pressiona Pagar uma segunda vez.
É assim que eu estruturo um checkout de uma página em React e Next.js, com os estados que importam e um trechinho de código para cada um. Funciona com shadcn/ui ou qualquer outro conjunto de componentes. A parte difícil é o fluxo, não o estilo.
Uma página, três etapas que se recolhem
Um checkout de uma página mantém tudo à vista: nenhum carregamento de página entre as etapas, e o botão Voltar do navegador não consegue jogar fora meio formulário. Mas um formulário longo é um paredão. A solução são três etapas na mesma página, contato, entrega e pagamento, com apenas uma aberta por vez.
Quando uma etapa termina, ela se recolhe numa linha de resumo, então um checkout concluído se lê como um recibo:
O estado por trás disso é pequeno. Guarde a lista de etapas concluídas e derive a que está aberta:
Dois detalhes fazem a experiência parecer certa. Quando alguém pressiona Alterar, mova o foco para o título da etapa que abriu, ou quem usa teclado e leitor de tela fica parado num botão que não existe mais. E quando o cliente está logado, pule o contato e comece pela entrega com o endereço padrão dele.
Em telas largas, o resumo do pedido fica ao lado do formulário. Nos celulares, ele se recolhe numa barra "Mostrar resumo do pedido" acima do formulário que ainda mostra o total, porque o total é o número que as pessoas ficam conferindo.
Valide no blur, resuma ao pressionar Pagar
Mostre o erro de um campo quando o cliente sair dele e depois atualize-o ao vivo enquanto ele digita, para que suma no momento em que for corrigido. Nunca enquanto ele ainda está digitando pela primeira vez.
Não desabilite o Pagar até o formulário ser válido. Um botão desabilitado não consegue dizer a ninguém o que está faltando. Deixe a pessoa pressioná-lo e então coloque um único resumo no topo listando cada problema, ligue cada item ao seu campo e mova o foco para o resumo. O resumo de erros do GOV.UK Design System é uma boa referência para esse padrão.
Frete e impostos seguem o endereço
Frete e impostos dependem do endereço, então só podem carregar quando o endereço estiver completo. Espere uma pausa curta na digitação, cancele a requisição anterior quando o endereço mudar de novo e mostre "Calculando…" no resumo em vez de um número desatualizado.
Quando chegarem novas opções de frete, mantenha a escolhida se ela ainda estiver disponível. Se não estiver, escolha a mais barata e avise: "Expresso não está disponível para este endereço." Trocar o método em silêncio é como as pessoas acabam pagando por um frete que não escolheram.
Pagamento expresso vem primeiro
Apple Pay, Google Pay, Shop Pay e PayPal substituem o formulário inteiro, então eles pertencem ao topo, acima do contato, com um divisor "Ou pague com cartão" logo abaixo. Colocá-los ao lado do botão Pagar significa que o cliente já digitou o endereço quando vê o caminho mais rápido. Renderize só as carteiras que o navegador realmente consegue usar. O Express Checkout Element da Stripe e SDKs parecidos descobrem isso por você.
Pagar é uma máquina de estados, não um booleano
isSubmitting não consegue descrever o que acontece depois do Pagar. Dê um tipo à resposta do servidor:
Depois trate cada caso de propósito.
Pagar não pode rodar duas vezes. Um clique duplo dispara dois handlers antes de o React renderizar de novo, então uma checagem com disabled={pending} não basta. Proteja com uma ref e trave os botões Alterar enquanto o pedido está sendo feito, para que o carrinho não mude por baixo do pagamento.
Toda tentativa recebe uma chave de idempotência. Crie-a com crypto.randomUUID() quando Pagar for pressionado e envie-a junto com a requisição de pagamento. A Stripe e a maioria das APIs de pagamento aceitam uma, e uma requisição repetida com a mesma chave não consegue cobrar duas vezes.
Sair no meio do pagamento pede confirmação antes. Adicione um listener de beforeunload enquanto um pagamento está em andamento e remova-o quando ele terminar.
Recusas precisam de palavras, não de "Algo deu errado"
Uma recusa é a falha mais comum no checkout e a que tem o pior texto. Mapeie os códigos que você recebe para frases que digam à pessoa o que fazer, mova o foco para a mensagem e mantenha os dados do cartão preenchidos:
requires_action não é um erro. Significa que o banco quer 3-D Secure, o que é rotina na Europa por causa da autenticação forte do cliente. Mantenha o estado de processamento, abra o desafio do banco num diálogo chamado "Confirme com o seu banco" e continue quando ele for resolvido.
Quando o preço mudou desde o carrinho
Preços, estoque e descontos podem mudar entre o carrinho e o Pagar. Envie com o pedido o total que o cliente viu, recalcule o preço no servidor e recuse a cobrança se forem diferentes:
No cliente, atualize o resumo, risque o total antigo e liste cada mudança em palavras simples, como "Ring vase agora custa $74, antes custava $68." O cliente pressiona Pagar de novo com o novo valor. Um item que esgotou durante o checkout funciona do mesmo jeito: tire-o do total, sinalize-o e deixe a pessoa removê-lo ou continuar sem ele.
Quando o pagamento nunca responde
Às vezes a requisição trava. A pior coisa que você pode mostrar depois de um timeout é "Tente novamente", porque a primeira cobrança pode ter passado. Depois de uma espera razoável, diga "Verificando o status do seu pagamento…" e consulte um endpoint de status com a mesma chave de idempotência até saber se existe um pedido. Só ofereça uma nova tentativa quando o servidor disser que a anterior falhou.
Um checklist
- As etapas se recolhem em linhas de resumo, e Alterar move o foco para a etapa reaberta.
- Erros no blur, um resumo com links ao pressionar Pagar, nenhum botão Pagar desabilitado.
- Frete e impostos esperam o endereço completo, cancelam requisições antigas e nunca mostram um total desatualizado.
- Carteiras expressas no topo, só as que o navegador suporta.
- Uma proteção com ref, uma chave de idempotência e um aviso de
beforeunloadem volta do Pagar. - Texto específico para cada código de recusa, com o formulário ainda preenchido.
- 3-D Secure como uma etapa, não como um erro.
- Preços alterados e itens esgotados interrompem a cobrança e se explicam.
- Um timeout consulta o status em vez de oferecer pagar de novo.
Construindo com shadcn/ui
O shadcn/ui dá a você os inputs, radio groups e collapsibles para tudo isso. O fluxo acima é a parte que você mesmo escreve, e é a maior parte do trabalho.
Eu construí isso uma vez como o bloco Checkout do HextaUI Pro, feito com os componentes gratuitos do HextaUI. Ele recebe o carrinho e três callbacks, onGetRates, onEstimateTax e onPlaceOrder, e trata os quatro resultados acima. Se onPlaceOrder não responder em 20 segundos, ele consulta onCheckStatus com a mesma chave de idempotência em vez de oferecer pagar de novo. Ele vem com exemplos para Stripe PaymentIntents com server actions, repasse para o checkout da Shopify e Medusa. Ele usa o bloco Payment para carteiras, cartões, recusas e 3-D Secure, e o Address Form para a entrega. Todo bloco tem um preview ao vivo que você pode explorar antes de decidir qualquer coisa.
Se a sua loja roda na Shopify, use o checkout hospedado da Shopify em vez de reconstruí-lo. Um checkout próprio faz sentido quando você mesmo recebe o pagamento.
FAQ
Um checkout de uma página é melhor que um de várias etapas? Para a maioria das lojas, uma única página com etapas que se recolhem oferece o melhor dos dois: tudo fica à vista, e a pessoa só encara uma seção por vez.
Devo desabilitar o botão Pagar até o formulário ser válido? Não. Deixe a pessoa pressioná-lo e mostre um resumo do que está faltando, com links para cada campo.
Como eu evito cobranças duplicadas? Proteja o handler com uma ref, envie uma chave de idempotência em toda tentativa e consulte o status de um pagamento que estourou o tempo em vez de deixar a pessoa pagar de novo.
Onde Apple Pay e Google Pay devem ficar? No topo do checkout, antes dos dados de contato, porque eles substituem o resto do formulário.