turash/bugulma/frontend/contexts/SubscriptionContext.tsx

185 lines
5.8 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import { useAuth } from './AuthContext';
import {
Subscription,
SubscriptionPlan,
SubscriptionStatus,
SubscriptionFeatureFlag,
planHasFeature,
isSubscriptionActive,
SUBSCRIPTION_PLANS,
} from '@/types/subscription';
interface SubscriptionContextType {
subscription: Subscription | null;
isLoading: boolean;
refreshSubscription: () => Promise<void>;
// Convenience methods
hasFeature: (feature: SubscriptionFeatureFlag) => boolean;
hasActiveSubscription: boolean;
canAccessFeature: (feature: SubscriptionFeatureFlag) => boolean;
isWithinLimits: (limitType: 'organizations' | 'users' | 'storage' | 'apiCalls', current: number) => boolean;
getRemainingLimit: (limitType: 'organizations' | 'users' | 'storage' | 'apiCalls', current: number) => number;
}
const SubscriptionContext = createContext<SubscriptionContextType | undefined>(undefined);
export const useSubscription = () => {
const context = useContext(SubscriptionContext);
if (context === undefined) {
throw new Error('useSubscription must be used within a SubscriptionProvider');
}
return context;
};
interface SubscriptionProviderProps {
children: React.ReactNode;
}
/**
* Subscription context provider for managing subscription state
*/
export const SubscriptionProvider = ({ children }: SubscriptionProviderProps) => {
const { user, isAuthenticated } = useAuth();
const [subscription, setSubscription] = useState<Subscription | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchSubscription = useCallback(async (): Promise<Subscription | null> => {
if (!isAuthenticated || !user) {
return null;
}
try {
const token = localStorage.getItem('auth_token');
const isProduction = import.meta.env.PROD;
const baseUrl = import.meta.env.VITE_API_BASE_URL || (isProduction ? 'https://api.bugulma.city' : '');
const response = await fetch(`${baseUrl}/api/v1/subscription`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(5000),
});
if (response.ok) {
const data = await response.json();
return {
...data,
currentPeriodStart: new Date(data.currentPeriodStart),
currentPeriodEnd: new Date(data.currentPeriodEnd),
trialEnd: data.trialEnd ? new Date(data.trialEnd) : undefined,
};
}
// If no subscription, return default free plan
return {
id: 'free',
userId: user.id,
plan: 'free' as SubscriptionPlan,
status: 'none' as SubscriptionStatus,
billingPeriod: 'monthly',
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(),
cancelAtPeriodEnd: false,
features: SUBSCRIPTION_PLANS.free.features,
limits: SUBSCRIPTION_PLANS.free.limits,
};
} catch (error) {
console.error('Failed to fetch subscription:', error);
// Return free plan on error
return {
id: 'free',
userId: user.id,
plan: 'free' as SubscriptionPlan,
status: 'none' as SubscriptionStatus,
billingPeriod: 'monthly',
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(),
cancelAtPeriodEnd: false,
features: SUBSCRIPTION_PLANS.free.features,
limits: SUBSCRIPTION_PLANS.free.limits,
};
}
}, [isAuthenticated, user]);
const refreshSubscription = useCallback(async () => {
setIsLoading(true);
try {
const sub = await fetchSubscription();
setSubscription(sub);
} catch (error) {
console.error('Failed to refresh subscription:', error);
} finally {
setIsLoading(false);
}
}, [fetchSubscription]);
useEffect(() => {
if (isAuthenticated) {
refreshSubscription();
} else {
setSubscription(null);
setIsLoading(false);
}
}, [isAuthenticated, refreshSubscription]);
const hasFeature = useCallback(
(feature: SubscriptionFeatureFlag): boolean => {
if (!subscription) return false;
return planHasFeature(subscription.plan, feature);
},
[subscription]
);
const hasActiveSubscription = subscription
? isSubscriptionActive(subscription.status)
: false;
const canAccessFeature = useCallback(
(feature: SubscriptionFeatureFlag): boolean => {
if (!subscription) return false;
return hasActiveSubscription && hasFeature(feature);
},
[subscription, hasActiveSubscription, hasFeature]
);
const isWithinLimits = useCallback(
(limitType: 'organizations' | 'users' | 'storage' | 'apiCalls', current: number): boolean => {
if (!subscription) return true; // No subscription = free plan limits
const limit = subscription.limits[limitType];
if (limit === undefined || limit === -1) return true; // Unlimited
return current < limit;
},
[subscription]
);
const getRemainingLimit = useCallback(
(limitType: 'organizations' | 'users' | 'storage' | 'apiCalls', current: number): number => {
if (!subscription) {
const freeLimit = SUBSCRIPTION_PLANS.free.limits[limitType] || 0;
return Math.max(0, freeLimit - current);
}
const limit = subscription.limits[limitType];
if (limit === undefined || limit === -1) return Infinity;
return Math.max(0, limit - current);
},
[subscription]
);
const value: SubscriptionContextType = {
subscription,
isLoading,
refreshSubscription,
hasFeature,
hasActiveSubscription,
canAccessFeature,
isWithinLimits,
getRemainingLimit,
};
return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;
};