Subscription dashboard improvements, AI text generation limit, and other fixes.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user