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

使用Stripe+React JS搭建订阅系统时遇'retrieve'未定义错误求助

解决Stripe + React订阅系统错误:"Cannot read properties of undefined (reading 'retrieve')"

错误原因

  1. 前端Stripe.js不支持客户检索接口:stripe.customers.retrieve是Stripe后端API专属方法,前端@stripe/stripe-js库未暴露该接口,直接调用会导致stripe.customers为undefined,触发报错。
  2. 结账模式配置错误:代码中将mode设为payment(单次付款模式),但订阅系统需使用subscription模式。
  3. 会话获取方式错误:stripe.redirectToCheckout.getSession()用法不符合Stripe前端SDK规范,无法直接通过该方式获取会话信息。

解决方案

1. 后端实现客户与订阅查询接口

所有涉及客户、订阅的敏感操作必须放在后端,使用Stripe服务端SDK(如stripe-node)封装接口,前端通过API调用获取数据。

示例Node.js后端代码:

const stripe = require('stripe')('YOUR_SECRET_KEY');
const express = require('express');
const app = express();
app.use(express.json());

// 获取客户订阅状态
app.get('/api/subscription-status/:customerId', async (req, res) => {
  try {
    const subscriptions = await stripe.subscriptions.list({
      customer: req.params.customerId,
      status: 'active'
    });
    
    res.json({
      hasActiveSubscription: subscriptions.data.length > 0
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// 创建订阅结账会话
app.post('/api/create-subscription-session', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price: 'YOUR_PRICE_ID',
        quantity: 1,
      },
    ],
    mode: 'subscription',
    success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${req.headers.origin}/cancel`,
  });

  res.json({ id: session.id });
});

app.listen(3001, () => console.log('服务运行在端口3001'));

2. 修正前端代码

移除前端直接调用stripe.customers.retrieve的逻辑,改为调用后端接口;修正结账模式,通过后端创建会话再跳转结账。

修正后的React组件代码:

import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';

let stripePromise;

const getStripe = () => {
  if (!stripePromise) {
    stripePromise = loadStripe('YOUR_PUBLISHABLE_KEY');
  }
  return stripePromise;
};

const Checkout = () => {
  const [stripeError, setStripeError] = useState(null);
  const [isLoading, setLoading] = useState(false);
  const [subscriptionStatus, setSubscriptionStatus] = useState(null);
  const [customerId, setCustomerId] = useState(null);

  // 结账成功后从URL获取会话ID并提取客户ID
  useEffect(() => {
    const urlParams = new URLSearchParams(window.location.search);
    const sessionId = urlParams.get('session_id');
    
    if (sessionId) {
      const fetchCustomerFromSession = async () => {
        const stripe = await getStripe();
        const session = await stripe.checkout.sessions.retrieve(sessionId);
        setCustomerId(session.customer);
      };
      fetchCustomerFromSession();
    }
  }, []);

  // 调用后端接口获取订阅状态
  useEffect(() => {
    if (!customerId) return;
    
    const fetchSubscriptionStatus = async () => {
      try {
        const response = await fetch(`/api/subscription-status/${customerId}`);
        const data = await response.json();
        setSubscriptionStatus(data.hasActiveSubscription ? 'active' : 'inactive');
      } catch (error) {
        setStripeError('获取订阅状态失败');
      }
    };

    fetchSubscriptionStatus();
  }, [customerId]);

  // 调用后端创建会话后跳转结账
  const redirectToCheckout = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/create-subscription-session', { method: 'POST' });
      const { id: sessionId } = await response.json();
      
      const stripe = await getStripe();
      const { error } = await stripe.redirectToCheckout({ sessionId });
      
      if (error) setStripeError(error.message);
    } catch (error) {
      setStripeError('创建结账会话失败');
    } finally {
      setLoading(false);
    }
  };

  if (stripeError) alert(stripeError);

  return (
    <div className="checkout">
      <h1>Stripe 订阅结账</h1>
      <p className="checkout-title">Design+Code React Hooks 课程</p>
      <p className="checkout-description">学习如何用React Hooks搭建网站</p>
      <h1 className="checkout-price">$19/月</h1>
      {subscriptionStatus === 'active' ? (
        <p>您已订阅该课程</p>
      ) : (
        <button className="checkout-button" onClick={redirectToCheckout} disabled={isLoading}>
          <div className="grey-circle">
            <div className="purple-circle"></div>
          </div>
          <div className="text-container">
            <p className="text">{isLoading ? '加载中...' : '订阅'}</p>
          </div>
        </button>
      )}
    </div>
  );
};

export default Checkout;

关键注意事项

  • 密钥区分:前端使用可发布密钥(publishable key),后端使用密钥(secret key),绝对不能在前端暴露密钥。
  • 订阅产品配置:确保Stripe后台的Price ID为订阅类型(recurring),而非单次购买产品。
  • Webhook验证:结账成功后,建议通过Stripe Webhook在后端验证会话状态,避免前端数据篡改风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 02:05:36