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
| Arquivo | Tipo | Descricao |
|---|---|---|
app/store/payments.ts | Store (Zustand) | Estado dos modais, providers, metodo selecionado, cupom, auto-open |
app/hooks/usePayments.ts | Hook | Orquestra fetch providers, submit deposit/withdraw, polling |
app/config/payments/deposit.ts | Config | Atalhos de valor por moeda (brand-overridable) |
app/config/payments/methods.ts | Config | Whitelist de metodos expostos (paymentsConfig) |
app/config/payments/kushki.ts | Config | merchantId do gateway Kushki (CL) |
app/config/payments/testers.ts | Config | Contas de teste |
app/components/payments/DepositModal.tsx | Component | Modal de deposito |
app/components/payments/WithdrawModal.tsx | Component | Modal de saque |
app/components/payments/LazyPaymentModals.tsx | Component | Lazy 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
| Propriedade | Descricao |
|---|---|
depositMethods / withdrawMethods | Providers filtrados pela moeda da marca e pela whitelist do paymentsConfig |
defaultDepositMethod / defaultWithdrawMethod | Metodo pre-selecionado |
selectedDepositProvider / selectedWithdrawProvider | Provider do metodo selecionado |
minDeposit / maxDeposit | Limites de deposito (brand settings) |
minWithdraw / maxWithdraw | Limites de saque (brand settings) |
currency / country | Moeda e pais do ambiente |
Actions
| Action | Descricao |
|---|---|
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
| Rota | Descricao |
|---|---|
api/payments/providers | Providers de deposito/saque |
api/payments/deposit | Submit deposito |
api/payments/deposit-status | Status do deposito (polling) |
api/payments/withdraw | Submit saque |
api/payments/bank-list | Lista de bancos |
api/payments/pix-key | Get/update chave PIX |
api/payments/coupon | Validar cupom |
api/payments/bank-account | Conta 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:
| Grupo | Slugs |
|---|---|
| Pix e agregadores Pix (BR) | pix, pixtopay, oktopay, paybrokers, anspacepay, pay2free, riopag, paag, triopay, starkbank, efibank, genialbank, iugu |
| SPEI / OXXO (MX) | spei, oxxo |
| Telefone | astropay |
| Redirect (iframe com URL da resposta) | astropay, worldline, prontopaga, crypto |
| Checkout page (iframe) | credit-card, bank-transfer, khipu, pixtopay-global, dusupay, korapay |
| Formulario de cartao | credit-card-2, credit-card-prontopaga |
| Kushki (CL) | kushki |
| Wallet / MACH (CL) | wallet |
| Bank transfer | bank-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 umcheckout_pagehospedado que embeda como iframe (cobrem transferencia bancaria, USSD, mobile money e cartao no proprio checkout). Mesmo caminho de render dokhipu.prontopaga— gateway CL/LatAm. Aparece em duas formas:prontopaga(redirect com iframe) ecredit-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-globalvspixtopay-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 queresolveCanonicalMethodnao o colapse emwallet(o prefixowallet-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:
KushkiCardForm(app/components/payments/kushki/) coleta os dados do cartao.- 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). - Apenas o token vai pro BFF. O transform grava o mesmo valor em duas chaves:
kushki_token(contrato legado, ainda vivo) ecard_token(contrato novo de one-step payments). A duplicacao e transitoria:kushki_tokensai 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".
MethodSelector— selecao do metodoDepositForm— valor + campo especifico do metodo (CreditCardFields,DepositDocumentInput,DepositBirthDateInput,DepositCoupon)KushkiCardForm— apenas quando o metodo ekushki(tela dedicada)- Resultado — roteado pelos classificadores do SDK:
| Classificador | Componente | Cobertura |
|---|---|---|
isPixMethod | DepositResultPix | QR code + copia-e-cola + countdown + polling |
isSpeiMethod | DepositResultSpei | CLABE + barcode + countdown |
isWalletMethod | DepositResultWallet | QR + deep-link (MACH) + countdown + polling |
isCheckoutPageMethod | DepositResultCheckoutPage | Iframe com checkout_page |
isBankTransferDepositMethod | DepositResultBankTransfer | Dados de transferencia |
isCreditCardFormMethod | DepositResultCreditCard | Tela de confirmacao pos-submit |
isRedirectMethod | DepositResultRedirect | Iframe com URL da resposta |
isExternalRedirectMethod | DepositResultExternalRedirect | Abre 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: WithdrawValueStep → WithdrawPixKeyStep (quando aplicavel) → WithdrawConfirm → WithdrawSuccess, 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. AceitacountryCodeAlpha3opcional pra resolver a bandeira, viagetCountriesByCurrency/getCountryByAlpha3de@cactus-agents/country-configAmountShortcuts— botoes de atalho de valorCopyableField— campo com botao de copiar (PIX, CLABE)PixKeySelector— seletor de tipo de chave PIXCreditCardFields— formulario de cartao com icones de bandeirasDepositCoupon— 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