You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 12:47:02