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

如何在Next.js+Redux+Firebase+Stripe中持久化用户专属高级状态

解决方案:将高级订阅状态与用户永久绑定

你的核心问题在于依赖本地存储保存订阅状态,且登出时强制重置状态,但未在登录时从权威数据源同步最新状态。以下是具体的修复和优化方案:

1. 修复现有状态监听Hook的问题

你的usePremiumStatus存在两个明显问题:

  • useEffect依赖数组格式错误,dispatch应该放在依赖数组内,而非第三个参数
  • 未强制刷新ID Token,导致可能拿到缓存的旧Claims

修改后的Hook代码:

import { useEffect } from "react";
import { onIdTokenChanged } from "firebase/auth";
import { auth } from "../firebase";
import { useDispatch, useSelector } from "react-redux";
import { setPremiumStatus } from "@/redux/userSlice";

export default function usePremiumStatus() {
  const dispatch = useDispatch();
  const premiumStatus = useSelector((state) => state.user.premium);

  useEffect(() => {
    // 监听Firebase auth状态变化,无需依赖外部传入的user
    const unsubscribe = onIdTokenChanged(auth, async (user) => {
      if (user) {
        // 强制刷新ID Token,确保拿到最新的自定义Claims(避免缓存)
        await user.getIdToken(true);
        const tokenResult = await user.getIdTokenResult();
        // 从Token Claims中获取订阅状态(这是权威来源)
        const isPremium = tokenResult.claims.stripeRole === "premium";
        dispatch(setPremiumStatus(isPremium));
      } else {
        // 用户登出时重置状态是合理的,此时不应保留用户专属状态
        dispatch(setPremiumStatus(false));
      }
    });

    // 组件卸载时取消监听
    return unsubscribe;
  }, [dispatch]);

  return premiumStatus;
}

2. 确保订阅状态与Stripe同步(核心步骤)

你当前用stripeRole这个Firebase自定义Claims来判断高级状态,这是正确的,但必须保证当用户订阅/取消订阅时,这个Claims会自动更新:

实现Stripe Webhook同步状态

在后端(推荐用Firebase Functions)监听Stripe的订阅事件,自动更新用户的Firebase自定义Claims:

// Firebase Functions + Stripe Webhook 示例
const admin = require("firebase-admin");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
const functions = require("firebase-functions");

exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
  const sig = req.headers["stripe-signature"];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.rawBody,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // 处理订阅成功事件
  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    // 注意:创建Stripe Checkout Session时,需将用户的Firebase UID存入metadata
    const userId = session.metadata.firebaseUid;
    // 设置用户的自定义Claims,标记为高级用户
    await admin.auth().setCustomUserClaims(userId, { stripeRole: "premium" });
  }

  // 处理订阅取消事件
  else if (event.type === "customer.subscription.deleted") {
    const subscription = event.data.object;
    const userId = subscription.metadata.firebaseUid;
    // 移除高级用户标记
    await admin.auth().setCustomUserClaims(userId, { stripeRole: null });
  }

  res.json({ received: true });
});

关键注意点

  • 创建Stripe Checkout Session时,必须将当前用户的Firebase UID存入Stripe的metadata字段,否则Webhook无法关联到具体用户
  • 不要依赖Redux Persist保存premium状态,因为本地存储的状态可能与实际订阅状态不同步(比如用户在其他设备取消订阅),每次登录都从Firebase Token Claims拉取最新状态才是可靠的

3. 调整Redux Persist配置(可选)

如果你仍想保留本地缓存作为优化,可以在Redux Persist中持久化premium状态,但登录时必须用权威来源的状态覆盖本地缓存(也就是上面Hook中强制刷新Token后更新状态的逻辑),避免使用过期的本地数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 08:37:39