知识库首页 模版 use-subscription.ts.md

use subscription.ts

本地来源:模版/模版文档对比说明/06-工具函数/use-subscription.ts.md

订阅钩子 (hooks/use-subscription.ts) 逐行分析

文件概述

这是一个自定义的 React Hook,用于管理用户的订阅状态。它处理订阅的各种状态,包括有效、宽限期、到期等,并提供订阅状态的实时更新。

导入语句分析

"use client";
  • 指示这是一个客户端组件
  • 确保在浏览器环境中执行
import { useEffect, useState } from "react";
  • 导入 React 的 Hook
  • useEffect 用于处理副作用
  • useState 用于管理组件状态
import { createClient } from "@/utils/supabase/client";
  • 导入客户端 Supabase 实例
  • 用于在客户端与 Supabase 交互
import {
  SubscriptionStatus,
  ACTIVE_STATUSES,
  GRACE_PERIOD_STATUSES,
  SubscriptionState,
} from "@/types/subscriptions";
  • 导入订阅相关的类型定义和常量
  • SubscriptionStatus:订阅状态接口
  • ACTIVE_STATUSES:有效状态数组
  • GRACE_PERIOD_STATUSES:宽限期状态数组
  • SubscriptionState:订阅状态枚举

辅助函数分析

订阅活跃状态检查

function isSubscriptionActive(
  status: SubscriptionState,
  endDate: string
): boolean {
  const now = new Date();
  const periodEnd = new Date(endDate);

  if (ACTIVE_STATUSES.includes(status as "active" | "trialing")) {
    return true;
  }

  if (
    GRACE_PERIOD_STATUSES.includes(
      status as "canceled" | "past_due" | "unpaid" | "paused"
    ) &&
    periodEnd > now
  ) {
    return true;
  }

  return false;
}

函数参数

  • status: 订阅状态
  • endDate: 订阅结束日期字符串

逻辑分析

const now = new Date();
const periodEnd = new Date(endDate);
  • 获取当前时间
  • 解析结束日期字符串为 Date 对象
if (ACTIVE_STATUSES.includes(status as "active" | "trialing")) {
  return true;
}
  • 检查是否为直接有效状态
  • 使用类型断言确保类型安全
  • 如果是 active 或 trialing 状态,直接返回 true
if (
  GRACE_PERIOD_STATUSES.includes(
    status as "canceled" | "past_due" | "unpaid" | "paused"
  ) &&
  periodEnd > now
) {
  return true;
}
  • 检查宽限期状态
  • 同时检查结束日期是否还未到期
  • 满足条件则认为订阅仍然有效

宽限期状态检查

function isInGracePeriod(status: SubscriptionState, endDate: string): boolean {
  const now = new Date();
  const periodEnd = new Date(endDate);
  return (
    GRACE_PERIOD_STATUSES.includes(
      status as "canceled" | "past_due" | "unpaid" | "paused"
    ) && periodEnd > now
  );
}
  • 检查订阅是否处于宽限期
  • 状态必须是宽限期状态之一
  • 且结束日期未到期

Hook 定义分析

export function useSubscription() {
  • 定义并导出 useSubscription Hook
  • 遵循 React Hook 命名规范

状态管理

const [subscription, setSubscription] = useState<SubscriptionStatus>({
  isSubscribed: false,
  status: null,
  willEndOn: null,
  isInGracePeriod: false,
  daysLeft: null,
});
  • 定义订阅状态
  • 使用 SubscriptionStatus 接口
  • 初始化为未订阅状态
  • 包含多个订阅相关字段
const [loading, setLoading] = useState(true);
  • 定义加载状态
  • 初始值为 true
const supabase = createClient();
  • 创建 Supabase 客户端实例

副作用处理

useEffect(() => {
  checkSubscription();
}, []);
  • 组件挂载时检查订阅状态
  • 空依赖数组确保只执行一次

订阅检查函数

const checkSubscription = async () => {
  try {
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      setLoading(false);
      return;
    }
    // ... 后续逻辑
  } catch (error) {
    console.error("Error checking subscription:", error);
  } finally {
    setLoading(false);
  }
};

用户认证检查

const {
  data: { user },
} = await supabase.auth.getUser();
if (!user) {
  setLoading(false);
  return;
}
  • 获取当前用户
  • 如果用户未登录,直接返回

数据库查询

const { data: customerData } = await supabase
  .from("customers")
  .select(
    `
    subscriptions (
      status,
      current_period_end,
      canceled_at
    )
  `
  )
  .eq("user_id", user.id)
  .single();
  • 查询用户的订阅信息
  • 使用关联查询获取订阅详情
  • 筛选当前用户的数据
  • 获取单条记录

订阅数据处理

const sub = customerData?.subscriptions?.[0];
if (!sub) {
  setLoading(false);
  return;
}
  • 提取第一个订阅记录
  • 如果没有订阅,直接返回

日期计算

const now = new Date();
const endDate = new Date(sub.current_period_end);
const daysLeft = Math.ceil(
  (endDate.getTime() - now.getTime()) / (1000 * 3600 * 24)
);
  • 计算剩余天数
  • 使用 Math.ceil 向上取整
  • 时间差转换为天数

状态更新

setSubscription({
  isSubscribed: isSubscriptionActive(sub.status, sub.current_period_end),
  status: sub.status,
  willEndOn: endDate,
  isInGracePeriod: isInGracePeriod(sub.status, sub.current_period_end),
  daysLeft: daysLeft,
});
  • 更新订阅状态
  • 使用辅助函数计算各种状态
  • 提供完整的订阅信息

返回值

return { ...subscription, loading, refresh: checkSubscription };
  • 展开订阅状态对象
  • 包含加载状态
  • 提供刷新功能

设计模式分析

状态管理模式

  • 使用单一状态对象管理复杂订阅信息
  • 提供清晰的状态更新逻辑
  • 分离加载状态和业务状态

辅助函数模式

  • 将复杂的业务逻辑提取为独立函数
  • 提高代码可读性和可测试性
  • 便于逻辑复用

错误处理模式

  • 使用 try-catch-finally 处理异步操作
  • 提供错误日志记录
  • 确保状态一致性

数据获取模式

  • 使用 Supabase 的关联查询
  • 一次性获取所有需要的数据
  • 减少网络请求次数

业务逻辑分析

订阅状态管理

  • 区分有效订阅和宽限期订阅
  • 处理各种订阅状态转换
  • 提供准确的到期时间计算

用户体验优化

  • 提供加载状态指示
  • 支持手动刷新订阅状态
  • 实时计算剩余天数

错误处理

  • 静默处理网络错误
  • 防止应用崩溃
  • 提供开发者调试信息

最佳实践总结

  1. 业务逻辑分离:将复杂逻辑提取为独立函数
  2. 状态管理:使用结构化的状态对象
  3. 错误处理:完善的异步错误处理机制
  4. 性能优化:合理使用 useEffect 依赖
  5. 类型安全:使用 TypeScript 确保类型安全
  6. 用户体验:提供加载状态和刷新功能
  7. 代码复用:辅助函数便于测试和复用

使用示例

function SubscriptionBanner() {
  const { isSubscribed, status, daysLeft, loading, refresh } = useSubscription();

  if (loading) return <div>Loading subscription...</div>;

  if (!isSubscribed) {
    return <div>No active subscription</div>;
  }

  return (
    <div>
      <p>Status: {status}</p>
      <p>Days left: {daysLeft}</p>
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

这个 Hook 提供了完整的订阅状态管理功能,是构建订阅相关功能的核心组件。它展示了如何处理复杂的业务逻辑、状态管理和用户体验优化。

本文档为站内渲染。原始文件本地路径:saas/source/templates/模版-模版文档对比说明-06-工具函数-use-subscription-ts-d734f3.md(仅本地保留,不入库不部署)