From b27d8cc50594585a43ef315380808c9bb119d8b0 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sun, 13 Jul 2025 15:15:44 +0800 Subject: [PATCH] refactor: add retry functionality for payment and credits data fetching with improved user experience --- messages/en.json | 1 + messages/zh.json | 1 + .../settings/billing/billing-card.tsx | 15 ++- .../settings/billing/credits-balance-card.tsx | 51 +++++--- src/hooks/use-credits.ts | 5 - src/hooks/use-payment.ts | 4 +- src/stores/credits-store.ts | 110 ++++++------------ src/stores/payment-store.ts | 61 ++++++---- 8 files changed, 117 insertions(+), 131 deletions(-) diff --git a/messages/en.json b/messages/en.json index 5b9d562..baa595b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -594,6 +594,7 @@ "creditsExpired": "Credits expired", "creditsAdded": "Credits have been added to your account", "viewTransactions": "View Credit Transactions", + "retry": "Retry", "subscriptionCredits": "{credits} credits from subscription this month", "lifetimeCredits": "{credits} credits from lifetime plan this month", "expiringCredits": "{credits} credits expiring on {date}" diff --git a/messages/zh.json b/messages/zh.json index 73a7acc..fb75177 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -594,6 +594,7 @@ "creditsExpired": "积分已过期", "creditsAdded": "积分已添加到您的账户", "viewTransactions": "查看积分记录", + "retry": "重试", "subscriptionCredits": "本月订阅获得 {credits} 积分", "lifetimeCredits": "本月终身会员获得 {credits} 积分", "expiringCredits": "{credits} 积分将在 {date} 过期" diff --git a/src/components/settings/billing/billing-card.tsx b/src/components/settings/billing/billing-card.tsx index 21bb902..ab33b80 100644 --- a/src/components/settings/billing/billing-card.tsx +++ b/src/components/settings/billing/billing-card.tsx @@ -16,14 +16,13 @@ import { getPricePlans } from '@/config/price-config'; import { usePayment } from '@/hooks/use-payment'; import { LocaleLink, useLocaleRouter } from '@/i18n/navigation'; import { authClient } from '@/lib/auth-client'; -import { formatDate, formatPrice } from '@/lib/formatter'; +import { formatDate } from '@/lib/formatter'; import { cn } from '@/lib/utils'; -import { PlanIntervals } from '@/payment/types'; import { Routes } from '@/routes'; import { CheckCircleIcon, ClockIcon, RefreshCwIcon } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { useSearchParams } from 'next/navigation'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { toast } from 'sonner'; export default function BillingCard() { @@ -37,7 +36,7 @@ export default function BillingCard() { error: loadPaymentError, subscription, currentPlan: currentPlanFromStore, - refetch, + fetchPayment, } = usePayment(); // Get user session for customer ID @@ -77,6 +76,12 @@ export default function BillingCard() { const isPageLoading = isLoadingPayment || isLoadingSession; // console.log('billing card, isLoadingPayment', isLoadingPayment, 'isLoadingSession', isLoadingSession); + // Retry payment data fetching + const handleRetry = useCallback(() => { + // console.log('handleRetry, refetch payment info'); + fetchPayment(true); + }, [fetchPayment]); + // Check for payment success and show success message useEffect(() => { const sessionId = searchParams.get('session_id'); @@ -133,7 +138,7 @@ export default function BillingCard() { diff --git a/src/hooks/use-credits.ts b/src/hooks/use-credits.ts index c2e822a..f3837c6 100644 --- a/src/hooks/use-credits.ts +++ b/src/hooks/use-credits.ts @@ -23,10 +23,6 @@ export function useCredits() { (force = false) => { const currentUser = session?.user; if (currentUser) { - console.log( - `${force ? 'force fetch' : 'fetch'} credits for user`, - currentUser.id - ); fetchCreditsFromStore(currentUser, force); } }, @@ -36,7 +32,6 @@ export function useCredits() { useEffect(() => { const currentUser = session?.user; if (currentUser) { - console.log('fetch credits info for user', currentUser.id); fetchCreditsFromStore(currentUser); } }, [session?.user, fetchCreditsFromStore]); diff --git a/src/hooks/use-payment.ts b/src/hooks/use-payment.ts index cd8983b..259c020 100644 --- a/src/hooks/use-payment.ts +++ b/src/hooks/use-payment.ts @@ -23,8 +23,7 @@ export function usePayment() { (force = false) => { const currentUser = session?.user; if (currentUser) { - console.log('fetch payment info for user', currentUser.id); - fetchPaymentFromStore(currentUser); + fetchPaymentFromStore(currentUser, force); } }, [session?.user, fetchPaymentFromStore] @@ -33,7 +32,6 @@ export function usePayment() { useEffect(() => { const currentUser = session?.user; if (currentUser) { - console.log('fetch payment info for user', currentUser.id); fetchPaymentFromStore(currentUser); } }, [session?.user, fetchPaymentFromStore]); diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts index 2723b5f..425f224 100644 --- a/src/stores/credits-store.ts +++ b/src/stores/credits-store.ts @@ -3,6 +3,9 @@ import { getCreditBalanceAction } from '@/actions/get-credit-balance'; import type { Session } from '@/lib/auth-types'; import { create } from 'zustand'; +// Cache duration: 30 seconds +const CACHE_DURATION = 30 * 1000; + /** * Credits state interface */ @@ -17,15 +20,13 @@ export interface CreditsState { lastFetchTime: number | null; // Actions - fetchCredits: (user: Session['user'] | null | undefined) => Promise; + fetchCredits: ( + user: Session['user'] | null | undefined, + force?: boolean + ) => Promise; consumeCredits: (amount: number, description: string) => Promise; - refreshCredits: (user: Session['user'] | null | undefined) => Promise; - resetCreditsState: () => void; } -// Cache duration: 30 seconds -const CACHE_DURATION = 30 * 1000; - /** * Credits store using Zustand * Manages the user's credit balance globally with caching and optimistic updates @@ -38,10 +39,11 @@ export const useCreditsStore = create((set, get) => ({ lastFetchTime: null, /** - * Fetch credit balance for the current user with caching + * Fetch credit balance for the current user with optional cache bypass * @param user Current user from auth session + * @param force Whether to force refresh and ignore cache */ - fetchCredits: async (user) => { + fetchCredits: async (user, force = false) => { // Skip if already loading if (get().isLoading) return; @@ -55,33 +57,43 @@ export const useCreditsStore = create((set, get) => ({ return; } - // Check if we have recent data (within cache duration) - const { lastFetchTime } = get(); - const now = Date.now(); - if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) { - return; // Use cached data + // 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) { + return; // Use cached data + } } - set({ isLoading: true, error: null }); + console.log(`fetchCredits, ${force ? 'force fetch' : 'fetch'} credits`); + set({ + isLoading: true, + error: null, + // Clear cache if force refresh + lastFetchTime: force ? null : get().lastFetchTime, + }); try { const result = await getCreditBalanceAction(); - if (result?.data?.success) { + const newBalance = result.data.credits || 0; + console.log('fetchCredits, set new balance', newBalance); set({ - balance: result.data.credits || 0, + balance: newBalance, isLoading: false, error: null, - lastFetchTime: now, + lastFetchTime: Date.now(), }); } else { + console.warn('fetchCredits, failed to fetch credit balance', result); set({ error: result?.data?.error || 'Failed to fetch credit balance', isLoading: false, }); } } catch (error) { - console.error('fetch credits error:', error); + console.error('fetchCredits, error:', error); set({ error: 'Failed to fetch credit balance', isLoading: false, @@ -100,8 +112,9 @@ export const useCreditsStore = create((set, get) => ({ // Check if we have enough credits if (balance < amount) { + console.log('consumeCredits, insufficient credits', balance, amount); set({ - error: `Insufficient credits. You need ${amount} credits but only have ${balance}.`, + error: 'Insufficient credits', }); return false; } @@ -128,6 +141,7 @@ export const useCreditsStore = create((set, get) => ({ } // Revert optimistic update on failure + console.warn('consumeCredits, reverting optimistic update'); set({ balance: balance, // Revert to original balance error: result?.data?.error || 'Failed to consume credits', @@ -135,7 +149,7 @@ export const useCreditsStore = create((set, get) => ({ }); return false; } catch (error) { - console.error('consume credits error:', error); + console.error('consumeCredits, error:', error); // Revert optimistic update on error set({ balance: balance, // Revert to original balance @@ -145,60 +159,4 @@ export const useCreditsStore = create((set, get) => ({ return false; } }, - - /** - * Force refresh credit balance (ignores cache) - * @param user Current user from auth session - */ - refreshCredits: async (user) => { - if (!user) { - set({ - error: 'No user found', - isLoading: false, - }); - return; - } - - set({ - isLoading: true, - error: null, - lastFetchTime: null, // Clear cache to force refresh - }); - - try { - const result = await getCreditBalanceAction(); - - if (result?.data?.success) { - set({ - balance: result.data.credits || 0, - isLoading: false, - error: null, - lastFetchTime: Date.now(), - }); - } else { - set({ - error: result?.data?.error || 'Failed to fetch credit balance', - isLoading: false, - }); - } - } catch (error) { - console.error('refresh credits error:', error); - set({ - error: 'Failed to fetch credit balance', - isLoading: false, - }); - } - }, - - /** - * Reset credits state - */ - resetCreditsState: () => { - set({ - balance: 0, - isLoading: false, - error: null, - lastFetchTime: null, - }); - }, })); diff --git a/src/stores/payment-store.ts b/src/stores/payment-store.ts index f2b11c6..9956073 100644 --- a/src/stores/payment-store.ts +++ b/src/stores/payment-store.ts @@ -5,6 +5,8 @@ import { getAllPricePlans } from '@/lib/price-plan'; import type { PricePlan, Subscription } from '@/payment/types'; import { create } from 'zustand'; +const CACHE_DURATION = 30 * 1000; + /** * Payment state interface */ @@ -17,9 +19,14 @@ export interface PaymentState { isLoading: boolean; // Error state error: string | null; + // Last fetch timestamp to avoid frequent requests + lastFetchTime: number | null; // Actions - fetchPayment: (user: Session['user'] | null | undefined) => Promise; + fetchPayment: ( + user: Session['user'] | null | undefined, + force?: boolean + ) => Promise; resetState: () => void; } @@ -33,12 +40,13 @@ export const usePaymentStore = create((set, get) => ({ 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) => { + fetchPayment: async (user, force = false) => { // Skip if already loading if (get().isLoading) return; @@ -48,10 +56,21 @@ export const usePaymentStore = create((set, get) => ({ 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 }); @@ -66,30 +85,26 @@ export const usePaymentStore = create((set, get) => ({ const result = await getLifetimeStatusAction({ userId: user.id }); if (result?.data?.success) { isLifetimeMember = result.data.isLifetimeMember || false; - console.log('get lifetime status result', result); + console.log('fetchPayment, lifetime status', isLifetimeMember); } else { - console.warn('get lifetime status failed', result?.data?.error); - // set({ - // error: result?.data?.error || 'Failed to fetch payment data', - // isLoading: false - // }); + console.warn( + 'fetchPayment, lifetime status error', + result?.data?.error + ); } } catch (error) { - console.error('get lifetime status error:', error); - // set({ - // error: 'Failed to fetch payment data', - // isLoading: false - // }); + console.error('fetchPayment, lifetime status error:', error); } // If lifetime member, set the lifetime plan if (isLifetimeMember) { - console.log('set lifetime plan for user', user.id); + console.log('fetchPayment, set lifetime plan'); set({ currentPlan: lifetimePlan || null, subscription: null, isLoading: false, error: null, + lastFetchTime: Date.now(), }); return; } @@ -108,34 +123,29 @@ export const usePaymentStore = create((set, get) => ({ (price) => price.priceId === activeSubscription.priceId ) ) || null; - console.log( - 'subscription found, setting plan for user', - user.id, - plan?.id - ); + 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( - 'no subscription found, setting free plan for user', - user.id - ); + 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( - 'fetch subscription for user failed', + 'fetchPayment, subscription for user failed', result?.data?.error ); set({ @@ -144,7 +154,7 @@ export const usePaymentStore = create((set, get) => ({ }); } } catch (error) { - console.error('fetch payment data error:', error); + console.error('fetchPayment, error:', error); set({ error: 'Failed to fetch payment data', isLoading: false, @@ -163,6 +173,7 @@ export const usePaymentStore = create((set, get) => ({ subscription: null, isLoading: false, error: null, + lastFetchTime: null, }); }, }));