Pular para o conteúdo principal

Payments

Integracao de deposito e saque no template via @cactus-agents/payments. Cobre PIX e agregadores Pix (BR), SPEI/OXXO (MX), Wallet/MACH e Kushki (CL), agregadores nigerianos, cartao de credito, checkout page, redirect e redirect externo.

Arquitetura

usePayments() hook
|
+---> Store Zustand (app/store/payments.ts)
+---> API Routes (server-side, token HttpOnly)
| |
| v
| @cactus-agents/payments (SDK)
| |
| v
| BFF
|
+---> Componentes (DepositModal, WithdrawModal)

Como o token JWT fica em cookie HttpOnly, todas as chamadas de pagamento passam por API routes no servidor.

Arquivos principais

ArquivoTipoDescricao
app/store/payments.tsStore (Zustand)Estado dos modais, providers, metodo selecionado, cupom, auto-open
app/hooks/usePayments.tsHookOrquestra fetch providers, submit deposit/withdraw, polling
app/config/payments/deposit.tsConfigAtalhos de valor por moeda (brand-overridable)
app/config/payments/methods.tsConfigWhitelist de metodos expostos (paymentsConfig)
app/config/payments/kushki.tsConfigmerchantId do gateway Kushki (CL)
app/config/payments/testers.tsConfigContas de teste
app/components/payments/DepositModal.tsxComponentModal de deposito
app/components/payments/WithdrawModal.tsxComponentModal de saque
app/components/payments/LazyPaymentModals.tsxComponentLazy wrapper

Todos os imports de config usam a forma ~/config/payments/<arquivo> — sem suffix .config.

Store — usePaymentsStore

interface PaymentsState {
paymentModal: PaymentModal; // "deposit" | "withdraw" | null

providers: PaymentProviders | null;
providersLoading: boolean;

selectedDepositMethod: PaymentMethodSlug | null;
selectedWithdrawMethod: PaymentMethodSlug | null;

depositLoading: boolean;
withdrawLoading: boolean;

coupon: CouponData | null;

initialDepositAmount: number | null;
pendingDepositAutoOpen: boolean;
pendingDepositAutoSubmit: boolean;
// ... setters + clearPayments()
}

Os tres ultimos campos alimentam o "Deposito Rapido" e os fluxos que pre-preenchem/auto-abrem o modal (deep links de marketing, FTD).

Hook — usePayments()

Estado derivado

PropriedadeDescricao
depositMethods / withdrawMethodsProviders filtrados pela moeda da marca e pela whitelist do paymentsConfig
defaultDepositMethod / defaultWithdrawMethodMetodo pre-selecionado
selectedDepositProvider / selectedWithdrawProviderProvider do metodo selecionado
minDeposit / maxDepositLimites de deposito (brand settings)
minWithdraw / maxWithdrawLimites de saque (brand settings)
currency / countryMoeda e pais do ambiente

Actions

ActionDescricao
fetchProviders()Busca providers disponiveis
submitDeposit(payload)Envia deposito
checkDepositStatus(transactionId)Polling do status do deposito
submitWithdraw(payload)Envia saque
fetchBankList()Lista de bancos disponiveis
fetchPixKey()Busca chave PIX do usuario
updatePixKey(payload)Atualiza chave PIX

API Routes

RotaDescricao
api/payments/providersProviders de deposito/saque
api/payments/depositSubmit deposito
api/payments/deposit-statusStatus do deposito (polling)
api/payments/withdrawSubmit saque
api/payments/bank-listLista de bancos
api/payments/pix-keyGet/update chave PIX
api/payments/couponValidar cupom
api/payments/bank-accountConta bancaria (PIX / CLABE / bancos CL / bancos NG)

:::note Padrao de proxy Rotas api/* leem o JWT do cookie HttpOnly, delegam ao service do SDK e retornam dados ja transformados. Erros usam proxyErrorResponse() + unauthorizedNoToken() de ~/utils/proxy-error.server — nunca um envelope proprio em 401/403. Ver Services. :::

Metodos de pagamento

PaymentMethodSlug (@cactus-agents/payments) e a uniao dos slugs conhecidos. O slug literal que o BFF devolve sempre segue intacto pro BFF; o que o front decide a partir dele e apenas qual tela renderizar. resolveCanonicalMethod() colapsa variacoes de teste (credit-card-blabla, kushki-teste) no slug canonico antes de escolher a tela.

Os grupos abaixo saem de packages/payments/src/transform.ts — consulte lá pra lista corrente:

GrupoSlugs
Pix e agregadores Pix (BR)pix, pixtopay, oktopay, paybrokers, anspacepay, pay2free, riopag, paag, triopay, starkbank, efibank, genialbank, iugu
SPEI / OXXO (MX)spei, oxxo
Telefoneastropay
Redirect (iframe com URL da resposta)astropay, worldline, prontopaga, crypto
Checkout page (iframe)credit-card, bank-transfer, khipu, pixtopay-global, dusupay, korapay
Formulario de cartaocredit-card-2, credit-card-prontopaga
Kushki (CL)kushki
Wallet / MACH (CL)wallet
Bank transferbank-transfer (deposito e saque)
Redirect externo (nova aba)webpay, bitfever, pixtopay-global-webpay, wallet-redirect

trio (PE / FI) tambem faz parte de PaymentMethodSlug, mas nao esta em nenhum dos conjuntos acima — ele aparece como provider_code do Hub, nao como metodo com tela dedicada.

Notas de contexto:

  • dusupay / korapay — agregadores nigerianos. Ambos devolvem um checkout_page hospedado que embeda como iframe (cobrem transferencia bancaria, USSD, mobile money e cartao no proprio checkout). Mesmo caminho de render do khipu.
  • prontopaga — gateway CL/LatAm. Aparece em duas formas: prontopaga (redirect com iframe) e credit-card-prontopaga (formulario de cartao no front). No Chile, o saque via ProntoPaga tambem e um dos gates da validacao de identidade RUT/DIDIT (ver KYC).
  • pixtopay-global vs pixtopay-global-webpay — o primeiro (Payku) embeda como iframe; o segundo (ETPay) precisa abrir em nova aba. Sao slugs distintos exatamente por isso.
  • wallet-redirect — wallet do Hub pago em pagina externa. Listado como metodo conhecido exato pra que resolveCanonicalMethod nao o colapse em wallet (o prefixo wallet- cairia na tela de QR do MACH).

Kushki (CL) — tokenizacao client-side

O kushki tem pipeline proprio (tokenizar → depositar), por isso nao entra no grupo de formulario de cartao:

  1. KushkiCardForm (app/components/payments/kushki/) coleta os dados do cartao.
  2. O SDK publico do Kushki (https://cdn.kushkipagos.com/kushki.min.js) tokeniza no browser — dado de cartao nao sai do navegador em claro (PCI).
  3. Apenas o token vai pro BFF. O transform grava o mesmo valor em duas chaves: kushki_token (contrato legado, ainda vivo) e card_token (contrato novo de one-step payments). A duplicacao e transitoria: kushki_token sai quando a integracao legada for desligada.

Config em app/config/payments/kushki.ts:

export const kushkiConfig: KushkiConfig = {
merchantId: "",
testEnvironment: false,
};

O base envia vazio. Brands CL que usam Kushki declaram no override (overrides/<brand>/app/config/payments/kushki.ts). O merchantId e publico — ele vai ao browser na propria chamada de tokenizacao. Secret keys (que autorizam transacao) ficam no BFF.

Brands que nao usam Kushki nao tem o slug kushki na resposta do BFF, entao o metodo nunca aparece no selector e o merchantId vazio e inocuo.

Configuracao — app/config/payments/methods.ts

Whitelist do que o front expoe, sobre o que o BFF retornar:

export const paymentsConfig: PaymentsConfig = {
deposit: "all",
withdraw: "all",
groupPixProviders: true,
};

Default passa tudo. Brands brasileiras herdam isso e recebem so Pix porque o BFF retorna so Pix — nenhuma config explicita necessaria. O MethodSelector aparece automaticamente quando ha 2+ metodos.

Override quando a brand precisa restringir o que o BFF expoe:

// overrides/<brand>/app/config/payments/methods.ts
export const paymentsConfig: PaymentsConfig = {
deposit: ["pix", "credit-card-2"],
withdraw: ["pix"],
};

Configuracao — app/config/payments/deposit.ts

Atalhos de valor por moeda (brand-overridable):

export const depositConfig: DepositConfig = {
shortcuts: {
BRL: [
{ value: 20 }, { value: 50, hot: true }, { value: 100 },
{ value: 250, hot: true }, { value: 500 }, { value: 1000, hot: true },
],
// ...
},
defaultShortcuts: [
{ value: 20 }, { value: 50, hot: true }, { value: 100 },
{ value: 250, hot: true }, { value: 500 }, { value: 1000, hot: true },
],
};

Moedas com atalhos hoje (11): BRL, MXN, ARS, CLP, COP, UYU, INR, NGN, PEN, PHP, EUR. Moedas sem entrada usam defaultShortcuts.

Componentes de deposito

DepositModal

O modal tem quatro telas (DepositScreen): "form", "kushki-card", "result", "success".

  1. MethodSelector — selecao do metodo
  2. DepositForm — valor + campo especifico do metodo (CreditCardFields, DepositDocumentInput, DepositBirthDateInput, DepositCoupon)
  3. KushkiCardForm — apenas quando o metodo e kushki (tela dedicada)
  4. Resultado — roteado pelos classificadores do SDK:
ClassificadorComponenteCobertura
isPixMethodDepositResultPixQR code + copia-e-cola + countdown + polling
isSpeiMethodDepositResultSpeiCLABE + barcode + countdown
isWalletMethodDepositResultWalletQR + deep-link (MACH) + countdown + polling
isCheckoutPageMethodDepositResultCheckoutPageIframe com checkout_page
isBankTransferDepositMethodDepositResultBankTransferDados de transferencia
isCreditCardFormMethodDepositResultCreditCardTela de confirmacao pos-submit
isRedirectMethodDepositResultRedirectIframe com URL da resposta
isExternalRedirectMethodDepositResultExternalRedirectAbre nova aba + tela informativa
isKushkiMethod(tela kushki-card + fluxo proprio)Tokeniza e depois deposita

Componentes auxiliares do fluxo: PaymentStepper, DepositSuccessStep, DepositUrgencyBanner, FirstDepositBonusBanner, FirstDepositCountdown.

WithdrawModal

Telas: WithdrawValueStepWithdrawPixKeyStep (quando aplicavel) → WithdrawConfirmWithdrawSuccess, com WithdrawRules e WithdrawCooldownOverlay como blocos de apoio.

Dados especificos por metodo:

  • PIX: PixKeySelector (tipo + valor da chave)
  • SPEI: tipo de conta (CLABE / debito / telefone) + numero
  • Bank-transfer (CL): tipo de conta + codigo do banco + numero
  • Wallet (CL): info do usuario (email + documento)
  • AstroPay: telefone com DDI

Componentes compartilhados

  • CurrencyInput — input com mascara de moeda. Aceita countryCodeAlpha3 opcional pra resolver a bandeira, via getCountriesByCurrency / getCountryByAlpha3 de @cactus-agents/country-config
  • AmountShortcuts — botoes de atalho de valor
  • CopyableField — campo com botao de copiar (PIX, CLABE)
  • PixKeySelector — seletor de tipo de chave PIX
  • CreditCardFields — formulario de cartao com icones de bandeiras
  • DepositCoupon — campo de cupom promocional

:::tip Formatacao de dinheiro Use useFormatMoney() de @cactus-agents/accounts/react — ele resolve locale + currency + displayDecimalDigits automaticamente. Nao monte Intl.NumberFormat({ style: "currency" }) a mao em componentes React. :::

Icones de pagamento

Os SVGs vem do pacote @cactus-agents/payments/icons/. Um plugin Vite (copyPaymentIcons em vite.config.ts) copia os icones para public/icons/ no build e no dev server.

public/icons/payments/ esta no .gitignore — nao precisa commitar, sao auto-gerados.

Para adicionar um icone novo, coloque o SVG em front-cactus-core/packages/payments/icons/payments/ e publique uma versao nova do pacote.

Abrindo o modal programaticamente

import { usePaymentsStore } from "~/store/payments";

const openPaymentModal = usePaymentsStore((s) => s.openPaymentModal);

openPaymentModal("deposit"); // abre deposito
openPaymentModal("withdraw"); // abre saque