Next.js 13中Stripe支付表单提交失败问题排查
Stripe Payment Intent支付提交无反应问题
在Next.js 13项目中使用Stripe Payment Intent功能,支付意图创建后状态为pending、金额显示、购物车变更同步都正常,但提交支付时表单直接重置,控制台无报错,无法完成支付。已集成@stripe/react-stripe-js的Address Element和Payment Element,代码如下:
CheckoutForm组件代码
"use client"; import { Button } from "@/components/ui/button"; import { useCart } from "@/hooks/use-cart"; import { FormatPrice } from "@/lib/format-price"; import { AddressElement, PaymentElement, useElements, useStripe, } from "@stripe/react-stripe-js"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; interface CheckoutFormProps { clientSecret: string; handleSetPaymentSuccess: (value: boolean) => void; } export const CheckoutForm: React.FC<CheckoutFormProps> = ({ clientSecret, handleSetPaymentSuccess, }) => { const { cartTotalAmount, clearWholeCart, handleSetPaymentIntent } = useCart(); const stripe = useStripe(); const elements = useElements(); const [isLoading, setIsLoading] = useState(false); const formattedPrice = FormatPrice(cartTotalAmount); useEffect(() => { if (!stripe) { return; } if (!clientSecret) { return; } handleSetPaymentSuccess(false); }, [stripe, clientSecret, handleSetPaymentSuccess]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { return; } setIsLoading(true); stripe .confirmPayment({ elements, redirect: "if_required", }) .then((result) => { if (!result.error) { toast.success("Payment Success! 😊"); clearWholeCart(); handleSetPaymentSuccess(true); handleSetPaymentIntent(null); } }); setIsLoading(false); }; return ( <form onSubmit={handleSubmit} id="payment-form"> <div className="mb-6"> <h1 className="text-2xl text-slate-900 mt-5 text-center font-semibold"> Enter payment details to complete checkout </h1> </div> <h2 className="font-semibold mt-4 mb-2">Shipping Address</h2> <AddressElement options={{ mode: "shipping" }} /> <h2 className="font-semibold mt-4 mb-2">Payment Information</h2> <PaymentElement id="payment-element" options={{ layout: "tabs" }} /> <div className="py-4 text-center text-slate-700 text-lg font-bold"> Total: {formattedPrice} </div> <div className="mb-4"> <Button className="w-full" disabled={isLoading}> {isLoading ? "Processing..." : "Complete Order"} </Button> </div> </form> ); };
CheckoutClient组件代码
"use client"; import { useCart } from "@/hooks/use-cart"; import { StripeElementsOptions, loadStripe } from "@stripe/stripe-js"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import toast from "react-hot-toast"; import { Elements } from "@stripe/react-stripe-js"; import { ImSpinner2 } from "react-icons/im"; import { CheckoutForm } from "./checkout-form"; import { AiOutlineCheckCircle } from "react-icons/ai"; import { Button } from "@/components/ui/button"; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string ); export const CheckoutClient = () => { const router = useRouter(); const { cartProducts, paymentIntent, handleSetPaymentIntent } = useCart(); const [error, setError] = useState(false); const [isLoading, setIsLoading] = useState(false); const [clientSecret, setClientSecret] = useState(""); const [paymentSuccess, setPaymentSuccess] = useState(false); useEffect(() => { if (cartProducts) { setIsLoading(true); setError(false); fetch("/api/create-payment-intent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: cartProducts, payment_intent_id: paymentIntent, }), }) .then((res) => { setIsLoading(false); if (res.status === 401) { return router.push("/login"); } return res.json(); }) .then((data) => { setClientSecret(data.paymentIntent.client_secret); handleSetPaymentIntent(data.paymentIntent.id); }) .catch((error) => { setError(true); toast.error("Something went wrong!"); }); } }, [cartProducts, handleSetPaymentIntent, paymentIntent, router]); const options: StripeElementsOptions = { clientSecret, appearance: { theme: "stripe", labels: "floating", }, }; const handleSetPaymentSuccess = useCallback((value: boolean) => { setPaymentSuccess(value); }, []); return ( <div className="w-full"> {clientSecret && cartProducts && ( <Elements options={options} stripe={stripePromise}> <CheckoutForm clientSecret={clientSecret} handleSetPaymentSuccess={handleSetPaymentSuccess} /> </Elements> )} {isLoading && ( <div className="text-3xl flex flex-col items-center justify-center w-full h-full gap-3"> <h1>Loading Checkout...</h1> <ImSpinner2 className="animate-spin" /> </div> )} {error && ( <div className="text-3xl flex items-center justify-center w-full h-full text-rose-500"> <h1>Something went wrong</h1> </div> )} {paymentSuccess && ( <div className="text-3xl flex flex-col items-center justify-center w-full h-full gap-4"> <div> <h1 className="flex items-center gap-2"> Payment Success <span> <AiOutlineCheckCircle className="text-green-500" /> </span> </h1> </div> <div> <Button onClick={() => router.push("/order")}> View your orders </Button> </div> </div> )} </div> ); };
问题分析与解决方案
1. 修复异步逻辑错误
handleSubmit中setIsLoading(false)被放在stripe.confirmPayment的Promise外面,导致加载状态立即被重置,同时没有等待支付确认完成就执行后续操作。此外,未处理支付失败的错误情况,导致表单默认提交行为触发重置。
修改后的handleSubmit代码:
const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { return; } setIsLoading(true); try { const result = await stripe.confirmPayment({ elements, redirect: "if_required", }); if (!result.error) { toast.success("Payment Success! 😊"); clearWholeCart(); handleSetPaymentSuccess(true); handleSetPaymentIntent(null); } else { toast.error(result.error.message || "Payment failed"); } } catch (error) { toast.error("An unexpected error occurred"); } finally { setIsLoading(false); } };
2. 验证Payment Intent后端配置
确保/api/create-payment-intent中创建Payment Intent时:
amount设置为最小货币单位(如人民币分、美元分)- 指定了正确的
currency参数 - 若使用Address Element,需在Payment Intent中配置
shipping相关字段,或在confirmPayment时传递收货地址信息 - 检查是否开启了手动确认模式,若开启需后端调用
confirmPaymentIntent接口完成支付
3. 排查Stripe控制台日志
登录Stripe Dashboard,查看对应支付意图的状态变化和详细日志,确认是否存在卡片被拒绝、金额不匹配、后端签名错误等具体原因。
4. 确认Elements初始化完整性
确保clientSecret完全加载后再渲染Elements组件,当前代码中的clientSecret && cartProducts &&判断已满足要求,可保留该逻辑避免初始化不完整。
内容的提问来源于stack exchange,提问作者MrRobot94
相关产品推荐
相关产品推荐

