@bemony/payment-form-react
Adaptador React oficial do @bemony/payment-form.
O pacote monta a mesma instância segura do SDK Web e oferece duas formas de integração:
ref, para checkouts em que formulário e botão estão próximos;BemonyPaymentFormProvider+useBemonyPaymentForm, para compartilhar estado epay()entre componentes.
O adaptador não realiza requests financeiros diretamente, não contém regras próprias de pagamento e não duplica o estado canônico do SDK Web.
Requisitos
- React
>=19 <20; - React DOM
>=19 <20; - CheckoutSession criada no backend;
- origem do checkout cadastrada na Bemony;
- CSP compatível com os frames e collectors utilizados.
Instalação
pnpm add @bemony/payment-form-react
@bemony/payment-form é instalado como dependência do adaptador. Não é necessário montar os dois
pacotes separadamente.
Preparação no backend
Crie a CheckoutSession no servidor usando a Secret Key. O navegador deve receber somente o
sessionClientSecret e as instruções públicas de riskCollectors.
const response = await fetch(
`${process.env.BEMONY_ORCHESTRATOR_API_BASE_URL}/v1/checkout-sessions`,
{
method: 'POST',
headers: {
authorization: `Bearer ${process.env.BEMONY_SECRET_KEY}`,
'content-type': 'application/json',
'idempotency-key': crypto.randomUUID(),
'x-bemony-api-version': '2026-11-02'
},
body: JSON.stringify({
mode: 'CREATE',
domainId: 'cdom_...',
checkout: { checkoutConfigId: 'ccfg_...' }
})
}
);
if (!response.ok) throw new Error('Não foi possível criar a CheckoutSession.');
const session = await response.json();
Nunca coloque BEMONY_SECRET_KEY em variável pública do React, bundle do navegador ou propriedade
do componente.
Integração com ref
Use esta forma quando o botão que chama pay() pertence ao mesmo componente.
'use client';
import {
BemonyPaymentForm,
PAYMENT_FORM_API_VERSION,
type BemonyPaymentFormRef,
type PayInput,
type PaymentFormError,
type RiskCollector
} from '@bemony/payment-form-react';
import { useRef, useState } from 'react';
type CheckoutProps = Readonly<{
sessionClientSecret: string;
riskCollectors?: readonly RiskCollector[];
}>;
export function Checkout({ sessionClientSecret, riskCollectors }: CheckoutProps) {
const formRef = useRef<BemonyPaymentFormRef>(null);
const [ready, setReady] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<PaymentFormError | null>(null);
const input: PayInput = {
merchantOrderId: 'order_123',
merchantReference: 'merchant-reference-123',
currency: 'BRL',
operation: 'PURCHASE',
customer: {
firstName: 'Ana',
lastName: 'Silva',
email: '[email protected]'
},
items: [
{
id: 'item_123',
sku: 'SKU-123',
description: 'Produto',
unitAmount: 10990,
quantity: 1,
type: 'ONE_TIME'
}
],
amount: 10990,
context: { funnelId: 'checkout', funnelStep: 'payment', channel: 'WEB' }
};
async function submit() {
setSubmitting(true);
setError(null);
try {
const payment = await formRef.current!.pay(input);
console.log(payment.paymentId, payment.status);
} catch (cause) {
setError(cause as PaymentFormError);
} finally {
setSubmitting(false);
}
}
return (
<section>
<BemonyPaymentForm
ref={formRef}
clientSecret={sessionClientSecret}
environment="sandbox"
version={PAYMENT_FORM_API_VERSION.TOKEN_INTENT}
riskCollectors={riskCollectors}
threeDS={{ presentation: 'modal' }}
appearance={{ theme: 'system' }}
onReady={() => setReady(true)}
onStatusChange={(status) =>
setSubmitting(
['TOKENIZING', 'SUBMITTING', 'AUTHENTICATING_3DS', 'PROCESSING'].includes(status)
)
}
onError={setError}
/>
{error && <p role="alert">{error.message}</p>}
<button type="button" disabled={!ready || submitting} onClick={() => void submit()}>
{submitting ? 'Processando…' : 'Pagar'}
</button>
</section>
);
}
Integração com Provider e hook
Use esta forma quando o botão, resumo ou tratamento de estado está em outro componente. O Provider
é a autoridade da configuração; não repita clientSecret, environment ou version no componente
filho.
'use client';
import {
BemonyPaymentForm,
BemonyPaymentFormProvider,
PAYMENT_FORM_API_VERSION,
useBemonyPaymentForm,
type PayInput,
type RiskCollector
} from '@bemony/payment-form-react';
function PayButton({ input }: Readonly<{ input: PayInput }>) {
const { ready, state, error, pay } = useBemonyPaymentForm();
return (
<>
{error && <p role="alert">{error.message}</p>}
<button type="button" disabled={!ready || state.isSubmitting} onClick={() => void pay(input)}>
{state.isSubmitting ? 'Processando…' : 'Pagar'}
</button>
</>
);
}
type CheckoutWithProviderProps = Readonly<{
session: Readonly<{
sessionClientSecret: string;
riskCollectors?: readonly RiskCollector[];
}>;
input: PayInput;
}>;
export function CheckoutWithProvider({ session, input }: CheckoutWithProviderProps) {
return (
<BemonyPaymentFormProvider
clientSecret={session.sessionClientSecret}
environment="sandbox"
version={PAYMENT_FORM_API_VERSION.TOKEN_INTENT}
riskCollectors={session.riskCollectors}
threeDS={{ presentation: 'modal' }}
>
<BemonyPaymentForm className="payment-form" />
<PayButton input={input} />
</BemonyPaymentFormProvider>
);
}
useBemonyPaymentForm() retorna:
| Campo | Tipo | Uso |
|---|---|---|
ready | boolean | formulário pronto para receber pay() |
state | PaymentFormState | estado, envio, resultado e erro atuais |
error | PaymentFormError | null | último erro público |
pay | (input: PayInput) => Promise<PayOutcome> | inicia ou compartilha a operação ativa |
O hook só pode ser usado dentro de BemonyPaymentFormProvider.
Props
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
clientSecret | string | obrigatório | segredo público da CheckoutSession |
environment | 'sandbox' | 'production' | 'production_aws' | sandbox | infraestrutura hospedada |
version | '2026-09-25' | '2026-10-20' | '2026-10-30' | 2026-10-30 | compatibilidade do protocolo hospedado |
riskCollectors | readonly RiskCollector[] | ausente | instruções públicas devolvidas pela sessão |
appearance | PaymentFormAppearance | sistema | tema e variáveis visuais |
threeDS | { presentation?: 'modal' | 'inline' } | modal | apresentação do challenge |
className | string | ausente | classe aplicada ao container React |
onReady | () => void | ausente | iframe e Card Frame prontos |
onCardStateChange | (state: CardState) => void | ausente | completude e validade dos campos |
onStatusChange | (status) => void | ausente | transição do estado público |
onSuccess | (payment) => void | ausente | pagamento AUTHORIZED ou CAPTURED |
onError | (error) => void | ausente | mesmo erro público rejeitado por pay() |
onEvent | (event) => void | ausente | telemetria segura de montagem/pagamento |
No Provider, children também é obrigatório. No componente standalone, clientSecret é
obrigatório; dentro do Provider, a configuração vem do contexto.
Bandeiras de cartão habilitadas
Não passe uma lista de bandeiras por prop. A CheckoutSession é a autoridade e o Card Frame aplica
a configuração. Se uma projeção legada estiver ausente ou inválida, o frame assume apenas VISA e
MASTERCARD para preservar esses pagamentos; nenhuma outra rede é habilitada por esse fallback.
Quando a bandeira detectada não é aceita, onCardStateChange informa valid: false e nenhum token
é criado; desabilite o botão enquanto o estado for inválido.
<BemonyPaymentForm
clientSecret={sessionClientSecret}
onCardStateChange={(state) => setCanPay(state.valid)}
/>
Caso pay() seja chamado, capture PaymentFormError.code === 'CARD_BRAND_NOT_SUPPORTED'. É uma
falha local, retryable: true, classificada como VALIDATION em CARD_TOKENIZATION; o usuário
pode corrigir o cartão sem gerar uma tentativa financeira.
Versões da API
import {
DEFAULT_PAYMENT_FORM_API_VERSION,
PAYMENT_FORM_API_VERSION,
type PaymentFormApiVersion
} from '@bemony/payment-form-react';
| Versão | Constante | Uso |
|---|---|---|
2026-09-25 | PAYMENT_FORM_API_VERSION.LEGACY | contrato legado |
2026-10-20 | PAYMENT_FORM_API_VERSION.ENRICHMENT | enriquecimento e body completo |
2026-10-30 | PAYMENT_FORM_API_VERSION.TOKEN_INTENT | Token Intent/CVV |
O padrão do protocolo hospedado é DEFAULT_PAYMENT_FORM_API_VERSION, atualmente 2026-10-30. O
tipo é fechado e o valor é validado novamente pelo SDK e pelo hosted BFF. A versão enviada pelo BFF
à API é definida pelo deploy via BEMONY_API_VERSION.
A prop version controla somente o handshake entre o adaptador, o SDK e o formulário hospedado.
Ela não substitui o header HTTP usado pelo backend para criar a CheckoutSession.
Ambientes
sandbox: padrão compatível atual conectado à Railway;production: produção legada hospedada na Vercel;production_aws: produção candidata hospedada na AWS.
O componente nunca recebe endpoint ou origem arbitrária. A configuração dos hosts pertence aos deploys Bemony.
Contrato de pay()
O adaptador reexporta os mesmos tipos do SDK Web:
import type {
Address,
CardStorageConsent,
CheckoutClient,
CommerceContext,
Customer,
CustomerHistory,
PayInput,
PayOutcome,
PaymentAmounts,
PaymentItem,
ShippingAddress
} from '@bemony/payment-form-react';
PayInput suporta pedido, customer, endereços, consentimento, itens, impostos/descontos, total de
integridade, contexto comercial, URLs, metadata e contexto do client. Sessão, token, PAN, CVC, IP,
User-Agent observado e evidência 3DS não pertencem ao input público.
Fallback de client por campo
Se o integrador enviar somente um campo de client, apenas esse campo substitui o valor observado.
Os demais continuam sendo preenchidos pelo SDK quando disponíveis. A mesma regra vale dentro de
client.utm e client.browserInfo; um objeto parcial nunca apaga o restante do contexto.
amount na versão enriquecida
amount é um eco opcional do total calculado pelo Runtime. Ele não define o valor cobrado. Uma
divergência retorna 422 AMOUNT_MISMATCH.
publicCheckoutUrl está tipado para a evolução do Runtime, mas não deve ser enviado antes de o
ambiente de destino publicar suporte ao campo.
Estados e erros
MOUNTING → READY → TOKENIZING → SUBMITTING → PROCESSING
├→ AUTHENTICATING_3DS → PROCESSING
├→ AUTHORIZED/CAPTURED
├→ DECLINED/FAILED/EXPIRED
└→ UNKNOWN
Trate a rejeição de pay() mesmo quando onError estiver configurado:
try {
const result = await formRef.current!.pay(input);
// Não faça fulfillment apenas com o resultado do browser.
console.log(result.paymentId);
} catch (cause) {
const error = cause as PaymentFormError;
console.error(error.code, error.requestId, error.details);
}
requestIdcorrelaciona a resposta do Runtime;commandIdidentifica o comando local;retryable: truepermite uma nova tentativa controlada, não replay automático;PAYMENT_STATUS_UNKNOWNexige consulta/webhook antes de qualquer nova cobrança;payment.transaction.attemptsé diagnóstico público, não fonte de fulfillment.
O produto consumidor escolhe a mensagem apresentada ao comprador. O Embed preserva o erro público da API e não reinterpreta o domínio financeiro.
Remontagem e React Strict Mode
O adaptador adia a montagem de forma cancelável para suportar o ciclo especulativo do React Strict Mode sem deixar dois iframes ativos.
Uma alteração real em clientSecret, environment, version, apresentação do 3DS, appearance ou
riskCollectors remonta o iframe. appearance e riskCollectors são comparados por valor, então
objetos equivalentes recriados durante renderizações comuns não provocam remontagem.
Callbacks são lidos pela referência mais recente e não exigem desmontar a instância quando apenas a função muda.
Next.js e SSR
Use o componente a partir de uma fronteira Client Component:
'use client';
import { BemonyPaymentForm } from '@bemony/payment-form-react';
Crie a CheckoutSession em Route Handler, Server Action protegida ou backend próprio. Envie ao Client
Component apenas sessionClientSecret e dados públicos da sessão. Nunca use uma variável
NEXT_PUBLIC_* para Secret Key ou token interno.
Aparência
<BemonyPaymentForm
clientSecret={clientSecret}
appearance={{
theme: 'dark',
variables: {
colorPrimary: '#00e68a',
colorBackground: '#101513',
colorText: '#ffffff',
colorDanger: '#ff5b6e',
colorBorder: '#33413a',
borderRadius: '12px',
fontFamily: 'Inter, sans-serif'
},
threeDS: { modalBackdrop: 'rgba(0, 0, 0, 0.72)' }
}}
/>
Risk collectors e CSP
Passe riskCollectors diretamente da CheckoutSession ao componente ou Provider. Collector lento,
bloqueado ou indisponível não bloqueia o pagamento e não deve ser persistido pelo checkout.
Durante a transição atual, use uma política consolidada que preserve as fontes exigidas pelo seu site e permita os hosts Bemony e os collectors habilitados:
default-src 'self';
frame-src https://form.bemony.com https://bemony-payment-form.vercel.app https://bemony-payment-form-sandbox.vercel.app https://embed-payment-orchestration.bemony.com;
script-src 'self' https://js.stripe.com https://tst.kaptcha.com https://ssl.kaptcha.com;
connect-src 'self' https://form.bemony.com https://bemony-payment-form.vercel.app https://bemony-payment-form-sandbox.vercel.app https://embed-payment-orchestration.bemony.com https://api.stripe.com https://r.stripe.com https://m.stripe.com https://tst.kaptcha.com https://ssl.kaptcha.com;
Migração e rollout
- publique primeiro o hosted Payment Form compatível com
apiVersion; - valide o hosted usando a versão antiga do pacote;
- publique Web e React com tag
latest; - atualize um consumidor de teste explicitamente para
0.2.0; - valide legado, enriquecimento, Token Intent, 3DS e reconciliação.
Omitir version usa 2026-10-30 no handshake. Consumidores antigos não mudam de pacote nem
recebem a nova implementação até atualizarem sua dependência.
Regras de segurança
- não envie Secret Key, token interno, PAN, CVC ou evidência 3DS por props;
- não registre
clientSecret, token do cartão ou valor bruto de collectors; - não permita que o browser escolha endpoints privados;
- não libere pedido apenas com
onSuccess; confirme por webhook ou consulta autenticada; - não implemente tokenização, 3DS ou retry financeiro fora do SDK.