diff --git a/src/app/[locale]/providers.tsx b/src/app/[locale]/providers.tsx
index 82192f0..40f290e 100644
--- a/src/app/[locale]/providers.tsx
+++ b/src/app/[locale]/providers.tsx
@@ -4,6 +4,7 @@ import { ActiveThemeProvider } from '@/components/layout/active-theme-provider';
import { PaymentProvider } from '@/components/layout/payment-provider';
import { TooltipProvider } from '@/components/ui/tooltip';
import { websiteConfig } from '@/config/website';
+import { CreditsProvider } from '@/providers/credits-provider';
import type { Translations } from 'fumadocs-ui/i18n';
import { RootProvider } from 'fumadocs-ui/provider';
import { useTranslations } from 'next-intl';
@@ -25,6 +26,7 @@ interface ProvidersProps {
* - RootProvider: Provides the root provider for Fumadocs UI.
* - TooltipProvider: Provides the tooltip to the app.
* - PaymentProvider: Provides the payment state to the app.
+ * - CreditsProvider: Provides the credits state to the app.
*/
export function Providers({ children, locale }: ProvidersProps) {
const theme = useTheme();
@@ -61,7 +63,9 @@ export function Providers({ children, locale }: ProvidersProps) {
- {children}
+
+ {children}
+
diff --git a/src/providers/credits-provider.tsx b/src/providers/credits-provider.tsx
new file mode 100644
index 0000000..3ac8ca5
--- /dev/null
+++ b/src/providers/credits-provider.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { useCurrentUser } from '@/hooks/use-current-user';
+import { useCreditsStore } from '@/stores/credits-store';
+import { useEffect } from 'react';
+
+/**
+ * Credits Provider Component
+ *
+ * This component initializes the credits store when the user is authenticated
+ * and handles cleanup when the user logs out.
+ */
+export function CreditsProvider({ children }: { children: React.ReactNode }) {
+ const user = useCurrentUser();
+ const { fetchCredits, resetState } = useCreditsStore();
+
+ useEffect(() => {
+ if (user) {
+ // User is logged in, fetch their credits
+ fetchCredits(user);
+ } else {
+ // User is logged out, reset the credits state
+ resetState();
+ }
+ }, [user, fetchCredits, resetState]);
+
+ return <>{children}>;
+}
diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts
new file mode 100644
index 0000000..7c39180
--- /dev/null
+++ b/src/stores/credits-store.ts
@@ -0,0 +1,212 @@
+import { consumeCreditsAction } from '@/actions/consume-credits';
+import { getCreditBalanceAction } from '@/actions/get-credit-balance';
+import type { Session } from '@/lib/auth-types';
+import { create } from 'zustand';
+
+/**
+ * Credits state interface
+ */
+export interface CreditsState {
+ // Current credit balance
+ balance: number;
+ // Loading state
+ isLoading: boolean;
+ // Error state
+ error: string | null;
+ // Last fetch timestamp to avoid frequent requests
+ lastFetchTime: number | null;
+
+ // Actions
+ fetchCredits: (user: Session['user'] | null | undefined) => Promise;
+ consumeCredits: (amount: number, description: string) => Promise;
+ refreshCredits: (user: Session['user'] | null | undefined) => Promise;
+ resetState: () => void;
+ // For optimistic updates
+ updateBalanceOptimistically: (amount: number) => 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
+ */
+export const useCreditsStore = create((set, get) => ({
+ // Initial state
+ balance: 0,
+ isLoading: false,
+ error: null,
+ lastFetchTime: null,
+
+ /**
+ * Fetch credit balance for the current user with caching
+ * @param user Current user from auth session
+ */
+ fetchCredits: async (user) => {
+ // Skip if already loading
+ if (get().isLoading) return;
+
+ // Skip if no user is provided
+ if (!user) {
+ set({
+ balance: 0,
+ error: null,
+ lastFetchTime: null,
+ });
+ 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
+ }
+
+ set({ isLoading: true, error: null });
+
+ try {
+ const result = await getCreditBalanceAction();
+
+ if (result?.data?.success) {
+ set({
+ balance: result.data.credits || 0,
+ isLoading: false,
+ error: null,
+ lastFetchTime: now,
+ });
+ } else {
+ set({
+ error: result?.data?.error || 'Failed to fetch credit balance',
+ isLoading: false,
+ });
+ }
+ } catch (error) {
+ console.error('fetch credits error:', error);
+ set({
+ error: 'Failed to fetch credit balance',
+ isLoading: false,
+ });
+ }
+ },
+
+ /**
+ * Consume credits with optimistic updates
+ * @param amount Amount of credits to consume
+ * @param description Description for the transaction
+ * @returns Promise Success status
+ */
+ consumeCredits: async (amount: number, description: string) => {
+ const { balance } = get();
+
+ // Check if we have enough credits
+ if (balance < amount) {
+ set({
+ error: `Insufficient credits. You need ${amount} credits but only have ${balance}.`,
+ });
+ return false;
+ }
+
+ // Optimistically update the balance
+ set({
+ balance: balance - amount,
+ error: null,
+ isLoading: true,
+ });
+
+ try {
+ const result = await consumeCreditsAction({
+ amount,
+ description,
+ });
+
+ if (result?.data?.success) {
+ set({
+ isLoading: false,
+ error: null,
+ });
+ return true;
+ }
+
+ // Revert optimistic update on failure
+ set({
+ balance: balance, // Revert to original balance
+ error: result?.data?.error || 'Failed to consume credits',
+ isLoading: false,
+ });
+ return false;
+ } catch (error) {
+ console.error('consume credits error:', error);
+ // Revert optimistic update on error
+ set({
+ balance: balance, // Revert to original balance
+ error: 'Failed to consume credits',
+ isLoading: false,
+ });
+ return false;
+ }
+ },
+
+ /**
+ * Force refresh credit balance (ignores cache)
+ * @param user Current user from auth session
+ */
+ refreshCredits: async (user) => {
+ if (!user) 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,
+ });
+ }
+ },
+
+ /**
+ * Update balance optimistically (for external credit additions)
+ * @param amount Amount to add to current balance
+ */
+ updateBalanceOptimistically: (amount: number) => {
+ const { balance } = get();
+ set({
+ balance: balance + amount,
+ lastFetchTime: null, // Clear cache to fetch fresh data next time
+ });
+ },
+
+ /**
+ * Reset credits state
+ */
+ resetState: () => {
+ set({
+ balance: 0,
+ isLoading: false,
+ error: null,
+ lastFetchTime: null,
+ });
+ },
+}));