Implementando o botão Apple Pay com o Appmax JS
Este guia cobre a parte do fluxo Apple Pay que roda no navegador, via appmax.min.js: como o botão é renderizado, o contrato de DOM que o script exige e como conectar os callbacks até o pagamento. Para o restante do fluxo, veja:
- Instalação do aplicativo — o
domain_nameprecisa estar configurado antes de qualquer coisa aqui funcionar. - Configuração de domínios para Apple Pay — publicação do arquivo
.well-known, obrigatória em todos os modelos. - Appmax JS — assinatura completa do
init, coleta de IP e tokenização de cartão (este guia assume que você já leu a seção "Como usar" de lá). - Pagamento com Apple Pay — o contrato do endpoint (
payload, mapeamento do Apple Token, modelo integrado × fluxo direto, FAQ). - Implementando o botão Google Pay com o Appmax JS — o mesmo
init()também ativa o botão do Google Pay quando a página ancora[data-appmax-google-pay]. Esta página cobre o que é específico do Apple Pay e aponta o que muda quando as duas carteiras convivem.
Pré-requisitos rápidos
- Instalação autorizada com
domain_name(oudomain_names). - Arquivo
.well-knownpublicado no domínio. - Script do Appmax JS carregado na página, no build do ambiente certo — a URL muda entre sandbox e produção, veja Ambientes.
Sem os três, o botão pode até aparecer, mas a validação do domínio falha na hora de abrir a PaymentSheet.
Renderizando o botão
O appmax.min.js reconhece dois seletores de Apple Pay no DOM. Você só precisa de um deles — não dos dois. Os seletores do Google Pay são independentes: ancore apenas os da carteira que a loja oferece.
| Seletor | Carteira | Papel |
|---|---|---|
.appmax-apple-pay-btn | Apple Pay | Container que você renderiza vazio. O SDK substitui o innerHTML dele pelo botão oficial da Apple (SVG + estilos inclusos). Caminho recomendado — você não precisa desenhar o botão. |
[data-appmax-apple-pay] | Apple Pay | O botão em si. É neste elemento que o SDK registra o click. Use esse atributo direto no seu próprio botão se preferir controlar o markup. |
[data-appmax-google-pay] | Google Pay | Âncora. O SDK insere um iframe com o botão oficial do Google imediatamente antes dela e a oculta. O iframe herda a altura da âncora, com mínimo de 48 px. Detalhes em Implementando o botão Google Pay. |
[data-appmax-google-pay-wrapper] | Google Pay | Opcional. Ocultado junto com a âncora quando a carteira não está disponível. |
data-appmax-merchant-name (em [data-appmax-checkout]) | Google Pay | Nome exibido ao comprador na folha do Google. Na ausência, o SDK utiliza o <title> da página ou o host. |
Caminho mais simples — deixe o SDK desenhar o botão:
<div class="appmax-apple-pay-btn"></div>O SDK injeta algo como:
<button data-appmax-apple-pay class="applepay-button">
<span class="applepay-button__label">Pagar com</span>
<svg class="applepay-button__logo">...</svg>
</button>O botão só aparece se o dispositivo suportar Apple Pay
Fora do Safari (ou em dispositivo sem Apple Pay configurado), ApplePaySession.canMakePayments() retorna false e o SDK não ativa o botão — ele pode ficar vazio ou oculto, dependendo de como você estilizou o container. Isso é esperado, não é bug.
Quando a página também ancora o Google Pay, o SDK aplica uma regra de precedência: onde o Apple Pay está disponível, o Google Pay é ocultado; nos demais navegadores suportados, o Google Pay é exibido. A loja não escolhe qual carteira aparece — a seleção é automática.
Ordem de carregamento: o botão precisa existir antes do init
O AppmaxScripts.init(...) procura o botão uma única vez, no momento em que roda, e não observa mudanças no DOM depois disso — o mesmo contrato descrito em "Contrato de DOM" na página do Appmax JS. Para o botão do Apple Pay, a consequência prática é:
- Se o container/botão só é montado depois do
init— atrás de uma rota, um passo do checkout, umv-if/condicional — o clique não vai disparar nada. Sem erro, sem log. - Se o componente que chama
initpode re-renderizar (StrictMode, Fast Refresh, um efeito mal dependenciado), lembre queinit()não é idempotente: cada chamada registra um novo listener por cima do anterior.
Regra prática para SPA: garanta que o container do botão já está no DOM no exato momento em que AppmaxScripts.init(...) é chamado — nunca o contrário.
Inicializando com os callbacks do Apple Pay
Para Apple Pay, o init exige externalId, onUpdate e onAuthorize (além de onSuccess/onError, sempre obrigatórios). A referência completa de cada parâmetro está em Appmax JS → Inicializar o AppmaxScripts; aqui o foco é como esses três se conectam ao fluxo de Apple Pay especificamente.
<!-- Gatilho da coleta de IP: obrigatório, e precisa existir antes do init -->
<form data-appmax-customer hidden></form>
<div class="appmax-apple-pay-btn"></div>
<script>
// onUpdate: chamado quando a PaymentSheet abre e sempre que o usuário
// muda algo nela. Deve retornar o carrinho atual, com valores numéricos
// em reais — veja "O que o onUpdate deve retornar", abaixo.
const onUpdate = () => ({
total: 129.90,
freight: 15.00,
discount: 10.00,
installments: 1,
products: [{ name: 'Camiseta Preta', price: 62.45, quantity: 2 }],
});
// onAuthorize: chamado quando o pagamento é autorizado pelo usuário.
// Recebe o token da carteira utilizada — o mesmo callback atende ao Apple
// Pay e ao Google Pay. O Apple Token (paymentData + transactionIdentifier)
// vai para POST /v1/payments/apple-pay; o paymentMethodData do Google
// (tokenizationData) vai para POST /v1/payments/google-pay.
// No Apple Pay, rejeite a Promise para sinalizar falha — veja "Como
// sinalizar falha", abaixo.
const onAuthorize = async (token) => {
if (token.tokenizationData) {
await processGooglePayPayment(token); // veja a página de Google Pay
return;
}
await processPayment(token);
};
// A coleta de IP é obrigatória em toda integração, e só acontece se
// houver um form[data-appmax-customer] no DOM — o form vazio acima serve.
const onIp = ({ ip }) => { customer.ip = ip };
const onError = (err) => console.error('Erro no Apple Pay:', err.code, err.message);
window.AppmaxScripts.init({ externalId, onIp, onError, onUpdate, onAuthorize });
</script>Se você usar a forma posicional, a ordem importa
init(onSuccess, onError, externalId, onUpdate, onAuthorize) — nessa ordem. Trocar onUpdate e onAuthorize de posição, ou escrever onAutorize, faz o SDK tratar o parâmetro errado como função de callback. O objeto de opções não tem esse risco, e é a forma recomendada.
A referência completa do init — as duas formas, todos os callbacks e o contrato do onError — vive em Appmax JS. Esta página cobre só o que é específico do Apple Pay.
O externalId é o mesmo external_id retornado com HTTP 200 na etapa de instalação do aplicativo — sem ele, init lança exceção síncrona (veja o aviso em Appmax JS).
Integrações Apple Pay existentes não são afetadas pelo Google Pay
O Google Pay só é montado quando a página contém a âncora [data-appmax-google-pay]. Sem ela, o init() se comporta exatamente como antes. A assinatura do init() e a URL do script não mudam.
O que o onUpdate deve retornar
O onUpdate é chamado quando a PaymentSheet abre e sempre que o usuário altera algo nela. O objeto devolvido descreve o carrinho, com valores numéricos em reais — o SDK converte esse objeto no formato que a PaymentSheet do Safari consome. Você não monta os lineItems nem formata o total.
const onUpdate = () => ({
total: 129.90,
freight: 15.00,
discount: 10.00,
installments: 1,
products: [
{ name: 'Camiseta Preta', price: 62.45, quantity: 2 },
],
});| Campo | Tipo | Obrigatório | O que aparece na PaymentSheet |
|---|---|---|---|
total | number | Sim | Linha de total. O rótulo é fixo em "Total" e não é configurável. |
products | array | Recomendado | Uma linha por item. Sem ele, a sheet mostra só o total. |
products[].name | string | Sim, se houver products | Rótulo da linha. |
products[].price | number | Sim, se houver products | Preço unitário, em reais. |
products[].quantity | number | Sim, se houver products | Quantidade. O valor exibido é price × quantity. |
freight | number | Não | Linha "Frete". Omitida quando 0 ou ausente. |
discount | number | Não | Linha "Desconto". Omitida quando 0 ou ausente. |
installments | number | Não | Não altera a PaymentSheet. Aceito por compatibilidade. |
Valores são números em reais — não centavos, nem strings
Esta é a causa mais comum de falha na abertura da PaymentSheet.
total: 12990cobra R$ 12.990,00, não R$ 129,90.total: '129.90'(string) faz o SDK lançarError: Error processing payment...antes de a sheet abrir, porque ele aplica.toFixed(2)sobre o valor.
Pelo mesmo motivo, não devolva o objeto já formatado:
// ✗ Errado — este é o formato INTERNO, que o SDK gera a partir do seu retorno
{ total: { label: 'Total', amount: { currency: 'BRL', value: '129.90' } },
displayItems: [ /* ... */ ] }
// ✓ Correto — o carrinho, com números
{ total: 129.90, products: [{ name: 'Camiseta Preta', price: 62.45, quantity: 2 }] }Como sinalizar falha no onAuthorize
O SDK decide o resultado da PaymentSheet pelo estado da Promise que o seu onAuthorize devolve:
Vale apenas para o Apple Pay
No Google Pay a folha do Google já está fechada quando o onAuthorize é invocado, e o valor retornado (ou a rejeição) não é utilizado. A aprovação ou recusa deve ser tratada na interface da loja — veja Como o onAuthorize se comporta.
| Sua Promise | O que o SDK faz |
|---|---|
| Resolve | completePayment(STATUS_SUCCESS) — a sheet fecha com a confirmação da Apple. |
| Rejeita | completePayment(STATUS_FAILURE) — a sheet informa a falha ao comprador. |
return false não sinaliza falha
Apenas a rejeição da Promise é lida como falha. Um try/catch em volta da chamada ao seu backend — o padrão natural para exibir a mensagem de erro na tela — faz a Promise resolver, e o comprador vê a confirmação da Apple em um pagamento que foi recusado.
Se você precisa tratar o erro, relance:
const onAuthorize = async (appleToken) => {
try {
const res = await fetch('/checkout/apple-pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId, customer_id: customerId, appleToken }),
});
if (!res.ok) throw new Error('Pagamento recusado');
} catch (err) {
showError(err);
throw err; // obrigatório: sem isto, a sheet fecha como sucesso
}
};Do clique ao pagamento
- O usuário clica no botão → o SDK abre a
ApplePaySession(PaymentSheet do Safari) e valida o merchant automaticamente — você não chama nenhum endpoint para isso. - Se o usuário muda frete, parcelas ou item no meio do caminho, a Apple chama seu
onUpdatepara atualizar o total exibido. - Ao confirmar (Face ID/Touch ID), o SDK chama o seu
onAuthorize(appleToken). - No
onAuthorize, com oappleTokenem mãos: crie o customer, crie a order e efetive o pagamento emPOST /v1/payments/apple-pay— o mapeamento completo deappleToken→ payload está na seção "Processar o pagamento" de Pagamento com Apple Pay.
Troubleshooting rápido
| Sintoma | Causa provável |
|---|---|
| Botão não aparece | Fora do Safari, ou dispositivo sem cartão configurado no Apple Wallet. Confirme com ApplePaySession.canMakePayments(). |
Error processing payment... no onError e a PaymentSheet nunca abre | O retorno do onUpdate está fora do contrato — normalmente valores em centavos, como string, ou o objeto já formatado. Veja "O que o onUpdate deve retornar". |
| Pagamento recusado aparece como aprovado para o comprador | O onAuthorize capturou o erro sem relançar. Veja "Como sinalizar falha no onAuthorize". |
| Clique não faz nada, sem erro | O botão foi montado depois do init (SPA) — veja "Ordem de carregamento" acima. |
init() lança Error: External ID is required... | externalId ausente ou inválido. No fluxo Apple Pay é uma exceção síncrona, que não passa por onError — envolva a chamada em try/catch. |
Erro genérico do Safari ao confirmar (ex.: DOMException) | Sessão do merchant inválida — confira se o domínio tem o .well-known publicado (guia) e se o external_id é o mais recente da instalação. |
| Funciona em uma loja e não em outra, mesmo código | Domínio da segunda loja não foi informado na instalação (domain_name) ou não tem o .well-known publicado. |
A sheet nunca abriu, ou abriu e falhou?
Essa é a primeira pergunta a fazer, e ela separa dois mundos que não se misturam:
- Nunca apareceu → o problema está no navegador, antes de qualquer rede: contrato do
onUpdate, ordem de DOM/init, ouexternalIdausente. - Apareceu e falhou no meio → o problema é de validação de merchant:
domain_nameda instalação, arquivo.well-knownouexternal_id.
Não é possível testar o fluxo completo em localhost — veja Testes e Sandbox para os detalhes de ambiente.
Em sandbox, a validação de domínio é feita contra a produção da Apple
Não existe merchant session de sandbox. O botão renderiza e a PaymentSheet pode abrir, mas a validação de domínio/merchant sempre acontece contra a infraestrutura de produção da Apple — então ela falha de forma consistente se o domínio não estiver registrado e com o .well-known publicado. Trate sandbox e produção como assimétricos neste ponto específico; os detalhes estão em Pagamento com Apple Pay.
Projeto de referência para implementação
O repositório appmaxbrasil/appstore-demo-php contém uma implementação de referência da integração completa de Apple Pay — instalação via App Store, renderização do botão e callbacks do appmax.js no checkout, e a chamada ao endpoint de pagamento — em PHP puro e JavaScript vanilla, sem framework. Use-o como base de estudo e ponto de partida para a própria implementação, não como código de produção.
O passo a passo do Apple Pay do repositório traz um link direto para a linha de código correspondente a cada etapa descrita neste guia.