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

Vercel+Supabase+Stripe环境clientSecret无法传递至前端排查求助

问题:Stripe clientSecret无法传递至前端,页面持续显示Loading...

页面无报错信息,但始终卡在“Loading...”状态,核心原因是clientSecret无法从Stripe经后端正确传递到前端。已确认页面加载处理逻辑正常,且.env.local配置无误。

当前排查方向:

  • 验证后端是否从Stripe正确获取到clientSecret
  • 检查PaymentsPage组件中的useEffect钩子是否存在问题

前端代码

"use client"

import { loadStripe } from '@stripe/stripe-js';
import { Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
import axios from 'axios';
import React, { useState, useEffect } from 'react';


const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');

// Define the packages
const packages = [
  { id: 'basic-001', name: 'Basic Package', description: 'Basic service offering.' },
  { id: 'standard-002', name: 'Standard Package', description: 'Enhanced features included.' },
  { id: 'premium-003', name: 'Premium Package', description: 'All features plus premium support.' },
  { id: 'exclusive-004', name: 'Exclusive Package', description: 'All premium features with exclusive benefits.' }
];

const PaymentsPage = () => {
  const [clientSecret, setClientSecret] = useState<string | undefined>(undefined); // Changed from null to undefined
  const [selectedPackage, setSelectedPackage] = useState(packages[0].id); // Moved up here

  useEffect(() => {
    axios.post('/api/checkout_sessions', { packageId: selectedPackage })
      .then(response => {
        setClientSecret(response.data.clientSecret);
      })
      .catch(error => {
        console.error('Error:', error);
      });
  }, [selectedPackage]); // Now depends on selectedPackage

  return (
    clientSecret ? (
      <Elements stripe={stripePromise} options={{ clientSecret }}>
        <CheckoutForm 
          selectedPackage={selectedPackage} 
          setSelectedPackage={setSelectedPackage}
        />
      </Elements>
    ) : (
      <div>Loading...</div>
    )
  );
};

type CheckoutFormProps = {
  selectedPackage: string;
  setSelectedPackage: React.Dispatch<React.SetStateAction<string>>;
};

const CheckoutForm = ({ selectedPackage, setSelectedPackage }: CheckoutFormProps) => {

  const stripe = useStripe();
  const elements = useElements();
  const [isLoading, setIsLoading] = useState(false);


  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setIsLoading(true);

    if (!stripe || !elements) {
      setIsLoading(false);
      return;
    }

    try {
      const response = await axios.post('/api/checkout_sessions', { packageId: selectedPackage });
      const clientSecret = response.data.clientSecret;

      const { error } = await stripe.confirmCardPayment(clientSecret);

      if (error) {
        console.log('[error]', error);
      } else {
        // Payment processed
      }
    } catch (error) {
      console.error('Error:', error);
    }
    setIsLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        {packages.map((pkg) => (
          <div key={pkg.id}>
            <input
              type="radio"
              id={pkg.id}
              name="package"
              value={pkg.id}
              checked={selectedPackage === pkg.id}
              onChange={(e) => setSelectedPackage(e.target.value)}
            />
            <label htmlFor={pkg.id}>{pkg.name}: {pkg.description}</label>
          </div>
        ))}
        </div>
        <PaymentElement />
        <button type="submit" disabled={!stripe || !elements || isLoading}>
          {isLoading ? 'Processing…' : 'Pay'}
        </button>
      </form>
    );
  };

export default PaymentsPage;

后端代码

import { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';




const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
  throw new Error('The STRIPE_SECRET_KEY environment variable is not set.');
}

const stripe = new Stripe(stripeSecretKey, {
  apiVersion: '2023-10-16',
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
    return;
  }

  try {
    const { packageId } = req.body;
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [{
        price: packageId,
        quantity: 1,
      }],
      mode: 'payment',
      success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/canceled`,
    });

    // Here we obtain the PaymentIntent's clientSecret from the session
    if (typeof session.payment_intent === 'string') {
      const paymentIntent = await stripe.paymentIntents.retrieve(session.payment_intent as string);
      res.status(200).json({ sessionId: session.id, clientSecret: paymentIntent.client_secret });
    } else {
      // Handle the case where payment_intent is null (e.g., log an error or throw an exception)
      res.status(500).json({ error: 'PaymentIntent is null' });
    }
  } catch (err: unknown) {
    if (err instanceof Error) {
      // Now we check if it's a Stripe error
      if ("statusCode" in err && "message" in err) {
        // Here we're assuming that if the error has statusCode, it's a Stripe error.
        // We cast the error type to any to access the statusCode and message properties.
        res.status((err as any).statusCode || 500).json({ error: err.message });
      } else {
        // If it's not a Stripe error, we return a generic error response
        res.status(500).json({ error: 'An unexpected error occurred' });
      }
    } else {
      // If it's not an Error instance, handle accordingly
      res.status(500).json({ error: 'An unexpected error occurred' });
    }
  }


};

目前无法确定是后端未从Stripe获取到clientSecret,还是传递到前端环节出现问题,请求协助排查,已陷入停滞。


内容的提问来源于stack exchange,提问作者filthysasuke

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 09:15:58