expo-popcore-old/hooks/use-pricing.ts

396 lines
12 KiB
TypeScript

import { useState, useEffect, useRef, useMemo } from 'react';
import { Linking } from 'react-native';
import { useRouter } from 'expo-router';
import { authClient, useSession } from '@/lib/auth/client';
import { getStripePlans, getPlanNames } from '@/lib/api/pricing';
import { Alert } from '@/utils/alert';
import type {
StripePricingTableResponse,
CreditPlan,
} from '@/lib/api/pricing';
export function usePricing() {
const router = useRouter();
const { data: session, isPending: isAuthPending } = useSession();
const [selectedPlanIndex, setSelectedPlanIndex] = useState(0);
// Stripe 套餐数据
const [stripePricingData, setStripePricingData] = useState<StripePricingTableResponse | null>(null);
const [isStripePricingLoading, setIsStripePricingLoading] = useState(true);
const [stripePricingError, setStripePricingError] = useState<string | null>(null);
// 订阅数据
const [authSubscriptions, setAuthSubscriptions] = useState<any[]>([]);
const [isLoadingSubscriptions, setIsLoadingSubscriptions] = useState(false);
const [isFetchingSubscriptions, setIsFetchingSubscriptions] = useState(false);
// Mutation 状态
const [createSubscriptionPending, setCreateSubscriptionPending] = useState(false);
const [upgradeSubscriptionPending, setUpgradeSubscriptionPending] = useState(false);
const [restoreSubscriptionPending, setRestoreSubscriptionPending] = useState(false);
const [rechargeTokenPending, setRechargeTokenPending] = useState(false);
const isInitialized = useRef(false);
// 获取 Stripe 套餐数据
useEffect(() => {
const fetchStripePlans = async () => {
setIsStripePricingLoading(true);
const result = await getStripePlans();
if (result.success && result.data) {
setStripePricingData(result.data as any);
setStripePricingError(null);
} else {
setStripePricingError(result.message || 'Failed to load pricing');
}
setIsStripePricingLoading(false);
};
fetchStripePlans();
}, []);
// 获取订阅数据
const fetchAuthSubscriptions = async () => {
if (!session?.user?.id) return;
setIsFetchingSubscriptions(true);
try {
const { data, error } = await authClient.subscription.list({
query: {
referenceId: session?.user?.id || '',
},
});
if (!error && data) {
setAuthSubscriptions(data);
}
} catch (error) {
console.error('Failed to fetch subscriptions:', error);
} finally {
setIsFetchingSubscriptions(false);
}
};
useEffect(() => {
if (session?.user?.id) {
setIsLoadingSubscriptions(true);
fetchAuthSubscriptions().finally(() => {
setIsLoadingSubscriptions(false);
});
}
}, [session?.user?.id]);
// 订阅类型分类
const licensedSubscriptions = authSubscriptions?.filter((sub: any) => sub.type === 'licenced') || [];
const meteredSubscriptions = authSubscriptions?.filter((sub: any) => sub.type === 'metered') || [];
// 订阅状态计算
const activeAuthSubscription = licensedSubscriptions?.find(
(sub: any) => sub.status === 'active' || sub.status === 'trialing'
);
const hasActiveSubscription = !!(
activeAuthSubscription &&
activeAuthSubscription.cancelAtPeriodEnd === false &&
activeAuthSubscription.status === 'active'
);
const hasCanceledButActiveSubscription = !!(
activeAuthSubscription &&
activeAuthSubscription.status === 'active' &&
activeAuthSubscription.cancelAtPeriodEnd === true
);
// 获取价格计划
const creditPlans = useMemo((): CreditPlan[] => {
if (stripePricingData?.pricing_table_items) {
return stripePricingData.pricing_table_items
.filter((item: any) => item.recurring?.interval === 'month')
.map((item: any) => {
const amountInCents = parseInt(item.amount);
const credits = item.metadata?.grant_token || amountInCents;
return {
name: item.name || 'basic',
recurring: item.recurring,
amountInCents,
credits,
popular: item.is_highlight || item.highlight_text === 'most_popular',
} as CreditPlan;
});
}
return [];
}, [stripePricingData?.pricing_table_items]);
// 当前订阅对应的积分
const currentSubscriptionCredits = useMemo((): number | null => {
if (!activeAuthSubscription?.plan) return null;
const planNameLower = activeAuthSubscription.plan.toLowerCase();
const planNames = getPlanNames(stripePricingData ?? undefined);
const matchingPlan = creditPlans.find((_, index) => {
return planNames[index]?.toLowerCase() === planNameLower;
});
return (matchingPlan as any)?.credits || null;
}, [activeAuthSubscription?.plan, creditPlans, stripePricingData]);
// 积分余额
const creditBalanceData = meteredSubscriptions?.[0]?.creditBalance;
const creditBalance = creditBalanceData?.remainingTokenBalance || 0;
const selectedPlan = creditPlans[selectedPlanIndex] || creditPlans[0];
// 初始化选中的计划
useEffect(() => {
const hasRequiredData = creditPlans.length > 0 && !isStripePricingLoading && !isLoadingSubscriptions;
if (!isInitialized.current && hasRequiredData) {
let targetIndex = Math.floor(creditPlans.length / 2);
if (currentSubscriptionCredits) {
const foundIndex = creditPlans.findIndex(plan => plan.credits?.toString() === currentSubscriptionCredits.toString());
if (foundIndex !== -1) {
targetIndex = foundIndex;
}
}
setSelectedPlanIndex(targetIndex);
isInitialized.current = true;
}
}, [currentSubscriptionCredits, creditPlans, isStripePricingLoading, isLoadingSubscriptions]);
// 创建订阅
const createSubscription = async (priceId: string, productId: string) => {
setCreateSubscriptionPending(true);
try {
const { data, error } = await authClient.subscription.create({
priceId,
productId,
successUrl: 'bestaibest://exchange',
cancelUrl: 'bestaibest://exchange?canceled=true',
});
if (error) {
Alert.alert('Error', error.message || 'Failed to create subscription');
return null;
}
if (data?.url) {
await Linking.openURL(data.url);
}
return data;
} catch (error) {
Alert.alert('Error', 'Failed to create subscription');
return null;
} finally {
setCreateSubscriptionPending(false);
}
};
// 升级订阅
const upgradeSubscription = async (credits: number) => {
if (!activeAuthSubscription?.stripeSubscriptionId) {
Alert.alert('Error', 'No active subscription found');
return null;
}
setUpgradeSubscriptionPending(true);
try {
const planIndex = creditPlans.findIndex(plan => plan.credits === credits);
const planNames = getPlanNames(stripePricingData ?? undefined);
const planName = planNames[planIndex] || 'basic';
const { data, error } = await authClient.subscription.upgrade({
plan: planName,
subscriptionId: activeAuthSubscription.stripeSubscriptionId,
successUrl: 'bestaibest://exchange',
cancelUrl: 'bestaibest://exchange?canceled=true',
});
if (error) {
Alert.alert('Error', error.message || 'Failed to upgrade subscription');
return null;
}
if (data?.url) {
await Linking.openURL(data.url);
}
return data;
} catch (error) {
Alert.alert('Error', 'Failed to upgrade subscription');
return null;
} finally {
setUpgradeSubscriptionPending(false);
}
};
// 恢复订阅
const restoreSubscription = async () => {
if (!activeAuthSubscription) {
Alert.alert('Error', 'No subscription to restore');
return null;
}
setRestoreSubscriptionPending(true);
try {
const { data, error } = await authClient.subscription.restore({
subscriptionId: activeAuthSubscription.stripeSubscriptionId,
referenceId: activeAuthSubscription.referenceId,
});
if (error) {
Alert.alert('Error', error.message || 'Failed to restore subscription');
return null;
}
Alert.alert('Success', 'Subscription restored successfully');
await fetchAuthSubscriptions();
return data;
} catch (error) {
Alert.alert('Error', 'Failed to restore subscription');
return null;
} finally {
setRestoreSubscriptionPending(false);
}
};
// 充值 Token
const rechargeToken = async (amount: number) => {
if (!amount || amount <= 0) {
Alert.alert('Error', 'Invalid recharge amount');
return null;
}
// 检查 pricing data 是否加载完成
if (!stripePricingData?.pricing_table_items?.[0]?.metered_price_id) {
Alert.alert('Error', 'Pricing data not available. Please try again later.');
return null;
}
// 检查用户是否有 metered subscription
if (!meteredSubscriptions || meteredSubscriptions.length === 0 || !meteredSubscriptions[0]?.priceId) {
Alert.alert('Error', 'No active metered subscription found. Please contact support.');
return null;
}
setRechargeTokenPending(true);
try {
const { data, error } = await authClient.subscription.credit.topup({
amount: amount,
priceId: meteredSubscriptions[0].priceId,
successUrl: 'bestaibest://exchange',
cancelUrl: 'bestaibest://exchange?canceled=true',
});
if (error) {
Alert.alert('Error', error.message || 'Failed to recharge');
return null;
}
if (data?.url) {
await Linking.openURL(data.url);
}
return data;
} catch (error) {
Alert.alert('Error', 'Failed to recharge');
return null;
} finally {
setRechargeTokenPending(false);
}
};
// 处理订阅操作
const handleSubscriptionAction = async (planIndexOverride?: number) => {
const targetPlanIndex = planIndexOverride !== undefined ? planIndexOverride : selectedPlanIndex;
const targetPlan = creditPlans[targetPlanIndex];
if (!targetPlan) {
Alert.alert('Error', 'Invalid plan selected');
return;
}
if (isStripePricingLoading) {
Alert.alert('Please wait', 'Loading pricing data...');
return;
}
// 如果选择的是当前订阅
if (hasActiveSubscription && currentSubscriptionCredits === targetPlan.credits) {
return;
}
// 如果是已取消但仍活跃的订阅
if (hasCanceledButActiveSubscription && currentSubscriptionCredits === targetPlan.credits) {
await restoreSubscription();
return;
}
const pricingItem = stripePricingData?.pricing_table_items?.[targetPlanIndex];
if (!pricingItem) {
Alert.alert('Error', 'Plan not found');
return;
}
// 判断创建还是升级
if (hasActiveSubscription || hasCanceledButActiveSubscription) {
await upgradeSubscription(targetPlan.credits!);
} else {
await createSubscription(pricingItem.price_id!, pricingItem.product_id!);
}
};
// 格式化积分
const formatCredits = (credits: number) => {
return credits.toLocaleString();
};
// 综合加载状态
const isDataFullyLoaded = useMemo(() => {
if (isAuthPending) return false;
if (!session) return !isStripePricingLoading;
return !isStripePricingLoading && !isLoadingSubscriptions;
}, [session, isAuthPending, isStripePricingLoading, isLoadingSubscriptions]);
return {
// 状态
selectedPlanIndex,
selectedPlan,
creditPlans,
// 认证状态
session,
isAuthPending,
isDataFullyLoaded,
// 订阅状态
activeAuthSubscription,
hasActiveSubscription,
hasCanceledButActiveSubscription,
currentSubscriptionCredits,
isLoadingSubscriptions,
isFetchingSubscriptions,
meteredSubscriptions,
// Credits 余额
creditBalance,
creditBalanceData,
// Stripe 数据
stripePricingData,
isStripePricingLoading,
stripePricingError,
// 操作方法
setSelectedPlanIndex,
handleSubscriptionAction,
fetchAuthSubscriptions,
rechargeToken,
// 工具方法
formatCredits,
// Mutation 状态
createSubscriptionPending,
upgradeSubscriptionPending,
restoreSubscriptionPending,
rechargeTokenPending,
};
}