npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  1. Visão geral
  2. Arquitetura
  3. Instalação
  4. Configuração
  5. API pública
  6. Fluxo do checkout token
  7. Métodos de pagamento
  8. Callbacks & eventos
  9. Estados
  10. Internacionalização
  11. Temas & personalização
  12. Comprovativo / Invoice
  13. Exemplos
  14. Estrutura de pastas
  15. Tratamento de erros
  16. Como testar
  17. Publicação no npm (deploy)
  18. Troubleshooting
  19. FAQ
  20. Roadmap
  21. Changelog
  22. 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 >=18

Configuraçã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 → fecho

O 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 única

Tratamento 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)

  1. Conta npm com acesso à organização @mirantes (cria a org em npmjs.com se ainda não existir — pacotes scoped precisam da org).
  2. Autenticar: npm login (ou, em CI, um NPM_TOKEN/NODE_AUTH_TOKEN).
  3. 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-tags

Confirma em https://www.npmjs.com/package/@mirantes/pay-sdk e testa a instalação num projeto limpo: npm install @mirantes/pay-sdk.

Notas

  • prepublishOnly corre typecheck + build automaticamente — o npm publish nunca envia um dist/ 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.registry e autentica; remove access: "public".
  • CI: npm publish --provenance (opcional) gera proveniência; autentica com NODE_AUTH_TOKEN.
  • Licença: está como UNLICENSED (proprietário). Para publicar como open-source, muda o campo license e adiciona um ficheiro LICENSE.
  • Rollback: o npm não deixa "despublicar" livremente após 72h; para corrigir, publica uma nova versão (ou npm deprecate a 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 baseURL para um proxy same-origin teu que reencaminhe para https://api.pay.mirantes.io/api/v1 (o examples/playground faz isto com o proxy do Vite). Assim que a API enviar CORS, podes voltar a usar o baseURL direto.

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 dispara onSuccess.
  • Resultado & callbacks pós-fecho: pay() resolve Promise<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: tsc estrito, 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).