Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Crie um PaymentIntent no servidor para obter um client secret, passe-o para o provedor Elements, renderize um PaymentElement para entrada de cartão e confirme o pagamento no lado do cliente.
Crie o PaymentIntent no lado do servidor:
// app/actions/payment.ts
"use server";
import { stripe } from "@/lib/stripe";
export async function createPaymentIntent(amount: number) {
const paymentIntent = await stripe.paymentIntents.create({
amount, // em centavos
currency: "usd",
automatic_payment_methods: { enabled: true },
});
return { clientSecret: paymentIntent.client_secret! };
}Envolva o formulário de checkout com Elements usando o client secret:
// app/checkout/page.tsx
"use client";
import { useEffect, useState } from "react";
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
import { createPaymentIntent } from "@/app/actions/payment";
import { CheckoutForm } from "./checkout-form";
export default function CheckoutPage() {
const [clientSecret, setClientSecret] = useState<string | null>(null);
useEffect(() => {
createPaymentIntent(2999).then(({ clientSecret }) => {
setClientSecret(clientSecret);
});
}, []);
if (!clientSecret) return <div>Carregando...</div>;
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
);
}Construa o formulário de checkout:
// app/checkout/checkout-form.tsx
"use client";
import { useState, type FormEvent } from "react";
import {
useStripe,
useElements,
PaymentElement,
} from "@stripe/react-stripe-js";
export function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [processing, setProcessing] = useState(false);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
setError(null);
const { error: submitError } = await elements.submit();
if (submitError) {
setError(submitError.message ?? "Falha na validação.");
setProcessing(false);
return;
}
const { error: confirmError } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
});
if (confirmError) {
setError(confirmError.message ?? "Falha no pagamento.");
setProcessing(false);
}
// Se for bem-sucedido, o Stripe redireciona para return_url
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
<PaymentElement />
{error && <p className="text-red-500 mt-4">{error}</p>}
<button
type="submit"
disabled={!stripe || processing}
className="w-full mt-6 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{processing ? "Processando..." : "Pagar $29.99"}
</button>
</form>
);
}// app/donate/page.tsx
"use client";
import { useEffect, useState } from "react";
import { Elements } from "@stripe/react-stripe-js";
import { stripePromise } from "@/lib/stripe-client";
import { createPaymentIntent } from "@/app/actions/payment";
import { CheckoutForm } from "@/app/checkout/checkout-form";
const DONATION_AMOUNTS = [500, 1000, 2500, 5000];
export default function DonatePage() {
const [amount, setAmount] = useState(1000);
const [clientSecret, setClientSecret] = useState<string | null>(null);
useEffect(() => {
setClientSecret(null);
createPaymentIntent(amount).then(({ clientSecret }) => {
setClientSecret(clientSecret);
});
}, [amount]);
return (
<div className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-6">Faça uma Doação</h1>
<div className="flex gap-2 mb-6">
{DONATION_AMOUNTS.map((amt) => (
<button
key={amt}
onClick={() => setAmount(amt)}
className={`px-4 py-2 rounded ${
amount === amt
? "bg-blue-600 text-white"
: "bg-gray-200 text-gray-800"
}`}
>
${(amt / 100).toFixed(0)}
</button>
))}
</div>
{clientSecret ? (
<Elements
stripe={stripePromise}
options={{ clientSecret }}
key={clientSecret}
>
<CheckoutForm />
</Elements>
) : (
<div>Carregando formulário de pagamento...</div>
)}
</div>
);
}client_secret é um token que permite que o código do lado do cliente confirme o pagamento sem expor sua chave secreta. Ele nunca deve ser registrado ou armazenado.automatic_payment_methods: { enabled: true } informa ao Stripe para mostrar dinamicamente os melhores métodos de pagamento para a localização do cliente e a moeda da transação.elements.submit() valida todos os campos do formulário antes da confirmação. Sempre chame-o antes de confirmPayment para capturar erros de validação antecipadamente.stripe.confirmPayment envia os detalhes do pagamento diretamente do navegador para o Stripe. Os dados do cartão nunca passam pelo seu servidor.return_url. Para 3D Secure, o Stripe lida com o fluxo de autenticação automaticamente.Confirmar sem redirecionamento (permanecer na página):
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
redirect: "if_required",
});
if (error) {
setError(error.message ?? "Falha no pagamento.");
} else if (paymentIntent?.status === "succeeded") {
// Exibir mensagem de sucesso sem redirecionar
setSuccess(true);
}Adicionar metadados ao PaymentIntent:
const paymentIntent = await stripe.paymentIntents.create({
amount: 2999,
currency: "usd",
automatic_payment_methods: { enabled: true },
metadata: {
userId: user.id,
productId: product.id,
},
});stripe.paymentIntents.create retorna Promise<Stripe.PaymentIntent>.confirmPayment inclui { error?: StripeError; paymentIntent?: PaymentIntent }.redirect: "if_required", sempre verifique ambos error e paymentIntent no resultado.import type { StripeError } from "@stripe/stripe-js";
function handleError(error: StripeError) {
switch (error.type) {
case "card_error":
return error.message;
case "validation_error":
return "Por favor, verifique os detalhes do seu cartão.";
default:
return "Ocorreu um erro inesperado.";
}
}useEffect com dependências estáveis.client_secret contém o ID do PaymentIntent. Trate-o como sensível e não o exponha em URLs ou logs.key no Elements para forçar uma remontagem completa quando o clientSecret mudar.confirmPayment com redirect: "always" (o padrão) sempre redirecionará, mesmo para pagamentos com cartão. Use redirect: "if_required" se quiser permanecer na página.| Abordagem | Prós | Contras |
|---|---|---|
| PaymentIntent + PaymentElement | Controle total da UI, suporta todos os métodos de pagamento | Mais código que o Checkout |
| Stripe Checkout | Código mínimo, UI hospedada | Personalização limitada |
| SetupIntent | Salva o cartão sem cobrar | Requer etapa de cobrança separada posteriormente |
| PaymentIntent + CardElement | Controle granular dos campos do cartão | Suporta apenas pagamentos com cartão |
client_secret é um token que permite que o código do lado do cliente confirme o pagamento sem expor sua chave secreta da API.elements.submit() valida todos os campos do formulário primeiro. Chamá-lo antes de confirmPayment captura erros de validação (campos ausentes, formato de cartão inválido) antecipadamente, proporcionando uma melhor experiência ao usuário antes de tentar a cobrança real.
Ele informa ao Stripe para selecionar e exibir dinamicamente os melhores métodos de pagamento com base na localização do cliente, na moeda da transação e nas configurações do seu Painel Stripe -- sem que você codifique métodos específicos.
Você acaba com PaymentIntents órfãos nos servidores do Stripe e um comportamento potencialmente confuso. Sempre crie o PaymentIntent em um useEffect com dependências estáveis para que ele seja executado uma vez por tentativa de checkout.
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
redirect: "if_required",
});
if (paymentIntent?.status === "succeeded") {
setSuccess(true);
}Use redirect: "if_required" -- ele só redireciona quando o método de pagamento o exige (por exemplo, 3D Secure).
Alterar a prop clientSecret por si só não redefine o estado interno do Stripe Elements. Usar key={clientSecret} força o React a desmontar e remontar o componente <Elements>, criando uma nova instância vinculada ao novo PaymentIntent.
const paymentIntent = await stripe.paymentIntents.create({
amount: 2999,
currency: "usd",
automatic_payment_methods: { enabled: true },
metadata: { userId: user.id, productId: product.id },
});Metadados são úteis para correlacionar pagamentos com os dados do seu aplicativo em manipuladores de webhook.
Os valores são na menor unidade monetária. Para USD, 2999 = $29.99 (centavos). Para JPY (uma moeda sem decimais), 2999 = 2999 ienes. Sempre verifique se a moeda tem casas decimais.
// Retorna { error?: StripeError; paymentIntent?: PaymentIntent }
// Ao usar redirect: "if_required", sempre verifique ambos os campos
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
redirect: "if_required",
});import type { StripeError } from "@stripe/stripe-js";
function handleError(error: StripeError) {
switch (error.type) {
case "card_error":
return error.message;
case "validation_error":
return "Verifique os detalhes do seu cartão.";
default:
return "Ocorreu um erro inesperado.";
}
}O padrão é redirect: "always", que redireciona o usuário mesmo para pagamentos simples com cartão que não exigem 3D Secure. Se você quiser lidar com o resultado no lado do cliente, deve definir explicitamente redirect: "if_required".
Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥