refactor: replace usePayment hook and store with useCurrentPlan for improved payment state management
This commit is contained in:
parent
ff1e72df13
commit
ac8d4dee4b
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
|
import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
|
||||||
import { PaymentProvider } from '@/components/layout/payment-provider';
|
|
||||||
import { QueryProvider } from '@/components/providers/query-provider';
|
import { QueryProvider } from '@/components/providers/query-provider';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
@ -63,9 +62,7 @@ export function Providers({ children, locale }: ProvidersProps) {
|
|||||||
>
|
>
|
||||||
<ActiveThemeProvider>
|
<ActiveThemeProvider>
|
||||||
<RootProvider theme={theme} i18n={{ locale, locales, translations }}>
|
<RootProvider theme={theme} i18n={{ locale, locales, translations }}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
<PaymentProvider>{children}</PaymentProvider>
|
|
||||||
</TooltipProvider>
|
|
||||||
</RootProvider>
|
</RootProvider>
|
||||||
</ActiveThemeProvider>
|
</ActiveThemeProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
@ -23,7 +23,6 @@ import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
|
|||||||
import { LOCALES, routing } from '@/i18n/routing';
|
import { LOCALES, routing } from '@/i18n/routing';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { useLocaleStore } from '@/stores/locale-store';
|
import { useLocaleStore } from '@/stores/locale-store';
|
||||||
import { usePaymentStore } from '@/stores/payment-store';
|
|
||||||
import type { User } from 'better-auth';
|
import type { User } from 'better-auth';
|
||||||
import {
|
import {
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
@ -55,7 +54,6 @@ export function SidebarUser({ user, className }: SidebarUserProps) {
|
|||||||
const pathname = useLocalePathname();
|
const pathname = useLocalePathname();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { currentLocale, setCurrentLocale } = useLocaleStore();
|
const { currentLocale, setCurrentLocale } = useLocaleStore();
|
||||||
const { resetState } = usePaymentStore();
|
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@ -81,8 +79,7 @@ export function SidebarUser({ user, className }: SidebarUserProps) {
|
|||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('sign out success');
|
console.log('sign out success');
|
||||||
// Reset payment state on sign out
|
// TanStack Query automatically handles cache invalidation on sign out
|
||||||
resetState();
|
|
||||||
router.replace('/');
|
router.replace('/');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -100,7 +97,7 @@ export function SidebarUser({ user, className }: SidebarUserProps) {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
className="cursor-pointer data-[state=open]:bg-sidebar-accent
|
className="cursor-pointer data-[state=open]:bg-sidebar-accent
|
||||||
data-[state=open]:text-sidebar-accent-foreground"
|
data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
@ -9,8 +9,9 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
import { usePayment } from '@/hooks/use-payment';
|
import { useCurrentPlan } from '@/hooks/use-payment-query';
|
||||||
import { LocaleLink } from '@/i18n/navigation';
|
import { LocaleLink } from '@/i18n/navigation';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import { SparklesIcon } from 'lucide-react';
|
import { SparklesIcon } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@ -23,14 +24,16 @@ export function UpgradeCard() {
|
|||||||
|
|
||||||
const t = useTranslations('Dashboard.upgrade');
|
const t = useTranslations('Dashboard.upgrade');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { isLoading, currentPlan, subscription } = usePayment();
|
const { data: session } = authClient.useSession();
|
||||||
|
const { data: paymentData, isLoading } = useCurrentPlan(session?.user?.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Don't show the upgrade card if the user has a lifetime membership or a subscription
|
// Don't show the upgrade card if the user has a lifetime membership or a subscription
|
||||||
const isMember = currentPlan?.isLifetime || !!subscription;
|
const isMember =
|
||||||
|
paymentData?.currentPlan?.isLifetime || !!paymentData?.subscription;
|
||||||
|
|
||||||
if (!mounted || isLoading || isMember) {
|
if (!mounted || isLoading || isMember) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { authClient } from '@/lib/auth-client';
|
|
||||||
import { usePaymentStore } from '@/stores/payment-store';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment provider component
|
|
||||||
*
|
|
||||||
* This component is responsible for initializing the payment state
|
|
||||||
* by fetching the current user's subscription and payment information when the app loads.
|
|
||||||
*/
|
|
||||||
export function PaymentProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const { fetchPayment } = usePaymentStore();
|
|
||||||
const { data: session } = authClient.useSession();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (session?.user) {
|
|
||||||
fetchPayment(session.user);
|
|
||||||
}
|
|
||||||
}, [session?.user, fetchPayment]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ import {
|
|||||||
import { getAvatarLinks } from '@/config/avatar-config';
|
import { getAvatarLinks } from '@/config/avatar-config';
|
||||||
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
|
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { usePaymentStore } from '@/stores/payment-store';
|
|
||||||
import type { User } from 'better-auth';
|
import type { User } from 'better-auth';
|
||||||
import { LogOutIcon } from 'lucide-react';
|
import { LogOutIcon } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@ -29,8 +28,6 @@ export function UserButtonMobile({ user }: UserButtonProps) {
|
|||||||
const avatarLinks = getAvatarLinks();
|
const avatarLinks = getAvatarLinks();
|
||||||
const localeRouter = useLocaleRouter();
|
const localeRouter = useLocaleRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { resetState } = usePaymentStore();
|
|
||||||
|
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
@ -40,8 +37,7 @@ export function UserButtonMobile({ user }: UserButtonProps) {
|
|||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('sign out success');
|
console.log('sign out success');
|
||||||
// Reset payment state on sign out
|
// TanStack Query automatically handles cache invalidation on sign out
|
||||||
resetState();
|
|
||||||
localeRouter.replace('/');
|
localeRouter.replace('/');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -64,7 +60,7 @@ export function UserButtonMobile({ user }: UserButtonProps) {
|
|||||||
<DrawerPortal>
|
<DrawerPortal>
|
||||||
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
|
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
|
||||||
<DrawerContent
|
<DrawerContent
|
||||||
className="fixed inset-x-0 bottom-0 z-50 mt-24
|
className="fixed inset-x-0 bottom-0 z-50 mt-24
|
||||||
overflow-hidden rounded-t-[10px] border bg-background px-3 text-sm"
|
overflow-hidden rounded-t-[10px] border bg-background px-3 text-sm"
|
||||||
>
|
>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
|
@ -12,7 +12,6 @@ import { getAvatarLinks } from '@/config/avatar-config';
|
|||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
import { useLocaleRouter } from '@/i18n/navigation';
|
import { useLocaleRouter } from '@/i18n/navigation';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { usePaymentStore } from '@/stores/payment-store';
|
|
||||||
import type { User } from 'better-auth';
|
import type { User } from 'better-auth';
|
||||||
import { LogOutIcon } from 'lucide-react';
|
import { LogOutIcon } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@ -29,15 +28,12 @@ export function UserButton({ user }: UserButtonProps) {
|
|||||||
const avatarLinks = getAvatarLinks();
|
const avatarLinks = getAvatarLinks();
|
||||||
const localeRouter = useLocaleRouter();
|
const localeRouter = useLocaleRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { resetState } = usePaymentStore();
|
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
await authClient.signOut({
|
await authClient.signOut({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
console.log('sign out success');
|
console.log('sign out success');
|
||||||
// Reset payment state on sign out
|
// TanStack Query automatically handles cache invalidation on sign out
|
||||||
resetState();
|
|
||||||
localeRouter.replace('/');
|
localeRouter.replace('/');
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { getPricePlans } from '@/config/price-config';
|
import { getPricePlans } from '@/config/price-config';
|
||||||
import { useMounted } from '@/hooks/use-mounted';
|
import { useMounted } from '@/hooks/use-mounted';
|
||||||
import { usePayment } from '@/hooks/use-payment';
|
import { useCurrentPlan } from '@/hooks/use-payment-query';
|
||||||
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
|
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { formatDate } from '@/lib/formatter';
|
import { formatDate } from '@/lib/formatter';
|
||||||
@ -33,34 +33,37 @@ export default function BillingCard() {
|
|||||||
const hasHandledSession = useRef(false);
|
const hasHandledSession = useRef(false);
|
||||||
const mounted = useMounted();
|
const mounted = useMounted();
|
||||||
|
|
||||||
const {
|
|
||||||
isLoading: isLoadingPayment,
|
|
||||||
error: loadPaymentError,
|
|
||||||
subscription,
|
|
||||||
currentPlan: currentPlanFromStore,
|
|
||||||
fetchPayment,
|
|
||||||
} = usePayment();
|
|
||||||
|
|
||||||
// Get user session for customer ID
|
// Get user session for customer ID
|
||||||
const { data: session, isPending: isLoadingSession } =
|
const { data: session, isPending: isLoadingSession } =
|
||||||
authClient.useSession();
|
authClient.useSession();
|
||||||
const currentUser = session?.user;
|
const currentUser = session?.user;
|
||||||
|
|
||||||
|
// TanStack Query hook for current plan and subscription
|
||||||
|
const {
|
||||||
|
data: paymentData,
|
||||||
|
isLoading: isLoadingPayment,
|
||||||
|
error: loadPaymentError,
|
||||||
|
refetch: refetchPayment,
|
||||||
|
} = useCurrentPlan(currentUser?.id);
|
||||||
|
|
||||||
|
const currentPlan = paymentData?.currentPlan;
|
||||||
|
const subscription = paymentData?.subscription;
|
||||||
|
|
||||||
// Get price plans with translations - must be called here to maintain hook order
|
// Get price plans with translations - must be called here to maintain hook order
|
||||||
const pricePlans = getPricePlans();
|
const pricePlans = getPricePlans();
|
||||||
const plans = Object.values(pricePlans);
|
const plans = Object.values(pricePlans);
|
||||||
|
|
||||||
// Convert current plan from store to a plan with translations
|
// Convert current plan to a plan with translations
|
||||||
const currentPlan = currentPlanFromStore
|
const currentPlanWithTranslations = currentPlan
|
||||||
? plans.find((plan) => plan.id === currentPlanFromStore?.id)
|
? plans.find((plan) => plan.id === currentPlan?.id)
|
||||||
: null;
|
: null;
|
||||||
const isFreePlan = currentPlan?.isFree || false;
|
const isFreePlan = currentPlanWithTranslations?.isFree || false;
|
||||||
const isLifetimeMember = currentPlan?.isLifetime || false;
|
const isLifetimeMember = currentPlanWithTranslations?.isLifetime || false;
|
||||||
|
|
||||||
// Get subscription price details
|
// Get subscription price details
|
||||||
const currentPrice =
|
const currentPrice =
|
||||||
subscription &&
|
subscription &&
|
||||||
currentPlan?.prices.find(
|
currentPlanWithTranslations?.prices.find(
|
||||||
(price) => price.priceId === subscription?.priceId
|
(price) => price.priceId === subscription?.priceId
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -77,8 +80,8 @@ export default function BillingCard() {
|
|||||||
// Retry payment data fetching
|
// Retry payment data fetching
|
||||||
const handleRetry = useCallback(() => {
|
const handleRetry = useCallback(() => {
|
||||||
// console.log('handleRetry, refetch payment info');
|
// console.log('handleRetry, refetch payment info');
|
||||||
fetchPayment(true);
|
refetchPayment();
|
||||||
}, [fetchPayment]);
|
}, [refetchPayment]);
|
||||||
|
|
||||||
// Check for payment success and show success message
|
// Check for payment success and show success message
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -132,7 +135,9 @@ export default function BillingCard() {
|
|||||||
<CardDescription>{t('currentPlan.description')}</CardDescription>
|
<CardDescription>{t('currentPlan.description')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 flex-1">
|
<CardContent className="space-y-4 flex-1">
|
||||||
<div className="text-destructive text-sm">{loadPaymentError}</div>
|
<div className="text-destructive text-sm">
|
||||||
|
{loadPaymentError?.message}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none">
|
<CardFooter className="mt-2 px-6 py-4 flex justify-end items-center bg-background rounded-none">
|
||||||
<Button
|
<Button
|
||||||
@ -149,7 +154,7 @@ export default function BillingCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// currentPlan maybe null, so we need to check if it is null
|
// currentPlan maybe null, so we need to check if it is null
|
||||||
if (!currentPlan) {
|
if (!currentPlanWithTranslations) {
|
||||||
return (
|
return (
|
||||||
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
|
<Card className={cn('w-full overflow-hidden pt-6 pb-0 flex flex-col')}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -187,7 +192,9 @@ export default function BillingCard() {
|
|||||||
<CardContent className="space-y-4 flex-1">
|
<CardContent className="space-y-4 flex-1">
|
||||||
{/* Plan name and status */}
|
{/* Plan name and status */}
|
||||||
<div className="flex items-center justify-start space-x-4">
|
<div className="flex items-center justify-start space-x-4">
|
||||||
<div className="text-3xl font-medium">{currentPlan?.name}</div>
|
<div className="text-3xl font-medium">
|
||||||
|
{currentPlanWithTranslations?.name}
|
||||||
|
</div>
|
||||||
{subscription &&
|
{subscription &&
|
||||||
(subscription.status === 'trialing' ||
|
(subscription.status === 'trialing' ||
|
||||||
subscription.status === 'active') && (
|
subscription.status === 'active') && (
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
import { getCreditPackages } from '@/config/credits-config';
|
import { getCreditPackages } from '@/config/credits-config';
|
||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
import { useCurrentUser } from '@/hooks/use-current-user';
|
import { useCurrentUser } from '@/hooks/use-current-user';
|
||||||
import { usePayment } from '@/hooks/use-payment';
|
import { useCurrentPlan } from '@/hooks/use-payment-query';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { formatPrice } from '@/lib/formatter';
|
import { formatPrice } from '@/lib/formatter';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { CircleCheckBigIcon, CoinsIcon } from 'lucide-react';
|
import { CircleCheckBigIcon, CoinsIcon } from 'lucide-react';
|
||||||
@ -31,7 +32,9 @@ export function CreditPackages() {
|
|||||||
|
|
||||||
// Get current user and payment info
|
// Get current user and payment info
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const { currentPlan } = usePayment();
|
const { data: session } = authClient.useSession();
|
||||||
|
const { data: paymentData } = useCurrentPlan(session?.user?.id);
|
||||||
|
const currentPlan = paymentData?.currentPlan;
|
||||||
|
|
||||||
// Get credit packages with translations - must be called here to maintain hook order
|
// Get credit packages with translations - must be called here to maintain hook order
|
||||||
const creditPackages = Object.values(getCreditPackages()).filter(
|
const creditPackages = Object.values(getCreditPackages()).filter(
|
||||||
|
@ -13,8 +13,9 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||||||
import { websiteConfig } from '@/config/website';
|
import { websiteConfig } from '@/config/website';
|
||||||
import { useCreditBalance, useCreditStats } from '@/hooks/use-credits-query';
|
import { useCreditBalance, useCreditStats } from '@/hooks/use-credits-query';
|
||||||
import { useMounted } from '@/hooks/use-mounted';
|
import { useMounted } from '@/hooks/use-mounted';
|
||||||
import { usePayment } from '@/hooks/use-payment';
|
import { useCurrentPlan } from '@/hooks/use-payment-query';
|
||||||
import { useLocaleRouter } from '@/i18n/navigation';
|
import { useLocaleRouter } from '@/i18n/navigation';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { formatDate } from '@/lib/formatter';
|
import { formatDate } from '@/lib/formatter';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
@ -48,7 +49,9 @@ export default function CreditsBalanceCard() {
|
|||||||
} = useCreditBalance();
|
} = useCreditBalance();
|
||||||
|
|
||||||
// Get payment info to check plan type
|
// Get payment info to check plan type
|
||||||
const { currentPlan } = usePayment();
|
const { data: session } = authClient.useSession();
|
||||||
|
const { data: paymentData } = useCurrentPlan(session?.user?.id);
|
||||||
|
const currentPlan = paymentData?.currentPlan;
|
||||||
|
|
||||||
// TanStack Query hook for credit statistics
|
// TanStack Query hook for credit statistics
|
||||||
const {
|
const {
|
||||||
|
107
src/hooks/use-payment-query.ts
Normal file
107
src/hooks/use-payment-query.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
|
||||||
|
import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
|
||||||
|
import { getAllPricePlans } from '@/lib/price-plan';
|
||||||
|
import type { PricePlan, Subscription } from '@/payment/types';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// Query keys
|
||||||
|
export const paymentKeys = {
|
||||||
|
all: ['payment'] as const,
|
||||||
|
subscription: (userId: string) =>
|
||||||
|
[...paymentKeys.all, 'subscription', userId] as const,
|
||||||
|
lifetime: (userId: string) =>
|
||||||
|
[...paymentKeys.all, 'lifetime', userId] as const,
|
||||||
|
currentPlan: (userId: string) =>
|
||||||
|
[...paymentKeys.all, 'currentPlan', userId] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to fetch active subscription
|
||||||
|
export function useActiveSubscription(userId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: paymentKeys.subscription(userId || ''),
|
||||||
|
queryFn: async (): Promise<Subscription | null> => {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('User ID is required');
|
||||||
|
}
|
||||||
|
const result = await getActiveSubscriptionAction({ userId });
|
||||||
|
if (!result?.data?.success) {
|
||||||
|
throw new Error(result?.data?.error || 'Failed to fetch subscription');
|
||||||
|
}
|
||||||
|
return result.data.data || null;
|
||||||
|
},
|
||||||
|
enabled: !!userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to fetch lifetime status
|
||||||
|
export function useLifetimeStatus(userId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: paymentKeys.lifetime(userId || ''),
|
||||||
|
queryFn: async (): Promise<boolean> => {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('User ID is required');
|
||||||
|
}
|
||||||
|
const result = await getLifetimeStatusAction({ userId });
|
||||||
|
if (!result?.data?.success) {
|
||||||
|
throw new Error(
|
||||||
|
result?.data?.error || 'Failed to fetch lifetime status'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result.data.isLifetimeMember || false;
|
||||||
|
},
|
||||||
|
enabled: !!userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to get current plan based on subscription and lifetime status
|
||||||
|
export function useCurrentPlan(userId: string | undefined) {
|
||||||
|
const {
|
||||||
|
data: subscription,
|
||||||
|
isLoading: isLoadingSubscription,
|
||||||
|
error: subscriptionError,
|
||||||
|
} = useActiveSubscription(userId);
|
||||||
|
const {
|
||||||
|
data: isLifetimeMember,
|
||||||
|
isLoading: isLoadingLifetime,
|
||||||
|
error: lifetimeError,
|
||||||
|
} = useLifetimeStatus(userId);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: paymentKeys.currentPlan(userId || ''),
|
||||||
|
queryFn: async (): Promise<{
|
||||||
|
currentPlan: PricePlan | null;
|
||||||
|
subscription: Subscription | null;
|
||||||
|
}> => {
|
||||||
|
const plans: PricePlan[] = getAllPricePlans();
|
||||||
|
const freePlan = plans.find((plan) => plan.isFree);
|
||||||
|
const lifetimePlan = plans.find((plan) => plan.isLifetime);
|
||||||
|
|
||||||
|
// If lifetime member, return lifetime plan
|
||||||
|
if (isLifetimeMember) {
|
||||||
|
return {
|
||||||
|
currentPlan: lifetimePlan || null,
|
||||||
|
subscription: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If has active subscription, find the corresponding plan
|
||||||
|
if (subscription) {
|
||||||
|
const plan =
|
||||||
|
plans.find((p) =>
|
||||||
|
p.prices.find((price) => price.priceId === subscription.priceId)
|
||||||
|
) || null;
|
||||||
|
return {
|
||||||
|
currentPlan: plan,
|
||||||
|
subscription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to free plan
|
||||||
|
return {
|
||||||
|
currentPlan: freePlan || null,
|
||||||
|
subscription: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enabled: !!userId && !isLoadingSubscription && !isLoadingLifetime,
|
||||||
|
});
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
import { authClient } from '@/lib/auth-client';
|
|
||||||
import { usePaymentStore } from '@/stores/payment-store';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for accessing and managing payment state
|
|
||||||
*
|
|
||||||
* This hook provides access to the payment state and methods to manage it.
|
|
||||||
* It also automatically fetches payment information when the user changes.
|
|
||||||
*/
|
|
||||||
export function usePayment() {
|
|
||||||
const {
|
|
||||||
currentPlan,
|
|
||||||
subscription,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
fetchPayment: fetchPaymentFromStore,
|
|
||||||
} = usePaymentStore();
|
|
||||||
|
|
||||||
const { data: session } = authClient.useSession();
|
|
||||||
|
|
||||||
const fetchPayment = useCallback(
|
|
||||||
(force = false) => {
|
|
||||||
const currentUser = session?.user;
|
|
||||||
if (currentUser) {
|
|
||||||
fetchPaymentFromStore(currentUser, force);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[session?.user, fetchPaymentFromStore]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentUser = session?.user;
|
|
||||||
if (currentUser) {
|
|
||||||
fetchPaymentFromStore(currentUser);
|
|
||||||
}
|
|
||||||
}, [session?.user, fetchPaymentFromStore]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
currentPlan,
|
|
||||||
subscription,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
fetchPayment,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
|
|
||||||
import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
|
|
||||||
import type { Session } from '@/lib/auth-types';
|
|
||||||
import { getAllPricePlans } from '@/lib/price-plan';
|
|
||||||
import type { PricePlan, Subscription } from '@/payment/types';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
// Cache duration: 2 minutes (optimized for better UX)
|
|
||||||
const CACHE_DURATION = 2 * 60 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment state interface
|
|
||||||
*/
|
|
||||||
export interface PaymentState {
|
|
||||||
// Current plan
|
|
||||||
currentPlan: PricePlan | null;
|
|
||||||
// Active subscription
|
|
||||||
subscription: Subscription | null;
|
|
||||||
// Loading state
|
|
||||||
isLoading: boolean;
|
|
||||||
// Error state
|
|
||||||
error: string | null;
|
|
||||||
// Last fetch timestamp to avoid frequent requests
|
|
||||||
lastFetchTime: number | null;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
fetchPayment: (
|
|
||||||
user: Session['user'] | null | undefined,
|
|
||||||
force?: boolean
|
|
||||||
) => Promise<void>;
|
|
||||||
resetState: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment store using Zustand
|
|
||||||
* Manages the user's payment and subscription data globally
|
|
||||||
*/
|
|
||||||
export const usePaymentStore = create<PaymentState>((set, get) => ({
|
|
||||||
// Initial state
|
|
||||||
currentPlan: null,
|
|
||||||
subscription: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch payment and subscription data for the current user
|
|
||||||
* @param user Current user from auth session
|
|
||||||
*/
|
|
||||||
fetchPayment: async (user, force = false) => {
|
|
||||||
// Skip if already loading
|
|
||||||
if (get().isLoading) return;
|
|
||||||
|
|
||||||
// Skip if no user is provided
|
|
||||||
if (!user) {
|
|
||||||
set({
|
|
||||||
currentPlan: null,
|
|
||||||
subscription: null,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have recent data (within cache duration) unless force refresh
|
|
||||||
if (!force) {
|
|
||||||
const { lastFetchTime } = get();
|
|
||||||
const now = Date.now();
|
|
||||||
if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) {
|
|
||||||
console.log('fetchPayment, use cached data');
|
|
||||||
return; // Use cached data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch subscription data
|
|
||||||
set({ isLoading: true, error: null });
|
|
||||||
|
|
||||||
// Get all price plans
|
|
||||||
const plans: PricePlan[] = getAllPricePlans();
|
|
||||||
const freePlan = plans.find((plan) => plan.isFree);
|
|
||||||
const lifetimePlan = plans.find((plan) => plan.isLifetime);
|
|
||||||
|
|
||||||
// Check if user is a lifetime member directly from the database
|
|
||||||
let isLifetimeMember = false;
|
|
||||||
try {
|
|
||||||
const result = await getLifetimeStatusAction({ userId: user.id });
|
|
||||||
if (result?.data?.success) {
|
|
||||||
isLifetimeMember = result.data.isLifetimeMember || false;
|
|
||||||
console.log('fetchPayment, lifetime status', isLifetimeMember);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
'fetchPayment, lifetime status error',
|
|
||||||
result?.data?.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('fetchPayment, lifetime status error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If lifetime member, set the lifetime plan
|
|
||||||
if (isLifetimeMember) {
|
|
||||||
console.log('fetchPayment, set lifetime plan');
|
|
||||||
set({
|
|
||||||
currentPlan: lifetimePlan || null,
|
|
||||||
subscription: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: Date.now(),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if user has an active subscription
|
|
||||||
const result = await getActiveSubscriptionAction({ userId: user.id });
|
|
||||||
if (result?.data?.success) {
|
|
||||||
const activeSubscription = result.data.data;
|
|
||||||
|
|
||||||
// Set subscription state
|
|
||||||
if (activeSubscription) {
|
|
||||||
const plan =
|
|
||||||
plans.find((p) =>
|
|
||||||
p.prices.find(
|
|
||||||
(price) => price.priceId === activeSubscription.priceId
|
|
||||||
)
|
|
||||||
) || null;
|
|
||||||
console.log('fetchPayment, subscription found, set pro plan');
|
|
||||||
set({
|
|
||||||
currentPlan: plan,
|
|
||||||
subscription: activeSubscription,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: Date.now(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// No subscription found - set to free plan
|
|
||||||
console.log('fetchPayment, no subscription found, set free plan');
|
|
||||||
set({
|
|
||||||
currentPlan: freePlan || null,
|
|
||||||
subscription: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Failed to fetch subscription
|
|
||||||
console.error(
|
|
||||||
'fetchPayment, subscription for user failed',
|
|
||||||
result?.data?.error
|
|
||||||
);
|
|
||||||
set({
|
|
||||||
error: result?.data?.error || 'Failed to fetch payment data',
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('fetchPayment, error:', error);
|
|
||||||
set({
|
|
||||||
error: 'Failed to fetch payment data',
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
set({ isLoading: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset payment state
|
|
||||||
*/
|
|
||||||
resetState: () => {
|
|
||||||
set({
|
|
||||||
currentPlan: null,
|
|
||||||
subscription: null,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastFetchTime: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
|
Loading…
Reference in New Issue
Block a user