use subscription.ts
本地来源:模版/template/模版文档对比说明/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 的关联查询
- 一次性获取所有需要的数据
- 减少网络请求次数
业务逻辑分析
订阅状态管理
- 区分有效订阅和宽限期订阅
- 处理各种订阅状态转换
- 提供准确的到期时间计算
用户体验优化
- 提供加载状态指示
- 支持手动刷新订阅状态
- 实时计算剩余天数
错误处理
- 静默处理网络错误
- 防止应用崩溃
- 提供开发者调试信息
最佳实践总结
- 业务逻辑分离:将复杂逻辑提取为独立函数
- 状态管理:使用结构化的状态对象
- 错误处理:完善的异步错误处理机制
- 性能优化:合理使用 useEffect 依赖
- 类型安全:使用 TypeScript 确保类型安全
- 用户体验:提供加载状态和刷新功能
- 代码复用:辅助函数便于测试和复用
使用示例
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/模版-template-模版文档对比说明-06-工具函数-use-subscription-ts-c189fc.md(仅本地保留,不入库不部署)