386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
import { Alert, Linking } from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { authClient, useSession } from '@/lib/auth/client';
|
|
import { getStripePlans, getPlanNames } from '@/lib/api/pricing';
|
|
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);
|
|
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) => item.recurring?.interval === 'month')
|
|
.map((item) => {
|
|
const amountInCents = parseInt(item.amount);
|
|
const credits = item.metadata?.grant_token || amountInCents;
|
|
return {
|
|
amountInCents,
|
|
credits,
|
|
popular: item.is_highlight || item.highlight_text === 'most_popular',
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
}, [stripePricingData?.pricing_table_items]);
|
|
|
|
// 当前订阅对应的积分
|
|
const currentSubscriptionCredits = useMemo((): number | null => {
|
|
if (!activeAuthSubscription?.plan) return null;
|
|
|
|
const planNameLower = activeAuthSubscription.plan.toLowerCase();
|
|
const planNames = getPlanNames(stripePricingData);
|
|
const matchingPlan = creditPlans.find((_, index) => {
|
|
return planNames[index]?.toLowerCase() === planNameLower;
|
|
});
|
|
return matchingPlan?.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);
|
|
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;
|
|
}
|
|
|
|
if (!stripePricingData?.pricing_table_items?.[0]?.metered_price_id) {
|
|
Alert.alert('Error', 'Pricing data not available');
|
|
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,
|
|
};
|
|
}
|