Skip to content

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_name precisa 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

  1. Instalação autorizada com domain_name (ou domain_names).
  2. Arquivo .well-known publicado no domínio.
  3. 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.

SeletorCarteiraPapel
.appmax-apple-pay-btnApple PayContainer 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 PayO 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 PayOpcional. Ocultado junto com a âncora quando a carteira não está disponível.
data-appmax-merchant-name (em [data-appmax-checkout])Google PayNome 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:

html
<div class="appmax-apple-pay-btn"></div>

O SDK injeta algo como:

html
<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, um v-if/condicional — o clique não vai disparar nada. Sem erro, sem log.
  • Se o componente que chama init pode re-renderizar (StrictMode, Fast Refresh, um efeito mal dependenciado), lembre que init() 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.

html
<!-- 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.

javascript
const onUpdate = () => ({
  total: 129.90,
  freight: 15.00,
  discount: 10.00,
  installments: 1,
  products: [
    { name: 'Camiseta Preta', price: 62.45, quantity: 2 },
  ],
});
CampoTipoObrigatórioO que aparece na PaymentSheet
totalnumberSimLinha de total. O rótulo é fixo em "Total" e não é configurável.
productsarrayRecomendadoUma linha por item. Sem ele, a sheet mostra só o total.
products[].namestringSim, se houver productsRótulo da linha.
products[].pricenumberSim, se houver productsPreço unitário, em reais.
products[].quantitynumberSim, se houver productsQuantidade. O valor exibido é price × quantity.
freightnumberNãoLinha "Frete". Omitida quando 0 ou ausente.
discountnumberNãoLinha "Desconto". Omitida quando 0 ou ausente.
installmentsnumberNãoNã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: 12990 cobra R$ 12.990,00, não R$ 129,90.
  • total: '129.90' (string) faz o SDK lançar Error: 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:

javascript
// ✗ 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 PromiseO que o SDK faz
ResolvecompletePayment(STATUS_SUCCESS) — a sheet fecha com a confirmação da Apple.
RejeitacompletePayment(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:

javascript
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 ​

  1. 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.
  2. Se o usuário muda frete, parcelas ou item no meio do caminho, a Apple chama seu onUpdate para atualizar o total exibido.
  3. Ao confirmar (Face ID/Touch ID), o SDK chama o seu onAuthorize(appleToken).
  4. No onAuthorize, com o appleToken em mãos: crie o customer, crie a order e efetive o pagamento em POST /v1/payments/apple-pay — o mapeamento completo de appleToken → payload está na seção "Processar o pagamento" de Pagamento com Apple Pay.

Troubleshooting rápido ​

SintomaCausa provável
Botão não apareceFora do Safari, ou dispositivo sem cartão configurado no Apple Wallet. Confirme com ApplePaySession.canMakePayments().
Error processing payment... no onError e a PaymentSheet nunca abreO 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 compradorO onAuthorize capturou o erro sem relançar. Veja "Como sinalizar falha no onAuthorize".
Clique não faz nada, sem erroO 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ódigoDomí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, ou externalId ausente.
  • Apareceu e falhou no meio → o problema é de validação de merchant: domain_name da instalação, arquivo .well-known ou external_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.