-
{currentPlan?.name}
+
+ {currentPlanWithTranslations?.name}
+
{subscription &&
(subscription.status === 'trialing' ||
subscription.status === 'active') && (
diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx
index 639556d..a2ac0fe 100644
--- a/src/components/settings/credits/credit-packages.tsx
+++ b/src/components/settings/credits/credit-packages.tsx
@@ -11,7 +11,8 @@ import {
import { getCreditPackages } from '@/config/credits-config';
import { websiteConfig } from '@/config/website';
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 { cn } from '@/lib/utils';
import { CircleCheckBigIcon, CoinsIcon } from 'lucide-react';
@@ -31,7 +32,9 @@ export function CreditPackages() {
// Get current user and payment info
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
const creditPackages = Object.values(getCreditPackages()).filter(
diff --git a/src/components/settings/credits/credits-balance-card.tsx b/src/components/settings/credits/credits-balance-card.tsx
index 2be18b7..398f0f9 100644
--- a/src/components/settings/credits/credits-balance-card.tsx
+++ b/src/components/settings/credits/credits-balance-card.tsx
@@ -13,8 +13,9 @@ import { Skeleton } from '@/components/ui/skeleton';
import { websiteConfig } from '@/config/website';
import { useCreditBalance, useCreditStats } from '@/hooks/use-credits-query';
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 { authClient } from '@/lib/auth-client';
import { formatDate } from '@/lib/formatter';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
@@ -48,7 +49,9 @@ export default function CreditsBalanceCard() {
} = useCreditBalance();
// 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
const {
diff --git a/src/hooks/use-payment-query.ts b/src/hooks/use-payment-query.ts
new file mode 100644
index 0000000..a721841
--- /dev/null
+++ b/src/hooks/use-payment-query.ts
@@ -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
=> {
+ 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 => {
+ 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,
+ });
+}
diff --git a/src/hooks/use-payment.ts b/src/hooks/use-payment.ts
deleted file mode 100644
index 259c020..0000000
--- a/src/hooks/use-payment.ts
+++ /dev/null
@@ -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,
- };
-}
diff --git a/src/stores/payment-store.ts b/src/stores/payment-store.ts
deleted file mode 100644
index ba8edd8..0000000
--- a/src/stores/payment-store.ts
+++ /dev/null
@@ -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;
- resetState: () => void;
-}
-
-/**
- * Payment store using Zustand
- * Manages the user's payment and subscription data globally
- */
-export const usePaymentStore = create((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,
- });
- },
-}));