@mirantes/pay-sdk
v0.2.0
Published
SDK oficial do Mirantes Pay: encapsula o fluxo de checkout (checkout token, GPO/REF, estados e UI) para aplicacoes React/Next.js/Vite.
Readme
Mirantes Pay SDK
SDK oficial do Mirantes Pay para React · Next.js (App & Pages Router) · Vite. Encapsula toda a experiência de checkout — Multicaixa Express (GPO) e referência bancária (REF) — por trás de uma única chamada.
import { MirantesPay } from "@mirantes/pay-sdk";
MirantesPay.configure({ locale: "pt", merchantName: "Minha Loja" });
await MirantesPay.pay({
token, // checkout token vindo do teu backend
onSuccess: (tx) => console.log("pago", tx),
onError: (err) => console.error(err),
onCancel: () => {},
onClose: () => {},
});O consumidor só precisa do checkout token que o backend emite. O SDK nunca vê a api_key — fala apenas com os endpoints públicos de checkout. Toda a UI é criada e destruída automaticamente (Shadow DOM + createPortal), sem componentes explícitos e sem importar CSS.
Índice
- Visão geral
- Arquitetura
- Instalação
- Configuração
- API pública
- Fluxo do checkout token
- Métodos de pagamento
- Callbacks & eventos
- Estados
- Internacionalização
- Temas & personalização
- Comprovativo / Invoice
- Exemplos
- Estrutura de pastas
- Tratamento de erros
- Como testar
- Publicação no npm (deploy)
- Troubleshooting
- FAQ
- Roadmap
- Changelog
- AI Implementation Prompt
Visão geral
O Mirantes Pay coloca-se entre a tua aplicação e os fornecedores de pagamento (Pay4All GPO, referência bancária, Multicaixa Express). O teu backend cria um payment intent e troca-o por um checkout token de curta duração (30 min), que entrega ao teu frontend. A partir daí, o SDK conduz tudo:
- carrega os detalhes do pagamento (montante, moeda, linhas);
- apresenta os métodos disponíveis;
- recolhe o número de telemóvel (GPO) ou gera a referência (REF);
- gere estados, animações, i18n e temas;
- mostra o ecrã de sucesso, comprovativo e executa os callbacks.
Documento de análise/arquitetura completo: docs/analysis/index.html.
Guia da API consumido: docs/api-docs/pay-docs.md.
Arquitetura
Clean Architecture com fronteiras estritas — a lógica (máquina de estados, cliente de checkout, orquestração) não conhece React; a UI é puramente apresentacional.
core/ (state machine, orchestrator) → api/ (checkout client) → 3 endpoints públicos
│ ▲
▼ │
state store ──► hooks/ + ui/ (Shadow DOM, portal, componentes por estado)- Isolamento total de estilos: a UI é montada dentro de um Shadow DOM, com o CSS injetado como constructable stylesheet (com fallback
<style>). Nada do host entra, nada do SDK sai — sem conflitos de Tailwind, sem reset global, sem importar CSS. - Métodos como adapters: GPO e REF são adapters registados num
ProviderRegistry(Open/Closed) — adicionar um método não toca no core.
Instalação
npm install @mirantes/pay-sdk
# peers: react >=18, react-dom >=18Configuração
Chama configure() uma vez (ex.: no arranque da app). Todas as opções são opcionais exceto quando indicado.
MirantesPay.configure({
locale: "pt", // "pt" | "en" | "fr"
merchantName: "Minha Loja",
merchantLogo: "https://…/logo.png",
environment: "production", // ou "sandbox"
theme: { colors: { accent: "#0b89ce" }, motion: "full" },
timeout: 120000, // ms (GPO pode ser demorado)
// baseURL, fetch, headers, invoice, debug, logger, accessToken (reservado)
});| Opção | Tipo | Notas |
|-------|------|-------|
| locale | "pt" \| "en" \| "fr" | Idioma inicial |
| merchantName | string | Nome da marca no checkout (a API não o devolve) |
| merchantLogo | string \| ReactNode | Logótipo: URL de imagem OU um ReactNode/SVG (renderiza dentro do Shadow DOM, nítido em qualquer resolução) |
| environment | "sandbox" \| "production" | Seleciona a baseURL |
| baseURL | string | Override manual (default https://api.pay.mirantes.io/api/v1) |
| configURL | string | Endpoint da configuração remota (métodos ativos/ordem), servido pelo Backoffice/admin com CORS. Default https://admin.mirantes.io/api/admin-mirantes-pay/sdk-config/public. Falha-aberto: se não ler, usa os defaults e nunca bloqueia o checkout |
| theme | object | Ver Temas |
| timeout | number | ms por pedido; no GPO é também a duração da contagem decrescente (default 120000) |
| fetch | typeof fetch | fetch custom (interceptors/SSR) |
| headers | object \| () => object | Headers extra |
| invoice | InvoiceCapability | Resolver de comprovativo (ver secção) |
| debug / logger | boolean / Logger | Diagnóstico |
API pública
| Função | Assinatura | Descrição |
|--------|------------|-----------|
| configure | (config) => void | Config global |
| pay | (params) => Promise<PaymentResult> | Abre o checkout; resolve ao fechar |
| close | () => void | Fecha o modal ativo |
| destroy | () => void | Desmonta e limpa config |
| version | () => string | Versão do SDK |
| setLocale | (locale) => void | Troca de idioma |
interface PayParams {
token: string;
// durante o fluxo (modal aberto)
onSuccess?: (tx: Transaction) => void;
onError?: (error: unknown) => void;
onCancel?: () => void;
onTimeout?: () => void;
onExpired?: () => void;
onReference?: (ref: RefResult) => void;
onStateChange?: (state: PaymentState) => void;
// depois de o modal fechar
onClose?: (result: PaymentResult) => void; // sempre
onSuccessClose?: (result: PaymentResult) => void; // só sucesso
onFailClose?: (result: PaymentResult) => void; // só falha
}
interface PaymentResult {
outcome: "success" | "reference-pending" | "declined" | "timeout" | "expired" | "cancelled" | "error";
state: PaymentState;
method?: "gpo" | "ref";
transaction?: Transaction; // quando outcome === "success"
reference?: RefResult; // fluxo REF
error?: { code: string; message: string };
}pay() devolve Promise<PaymentResult> que resolve quando o modal fecha — nunca rejeita por um desfecho de negócio (recusa/timeout são outcome, não exceções). Podes usar callbacks, a promise, ou ambos.
Fluxo do checkout token
Backend: cria payment intent → troca por checkout token → entrega o token ao frontend
Frontend: MirantesPay.pay({ token })
→ GET /checkout/tokens/:token (detalhes)
→ escolha do método
→ GPO: POST /checkout/payments/gpo { token, phone } (resolve síncrono)
REF: POST /checkout/payments/ref { token } (entidade + referência)
→ sucesso / erro → callbacks → fechoO SDK consome apenas estes 3 endpoints públicos (sem api_key):
| Endpoint | Uso |
|----------|-----|
| GET /api/v1/checkout/tokens/:token | Detalhes do pagamento |
| POST /api/v1/checkout/payments/gpo | Pagar por telemóvel |
| POST /api/v1/checkout/payments/ref | Gerar referência bancária |
Métodos de pagamento
Os dois métodos têm naturezas diferentes e o SDK trata-os de forma completamente desacoplada:
| | Multicaixa Express (ekwanza-gpo) | Referência bancária (ekwanza-ref) |
|---|---|---|
| Confirmação | Imediata (síncrona) | Assíncrona — até 24 horas |
| Fluxo | Introduz telemóvel → confirma na app → resultado no ecrã | Gera Entidade + Referência → paga no banco/ATM mais tarde |
| Estado final | success / failed / timeout | reference-ready |
| Callback | onSuccess / onError / onTimeout | onReference (nunca onSuccess) |
| Utilizador espera no modal? | Sim (segundos) | Não — pode fechar o modal |
GPO — Multicaixa Express no telemóvel. O utilizador introduz o número (9 dígitos), o SDK envia POST /gpo e apresenta as instruções (abrir o Multicaixa Express → Operações por Autorizar → confirmar) enquanto aguarda. Durante a espera mostra uma contagem decrescente — o pedido expira ao fim do timeout configurado e o utilizador é levado ao ecrã de tempo esgotado. O desfecho (payment_accepted / payment_rejected / payment_timed_out) chega na própria resposta: em caso de recusa, o ecrã diz explicitamente que o pagamento foi recusado.
REF — referência bancária (pagamento assíncrono). O SDK envia POST /ref, gera Entidade + Referência + Valor e mostra um ecrã dedicado (estado reference-ready) com:
- os dados a copiar (Entidade, Referência, e copiar tudo);
- método e data/hora de geração;
- instruções passo-a-passo para Multicaixa Express, ATM e Internet Banking;
- uma mensagem clara de que o pagamento é confirmado mais tarde.
A confirmação do pagamento por referência pode demorar até 24 horas, consoante o canal usado. Assim que o sistema financeiro confirmar, o produto é disponibilizado automaticamente e o utilizador é notificado — não é preciso mais nenhuma ação. Por isso o SDK não mostra "aguardando pagamento" nem pede para manter a janela aberta: o utilizador pode fechar o modal normalmente.
A confirmação chega ao teu backend via o callback assinado (
ACCEPTED/REJECTED) — é aí que deves libertar o produto e notificar o utilizador.
Callbacks & eventos
| Callback | Quando |
|----------|--------|
| onSuccess(tx) | GPO aceite (modal ainda aberto) |
| onError(err) | Recusado / erro de API / erro de rede |
| onCancel() | Utilizador fecha antes de concluir |
| onTimeout() / onExpired() | Timeout do GPO / token expirado |
| onReference(ref) | REF: entidade+referência prontas |
| onStateChange(state) | Qualquer transição (analytics) |
| onClose(result) | Modal fechou — sempre, com o resultado final |
| onSuccessClose(result) | Fechou após sucesso (ex.: redirecionar para o produto) |
| onFailClose(result) | Fechou após falha (recusa/timeout/expirado/erro) |
Redirecionar após o fecho (o teu caso de uso)
Com callback — redirecionar para o produto quando o pagamento teve sucesso e o modal fechou:
MirantesPay.pay({
token,
onSuccessClose: (r) => router.push(`/produto/${r.transaction!.paymentIntentId}`),
onFailClose: (r) => toast.error(`Pagamento ${r.outcome}`),
});Com promise — o mesmo, tratando REF vs Express:
const r = await MirantesPay.pay({ token });
switch (r.outcome) {
case "success": router.push(`/produto/${r.transaction!.paymentIntentId}`); break;
case "reference-pending": toast.info("Referência gerada — avisamos-te quando o pagamento for confirmado."); break;
case "declined":
case "timeout":
case "expired":
case "error": toast.error("Pagamento não concluído."); break;
case "cancelled": /* utilizador fechou */ break;
}
// r.method distingue "gpo" de "ref"Estados
idle · loading · payment-loading · method-selection · method-form · submitting · awaiting-user · processing · reference-ready · success · invoice-loading · invoice-ready · failed · timeout · expired · cancelled · api-error · network-error · closing · closed
Internacionalização
Português (default), Inglês e Francês incluídos. Nenhum texto hardcoded — dicionários JSON em src/i18n/locales. Muda o idioma com configure({ locale }) ou setLocale(). Novo idioma = novo ficheiro JSON.
Temas & personalização
Modal branco com cantos arredondados por defeito, com cor de destaque #0b89ce. O cabeçalho mostra o comerciante (nome/logo), o primeiro produto (das linhas do checkout) e o total em destaque numa linha própria, sempre com a moeda. Com vários produtos, aparece um +N clicável que abre um dropdown com todos os produtos e o preço de cada; nomes longos são truncados com reticências (nome completo no title). No ecrã de sucesso mostram-se os 3 primeiros produtos com "Ver todos".
Personaliza via tokens (CSS custom properties), aplicados no host do Shadow DOM:
MirantesPay.configure({
theme: {
colors: { accent: "#0b89ce", radius: "16px", surface: "#ffffff" },
fontFamily: "Inter, sans-serif",
motion: "reduced",
},
});Aceita nomes curtos (accent) ou completos (--mp-accent). Tokens disponíveis: accent, accent-strong, accent-soft, on-accent, surface, surface-alt, ink, ink-soft, border, radius, radius-sm, success, danger, entre outros.
Comprovativo / Invoice
A API pública ainda não expõe endpoint de recibo ao frontend. Para não bloquear, fornece uma capability:
MirantesPay.configure({
invoice: {
resolve: async (paymentIntentId) => ({ url: `/api/receipts/${paymentIntentId}.pdf` }),
},
});Quando presente, o ecrã de sucesso mostra Ver / Descarregar. A mesma capability serve para dar estado ao REF no futuro, sem refatoração.
Exemplos
Next.js (App Router) — componente cliente:
"use client";
import { MirantesPay } from "@mirantes/pay-sdk";
export function CheckoutButton({ token }: { token: string }) {
return (
<button
onClick={() =>
MirantesPay.pay({
token,
// redireciona depois de o modal fechar, não a meio do ecrã de sucesso
onSuccessClose: (r) => location.assign(`/produto/${r.transaction!.paymentIntentId}`),
})
}
>
Pagar
</button>
);
}Estrutura de pastas
src/
├─ core/ state machine, orchestrator, config, errors
├─ api/ checkout client (3 endpoints públicos)
├─ providers/ gpo/ ref/ registry (adapters de método)
├─ state/ store observável
├─ hooks/ useStore (useSyncExternalStore)
├─ portal/ Shadow DOM mount + createRoot
├─ ui/ CheckoutApp, states/, primitives/, layout/
├─ themes/ tokens + CSS (white rounded modal)
├─ i18n/ locales/{pt,en,fr}.json
├─ icons/ utils/ types/
└─ index.ts porta pública únicaTratamento de erros
Erros de negócio chegam como MirantesPayError ({ code, message, statusCode }). Códigos: CHECKOUT_TOKEN_NOT_FOUND (→ ecrã "expirado"), VALIDATION_ERROR, NETWORK, TIMEOUT, PAYMENT_REJECTED.
Como testar
Há um playground pronto em examples/playground:
cd examples/playground && npm install && npm run dev- Modo mock — sem API, botões que mostram todos os ecrãs (sucesso, recusado, timeout, referência, expirado, erro de rede).
- Modo live — cria um pagamento de teste real (serviço → payment intent → checkout token) do lado do servidor e abre o SDK.
- Token existente — cola um token do teu backend.
Verificação automática do próprio SDK: npm test (build + smoke test jsdom que percorre o fluxo GPO/REF/expirado).
Publicação no npm (deploy)
O pacote é scoped (@mirantes/pay-sdk) e publica apenas dist/ (campo files) — src, examples, docs e scripts não vão para o npm.
Pré-requisitos (uma vez)
- Conta npm com acesso à organização
@mirantes(cria a org em npmjs.com se ainda não existir — pacotes scoped precisam da org). - Autenticar:
npm login(ou, em CI, umNPM_TOKEN/NODE_AUTH_TOKEN). - Recomendado: ativar 2FA na conta/organização.
Passo-a-passo
# 1. Árvore limpa e na branch certa
git status # nada por commitar
# 2. Verifica antes de publicar (o prepublishOnly também corre typecheck+build)
npm test # build + smoke (jsdom)
# 3. Sobe a versão (cria commit + tag git automaticamente)
npm version patch # 0.1.0 -> 0.1.1 (usa minor/major conforme o caso)
# 4. Inspeciona EXACTAMENTE o que vai ser publicado (deve conter só dist/, package.json, README)
npm pack --dry-run
# 5. Publica (publishConfig.access:"public" já trata do scoped público)
npm publish
# 1ª publicação do scope pode exigir explicitamente:
# npm publish --access public
# 6. Envia a tag para o repositório
git push --follow-tagsConfirma em https://www.npmjs.com/package/@mirantes/pay-sdk e testa a instalação num projeto limpo: npm install @mirantes/pay-sdk.
Notas
prepublishOnlycorretypecheck+buildautomaticamente — onpm publishnunca envia umdist/desatualizado.- Peers: o consumidor instala
react/react-dom(>=18) — não são embutidos. - Registo privado (em vez do npm público): usar GitHub Packages ou registo privado — define
publishConfig.registrye autentica; removeaccess: "public". - CI:
npm publish --provenance(opcional) gera proveniência; autentica comNODE_AUTH_TOKEN. - Licença: está como
UNLICENSED(proprietário). Para publicar como open-source, muda o campolicensee adiciona um ficheiroLICENSE. - Rollback: o npm não deixa "despublicar" livremente após 72h; para corrigir, publica uma nova versão (ou
npm deprecatea versão problemática).
Troubleshooting
- "must run in the browser": chama
pay()num componente cliente / handler de evento, não no servidor. - Token expira: o checkout token dura 30 min; gera um novo no backend.
- Estilos do meu site afetados: não acontece — a UI vive num Shadow DOM isolado.
- CORS ao chamar a API: os endpoints públicos de checkout ainda não enviam cabeçalhos CORS, por isso um browser não os chama diretamente cross-origin. Solução: aponta
baseURLpara um proxy same-origin teu que reencaminhe parahttps://api.pay.mirantes.io/api/v1(oexamples/playgroundfaz isto com o proxy do Vite). Assim que a API enviar CORS, podes voltar a usar obaseURLdireto.
FAQ
Preciso de importar CSS? Não. Os estilos são injetados no Shadow DOM.
Funciona com Tailwind na app? Sim, sem conflitos (isolamento por Shadow DOM).
O REF confirma sozinho? Não em tempo real hoje — a API pública não expõe estado ao frontend; usa a capability invoice/status.
Roadmap
- Estado do REF em tempo real quando a API expuser endpoint público.
- Comprovativo oficial quando existir endpoint público de recibo.
- Novos métodos via novos adapters (sem tocar no core).
Changelog
0.1.0
- Fluxo GPO (Multicaixa Express) ponta-a-ponta: seleção, formulário de telemóvel (+244, 9 dígitos), instruções, contagem decrescente ligada ao
timeout, e ecrã de resultado. Recusa é explícita ("Pagamento recusado"). - Fluxo REF (referência bancária) assíncrono e desacoplado: ecrã próprio (
reference-ready) com Entidade/Referência/Valor, copiar individual + copiar tudo, método e data/hora de geração, instruções por canal (Multicaixa Express / ATM / Internet Banking) em abas, e mensagem clara de confirmação em até 24h + libertação automática + notificação. Fecha normalmente; nunca disparaonSuccess. - Resultado & callbacks pós-fecho:
pay()resolvePromise<PaymentResult>(outcome,state,method,transaction,reference,error);onSuccessClose/onFailClose/onClose(result)— ex.: redirecionar para o produto após o fecho. - UI: Shadow DOM + CSS injetado (modal branco arredondado, cor
#0b89ce), cabeçalho com produto + preço em destaque com moeda; i18n pt/en/fr sem texto hardcoded; subtítulos por estado (recusado/timeout/expirado/erro). - API pública
configure/pay/close/destroy/version/setLocale; máquina de estados; capability opcional de comprovativo. - Verificado:
tscestrito, build tsup (ESM+CJS+.d.ts), smoke test jsdom (GPO/recusa/REF/expirado) e playground (mock + live + token).
AI Implementation Prompt
Copia o bloco abaixo para o ChatGPT/Claude/Gemini/Cursor para integrar o SDK sem ler toda a documentação.
Integra o SDK "@mirantes/pay-sdk" numa app React/Next.js/Vite.
OBJETIVO
Abrir o checkout do Mirantes Pay a partir de um "checkout token" que o meu
backend já me dá, com uma única chamada. O SDK trata de toda a UI, estados,
métodos de pagamento (GPO/Multicaixa Express e referência bancária), i18n e
comprovativo.
INSTALAÇÃO
- npm install @mirantes/pay-sdk
- peers: react >=18, react-dom >=18
REGRA DE SEGURANÇA (IMPORTANTE)
- O SDK usa APENAS o checkout token. NUNCA lhe passes a api_key do Mirantes Pay.
- O token vem do backend (que cria o payment intent e o troca por token de 30 min).
- Chama o SDK só do lado do cliente (client component / event handler).
CONFIGURAÇÃO (uma vez, no arranque)
import { MirantesPay } from "@mirantes/pay-sdk";
MirantesPay.configure({ locale: "pt", merchantName: "Minha Loja" });
// opcional: environment "sandbox"|"production", theme.colors, merchantLogo,
// timeout, fetch, headers, invoice.resolve, debug.
DOIS MÉTODOS, NATUREZAS DIFERENTES
- GPO (Multicaixa Express): confirmação IMEDIATA no telemóvel. Mostra contagem
decrescente; se recusado, diz explicitamente "recusado".
- REF (referência bancária): ASSÍNCRONO. Gera Entidade+Referência; o pagamento
é confirmado mais tarde (até 24h) e o teu backend recebe o callback assinado
(ACCEPTED/REJECTED) — é aí que libertas o produto. O SDK resolve como
outcome "reference-pending" (NUNCA "success").
ABRIR O PAGAMENTO
const result = await MirantesPay.pay({
token, // string vinda do backend
onSuccess: (tx) => {}, // GPO aceite (modal ainda aberto)
onError: (err) => {}, // recusado / erro
onReference: (ref) => {}, // REF: { entity, reference, chargeId }
onStateChange: (state) => {}, // analytics
// depois de o modal fechar:
onSuccessClose: (r) => {}, // ex.: redirecionar para o produto
onFailClose: (r) => {}, // recusa/timeout/expirado/erro
onClose: (r) => {}, // sempre
});
TRATAR O RESULTADO (promise) — distingue GPO vs REF
switch (result.outcome) {
case "success": /* r.transaction — produto disponível, redireciona */ break;
case "reference-pending": /* r.reference — "avisamos-te quando confirmar" */ break;
case "declined": case "timeout": case "expired": case "error": /* falhou */ break;
case "cancelled": /* utilizador fechou */ break;
}
// result.method === "gpo" | "ref"
// pay() NUNCA rejeita por desfecho de negócio; o outcome diz tudo.
PERSONALIZAÇÃO
- Idioma: configure({ locale: "pt"|"en"|"fr" }) ou MirantesPay.setLocale("en").
- Tema (modal branco arredondado, cor #0b89ce por defeito):
configure({ theme: { colors: { accent: "#0b89ce" }, fontFamily: "Inter" } }).
- Comprovativo: configure({ invoice: { resolve: async (id) => ({ url }) } }).
ERROS COMUNS A EVITAR
- Não chamar pay() no servidor (SSR) — só no cliente.
- Não assumir que a referência (REF) confirma em tempo real: não confirma hoje.
- Não tratar REF como sucesso imediato: o outcome é "reference-pending".
- Não importar ficheiros CSS — os estilos vivem num Shadow DOM isolado.
- CORS: se o browser não conseguir chamar a API diretamente, aponta baseURL
para um proxy same-origin teu.
- Renovar o token no backend se passarem 30 min.
CHECKLIST DE INTEGRAÇÃO
[ ] Backend devolve o checkout token ao frontend.
[ ] configure() chamado uma vez no arranque.
[ ] pay({ token }) chamado num handler de evento no cliente.
[ ] onSuccessClose (ou await result.outcome === "success") redireciona para o produto.
[ ] REF tratado como "reference-pending"; produto libertado pelo callback do backend.
[ ] Testado GPO (telemóvel) e REF (referência).