bookwiz.io / lib / hooks / useSubscription.ts
useSubscription.ts
Raw
'use client'

import { useState, useEffect, useRef, useCallback } from 'react';
import { useAuth } from '@/components/AuthProvider';
import { supabase } from '@/lib/supabase';
import { PRICING_TIERS, getPlanByPriceId, PricingTier } from '@/lib/stripe';

export interface Subscription {
  id: string;
  status: string;
  price_id: string;
  current_period_start: string;
  current_period_end: string;
  cancel_at_period_end: boolean;
  stripe_customer_id?: string | null;
}

export function useSubscription() {
  const { user } = useAuth();
  const [subscription, setSubscription] = useState<Subscription | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const hasFetched = useRef(false);

  useEffect(() => {
    if (!user) {
      setSubscription(null);
      setLoading(false);
      hasFetched.current = false;
      return;
    }

    // Always fetch when user changes
    fetchSubscription();
  }, [user?.id]);

  // Refetch subscription when page becomes visible (in case user upgraded in another tab)
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (!document.hidden && user?.id) {
        // Reset the fetch flag to allow refetching
        hasFetched.current = false;
        fetchSubscription();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [user?.id]);

  const fetchSubscription = async () => {
    if (!user) return;
    
    try {
      setLoading(true);
      
      const { data, error } = await supabase
        .from('subscriptions')
        .select('id, status, price_id, current_period_start, current_period_end, cancel_at_period_end, stripe_customer_id')
        .eq('user_id', user.id)
        .in('status', ['active', 'trialing', 'past_due']) // Include all manageable statuses
        .order('created_at', { ascending: false })
        .maybeSingle(); // Use maybeSingle() instead of single() to handle 0 or 1 rows gracefully

      if (error) {
        throw error;
      }

      setSubscription(data);
      setError(null);
      hasFetched.current = true;
    } catch (err) {
      console.error('Error fetching subscription:', err);
      setError(err instanceof Error ? err.message : 'Failed to fetch subscription');
    } finally {
      setLoading(false);
    }
  };

  // Subscription status helpers
  const isActive = subscription?.status === 'active' || subscription?.status === 'trialing';
  const isPastDue = subscription?.status === 'past_due';
  const isCanceled = subscription?.status === 'canceled';
  const willCancelAtPeriodEnd = subscription?.cancel_at_period_end || false;

  // Get current plan (same logic as billing page)
  const getCurrentPlan = (): PricingTier => {
    if (!subscription?.price_id) return PRICING_TIERS.FREE;
    return getPlanByPriceId(subscription.price_id) || PRICING_TIERS.FREE;
  };

  // Simple feature access checking based on plan features
  const hasFeatureAccess = (feature: string): boolean => {
    const currentPlan = getCurrentPlan();
    if (!isActive && currentPlan.id !== 'free') return false;
    return currentPlan.features.includes(feature);
  };

  // Check if user can manage billing (has a Stripe customer ID and manageable status)
  const canManageBilling = (): boolean => {
    return !!(
      subscription?.stripe_customer_id && 
      subscription?.status && 
      ['active', 'trialing', 'past_due'].includes(subscription.status)
    );
  };

  // Plan comparison helpers
  const canUpgrade = (targetPriceId: string): boolean => {
    const currentPlan = getCurrentPlan();
    const targetPlan = getPlanByPriceId(targetPriceId);
    
    if (!targetPlan) return false;
    if (currentPlan.id === 'free') return true;
    
    return targetPlan.pricing.monthly.price > currentPlan.pricing.monthly.price;
  };

  const canDowngrade = (targetPriceId: string): boolean => {
    const currentPlan = getCurrentPlan();
    const targetPlan = getPlanByPriceId(targetPriceId);
    
    if (!targetPlan || currentPlan.id === 'free') return false;
    
    return targetPlan.pricing.monthly.price < currentPlan.pricing.monthly.price;
  };

  // Force refresh by fetching fresh data
  const refetch = useCallback(async () => {
    hasFetched.current = false;
    await fetchSubscription();
  }, []);

  // Clear cache and refetch (useful after checkout)
  const refreshAfterCheckout = useCallback(async () => {
    setSubscription(null);
    setError(null);
    hasFetched.current = false;
    await fetchSubscription();
  }, []);

  return {
    subscription,
    loading,
    error,
    isActive,
    isPastDue,
    isCanceled,
    willCancelAtPeriodEnd,
    getCurrentPlan,
    hasFeatureAccess,
    canUpgrade,
    canDowngrade,
    canManageBilling,
    refetch,
    refreshAfterCheckout
  };
}