Subscription dashboard improvements, AI text generation limit, and other fixes.

This commit is contained in:
ajaysi
2025-11-01 18:01:14 +05:30
parent cdb41aec1b
commit de4328175d
64 changed files with 5809 additions and 444 deletions

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback, useRef } from 'react';
import { apiClient, setGlobalSubscriptionErrorHandler } from '../api/client';
import SubscriptionExpiredModal from '../components/SubscriptionExpiredModal';
@@ -60,6 +60,8 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
// New: Grace window after plan changes to avoid noisy UX
const [graceUntil, setGraceUntil] = useState<number>(0);
const [planSignature, setPlanSignature] = useState<string>("");
// Flag to track if current modal is a usage limit modal (should never be auto-closed)
const [isUsageLimitModal, setIsUsageLimitModal] = useState<boolean>(false);
const checkSubscription = useCallback(async () => {
// Throttle subscription checks to prevent excessive API calls
@@ -86,6 +88,10 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
return;
}
// Wait a moment to ensure auth token getter is installed
// This prevents 401 errors during app initialization
await new Promise(resolve => setTimeout(resolve, 200));
console.log('SubscriptionContext: Checking subscription for user:', userId);
const response = await apiClient.get(`/api/subscription/status/${userId}`);
const subscriptionData = response.data.data;
@@ -101,29 +107,42 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
setPlanSignature(newSignature);
setGraceUntil(Date.now() + 5 * 60 * 1000);
// Close any existing modal as plan just changed
if (showModal) {
// BUT: Don't close usage limit modals - they're important even after plan changes
if (showModal && !isUsageLimitModal) {
console.log('SubscriptionContext: Plan changed, closing non-usage-limit modal');
setShowModal(false);
setModalErrorData(null);
} else if (showModal && isUsageLimitModal) {
console.log('SubscriptionContext: Plan changed but usage limit modal is open, keeping it open');
}
}
} catch (_e) {}
// If we have a valid subscription and the modal is open, close it
// BUT: NEVER close usage limit modals - user needs to see they hit a limit even with active subscription
if (subscriptionData && subscriptionData.active && showModal) {
console.log('SubscriptionContext: Valid subscription detected, closing modal');
setShowModal(false);
setModalErrorData(null);
setLastModalShowTime(0); // Reset the cooldown timer
}
// Also check if this is a usage limit error that should be suppressed
if (subscriptionData && subscriptionData.active && modalErrorData) {
const now = Date.now();
const timeSinceLastModal = now - lastModalShowTime;
// If it's been less than 10 minutes since modal was shown for usage limits, keep it closed
if (timeSinceLastModal < 600000 && modalErrorData.usage_info) {
console.log('SubscriptionContext: Recent usage limit modal, keeping it closed');
// Check if this is a usage limit modal (using flag or checking error data)
const hasUsageInfo = modalErrorData?.usage_info ||
(modalErrorData?.current_tokens !== undefined) ||
(modalErrorData?.current_calls !== undefined) ||
(modalErrorData?.limit !== undefined) ||
(modalErrorData?.requested_tokens !== undefined);
const isUsageLimit = isUsageLimitModal || hasUsageInfo;
if (isUsageLimit) {
console.log('SubscriptionContext: Usage limit modal detected - KEEPING OPEN (never auto-close usage limit modals)', {
isUsageLimitModal,
hasUsageInfo,
modalErrorDataKeys: modalErrorData ? Object.keys(modalErrorData) : []
});
// Do NOT close - usage limit modals should stay open until user dismisses them
} else {
console.log('SubscriptionContext: Non-usage-limit modal detected, closing since subscription is active');
setShowModal(false);
setModalErrorData(null);
setIsUsageLimitModal(false);
setLastModalShowTime(0); // Reset the cooldown timer
}
}
@@ -156,7 +175,7 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
setLastModalShowTime(now);
}
}
} catch (err) {
} catch (err: any) {
console.error('Error checking subscription:', err);
// Check if it's a connection error that should be handled at the app level
@@ -165,6 +184,16 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
throw err;
}
// Handle 401 errors gracefully during initialization - don't block routing
// 401 might happen if auth token getter isn't ready yet
if (err?.response?.status === 401) {
console.warn('Subscription check failed with 401 - auth may not be ready yet, will retry later');
setError(null); // Don't set error for 401 during init
setLoading(false);
// Don't throw - allow routing to proceed, subscription check will retry later
return;
}
setError(err instanceof Error ? err.message : 'Failed to check subscription');
// Don't default to free tier on error - preserve existing subscription or leave null
@@ -173,21 +202,30 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
} finally {
setLoading(false);
}
}, [lastCheckTime, planSignature, showModal, modalErrorData, lastModalShowTime, graceUntil]);
}, [lastCheckTime, planSignature, showModal, modalErrorData, lastModalShowTime, graceUntil, isUsageLimitModal]);
const refreshSubscription = useCallback(async () => {
await checkSubscription();
}, [checkSubscription]);
const showExpiredModal = useCallback(() => {
setIsUsageLimitModal(false);
setShowModal(true);
}, []);
const hideExpiredModal = useCallback(() => {
console.log('SubscriptionExpiredModal: User manually closed modal');
setShowModal(false);
setIsUsageLimitModal(false); // Reset flag when user closes modal
setModalErrorData(null);
}, []);
const handleRenewSubscription = useCallback(() => {
// Save current location so we can return after renewal
const currentPath = window.location.pathname;
sessionStorage.setItem('subscription_referrer', currentPath);
console.log('SubscriptionContext: Navigating to pricing page, saved referrer:', currentPath);
window.location.href = '/pricing';
}, []);
@@ -203,42 +241,131 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
const now = Date.now();
// If we have subscription data and it's active, always suppress modal for usage limits
if (subscription && subscription.active) {
console.log('SubscriptionContext: Active subscription; suppressing usage-limit modal');
return true; // Do not show modal for active plan usage limits
// Check if this is a usage limit error (status 429) vs subscription expired (402)
let errorData = error.response?.data || {};
// DEBUG: Log the raw error data structure
console.log('SubscriptionContext: Raw error data', {
type: typeof errorData,
isArray: Array.isArray(errorData),
data: errorData,
stringified: JSON.stringify(errorData)
});
// If errorData is an array, extract the first element (common FastAPI response format)
if (Array.isArray(errorData)) {
console.log('SubscriptionContext: errorData is array, extracting first element');
errorData = errorData[0] || {};
}
// If we don't have subscription data yet, defer the decision
if (!subscription) {
console.log('SubscriptionContext: No subscription data yet, deferring modal decision');
setDeferredError(error);
return true; // Handle the error but don't show modal yet
}
// If subscription is not active, show modal immediately
if (!subscription.active) {
console.log('SubscriptionContext: Inactive subscription, showing modal immediately');
const errorData = error.response?.data || {};
setModalErrorData({
provider: errorData.provider,
usage_info: errorData.usage_info,
message: errorData.message || errorData.error
// Check for usage_info in various possible locations
const usageInfo = errorData.usage_info ||
(errorData.current_calls !== undefined ? errorData : null) ||
null;
// Usage limit error: 429 status with usage info OR 429 status without explicit expiration
const isUsageLimitError = status === 429 && (usageInfo || errorData.provider || errorData.message);
const isSubscriptionExpired = status === 402 || (status === 429 && !isUsageLimitError);
console.log('SubscriptionContext: Error analysis', {
status,
isUsageLimitError,
isSubscriptionExpired,
hasUsageInfo: !!usageInfo,
errorDataType: typeof errorData,
errorDataKeys: typeof errorData === 'object' && !Array.isArray(errorData) ? Object.keys(errorData) : 'not-an-object',
errorData: errorData
});
// For usage limit errors (429 with usage_info), always show modal - even for active subscriptions
// Ignore grace window and cooldown for usage limit errors (user needs to know immediately)
if (isUsageLimitError) {
const modalData = {
provider: errorData.provider || usageInfo?.provider || 'unknown',
usage_info: usageInfo || errorData,
message: errorData.message || errorData.error || 'You have reached your usage limit.'
};
console.log('SubscriptionContext: Usage limit exceeded, showing modal (ignoring grace window/cooldown)', {
modalData,
errorData: Object.keys(errorData),
usageInfo: usageInfo ? Object.keys(usageInfo) : null
});
// Set flag to mark this as a usage limit modal (should never be auto-closed)
setIsUsageLimitModal(true);
setModalErrorData(modalData);
setShowModal(true);
setLastModalShowTime(now);
console.log('SubscriptionContext: Modal state updated - showModal should be true, isUsageLimitModal = true');
return true;
}
// For subscription expired errors, handle based on subscription status
if (isSubscriptionExpired) {
// If we have subscription data and it's active, this shouldn't happen but suppress anyway
if (subscription && subscription.active) {
console.log('SubscriptionContext: Active subscription but got expired error, suppressing modal');
return true;
}
// If we don't have subscription data yet, defer the decision
if (!subscription) {
console.log('SubscriptionContext: No subscription data yet, deferring modal decision');
setDeferredError(error);
return true; // Handle the error but don't show modal yet
}
// If subscription is not active, show modal immediately
if (!subscription.active) {
console.log('SubscriptionContext: Inactive subscription, showing modal immediately');
setIsUsageLimitModal(false);
setModalErrorData({
provider: errorData.provider,
usage_info: errorData.usage_info,
message: errorData.message || errorData.error
});
setShowModal(true);
setLastModalShowTime(now);
return true;
}
}
}
return false; // Not a subscription error
}, [subscription]);
// Register the global error handler with the API client
// Use a ref to ensure the latest handler is always used
const handlerRef = useRef(globalSubscriptionErrorHandler);
useEffect(() => {
handlerRef.current = globalSubscriptionErrorHandler;
}, [globalSubscriptionErrorHandler]);
useEffect(() => {
console.log('SubscriptionContext: Registering global subscription error handler');
setGlobalSubscriptionErrorHandler(globalSubscriptionErrorHandler);
}, [globalSubscriptionErrorHandler]);
setGlobalSubscriptionErrorHandler((error: any) => {
// Always use the latest handler from ref
return handlerRef.current(error);
});
// Cleanup: Don't remove the handler on unmount - it should persist
// This ensures errors can still be caught even during component transitions
}, []); // Empty deps - only register once, but handler ref updates automatically
useEffect(() => {
const eventHandler = (event: Event) => {
const customEvent = event as CustomEvent;
console.log('SubscriptionContext: Received subscription-error event fallback', customEvent.detail);
handlerRef.current(customEvent.detail);
};
window.addEventListener('subscription-error', eventHandler as EventListener);
return () => {
window.removeEventListener('subscription-error', eventHandler as EventListener);
};
}, []);
useEffect(() => {
// Check subscription on mount