/** * @ai-summary React hook for tier-based feature access checking * @ai-context Used to gate premium features based on user subscription tier */ import { useQuery } from '@tanstack/react-query'; import { useAuth0 } from '@auth0/auth0-react'; import { apiClient } from '../api/client'; import type { SubscriptionTier } from '../../features/settings/types/profile.types'; // Feature tier configuration (mirrors backend) export interface FeatureConfig { minTier: SubscriptionTier; name: string; upgradePrompt: string; } interface FeatureTiersResponse { tiers: Record; features: Record; } interface AccessCheckResult { allowed: boolean; requiredTier: SubscriptionTier | null; config: FeatureConfig | null; } // Tier hierarchy for comparison const TIER_LEVELS: Record = { free: 0, pro: 1, enterprise: 2, }; // Resource limits per tier (mirrors backend VEHICLE_LIMITS) const RESOURCE_LIMITS = { vehicles: { free: 2, pro: 5, enterprise: null, // unlimited } as Record, }; /** * Hook to check if user can access tier-gated features * Fetches user profile for tier and feature config from backend */ export const useTierAccess = () => { const { isAuthenticated, isLoading: authLoading } = useAuth0(); // Fetch user profile for current tier const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: async () => { const response = await apiClient.get('/user/profile'); return response.data; }, enabled: isAuthenticated && !authLoading, staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, }); // Fetch feature tier config from backend (single source of truth) const featureConfigQuery = useQuery({ queryKey: ['feature-tiers'], queryFn: async () => { const response = await apiClient.get('/config/feature-tiers'); return response.data; }, enabled: isAuthenticated && !authLoading, staleTime: 30 * 60 * 1000, // 30 minutes - config rarely changes gcTime: 60 * 60 * 1000, // 1 hour cache refetchOnWindowFocus: false, refetchOnMount: false, }); const tier: SubscriptionTier = profileQuery.data?.subscriptionTier || 'free'; const features = featureConfigQuery.data?.features || {}; /** * Check if user can access a feature by key */ const hasAccess = (featureKey: string): boolean => { const config = features[featureKey]; if (!config) { // Unknown features are allowed (fail open for safety) return true; } return TIER_LEVELS[tier] >= TIER_LEVELS[config.minTier]; }; /** * Get detailed access information for a feature */ const checkAccess = (featureKey: string): AccessCheckResult => { const config = features[featureKey] || null; if (!config) { return { allowed: true, requiredTier: null, config: null, }; } return { allowed: TIER_LEVELS[tier] >= TIER_LEVELS[config.minTier], requiredTier: config.minTier, config, }; }; /** * Get resource limit for current user's tier * Resource-agnostic method for count-based limits (vehicles, documents, etc.) * * @param resourceType - Type of resource (e.g., 'vehicles') * @returns Maximum allowed count, or null for unlimited */ const getResourceLimit = (resourceType: keyof typeof RESOURCE_LIMITS): number | null => { const limits = RESOURCE_LIMITS[resourceType]; if (!limits) { return null; // Unknown resource type = unlimited } return limits[tier] ?? null; }; /** * Check if user is at or over their resource limit * Resource-agnostic method for count-based limits (vehicles, documents, etc.) * * @param resourceType - Type of resource (e.g., 'vehicles') * @param currentCount - Current number of resources user has * @returns true if user is at or over limit, false if under limit or unlimited */ const isAtResourceLimit = ( resourceType: keyof typeof RESOURCE_LIMITS, currentCount: number ): boolean => { const limit = getResourceLimit(resourceType); if (limit === null) { return false; // Unlimited } return currentCount >= limit; }; return { tier, loading: profileQuery.isLoading || featureConfigQuery.isLoading, hasAccess, checkAccess, getResourceLimit, isAtResourceLimit, }; }; export default useTierAccess;