Refactor billing flows to require authenticated user IDs

This commit is contained in:
ي
2026-03-04 20:41:07 +05:30
parent 2318fd8a48
commit 5a7b9e6c6b
7 changed files with 54 additions and 33 deletions

View File

@@ -24,7 +24,7 @@ interface UseCompactBillingDataReturn {
/**
* Custom hook for managing CompactBillingDashboard data fetching and state
*/
export const useCompactBillingData = (userId?: string): UseCompactBillingDataReturn => {
export const useCompactBillingData = (userId: string): UseCompactBillingDataReturn => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);

View File

@@ -21,7 +21,7 @@ import { MonthlyBudgetUsage } from './components/MonthlyBudgetUsage';
import { AlertsSection } from './components/AlertsSection';
interface CompactBillingDashboardProps {
userId?: string;
userId: string;
terminalTheme?: boolean;
}

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '@clerk/clerk-react';
import {
Box,
Container,
@@ -64,6 +65,8 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
// Conditional component selection based on terminal theme
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
const AlertComponent = terminalTheme ? TerminalAlert : Alert;
const { userId: authUserId } = useAuth();
const effectiveUserId = userId || authUserId;
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
@@ -73,10 +76,16 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
const [healthError, setHealthError] = useState<string | null>(null);
const fetchDashboardData = async (showSuccessToast: boolean = false) => {
if (!effectiveUserId) {
setLoading(false);
setError('Unable to load billing data: missing authenticated user context.');
return;
}
try {
// Use Promise.allSettled to prevent health check timeout from blocking dashboard
const results = await Promise.allSettled([
billingService.getDashboardData(),
billingService.getDashboardData(effectiveUserId),
monitoringService.getSystemHealth()
]);
@@ -147,14 +156,16 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
};
useEffect(() => {
if (!effectiveUserId) return;
fetchDashboardData();
}, [userId]);
}, [effectiveUserId]);
// Event-driven refresh: refresh only when non-billing/monitoring APIs complete
useEffect(() => {
const unsubscribe = onApiEvent((detail) => {
if (detail.source && detail.source !== 'other') return;
Promise.allSettled([billingService.getDashboardData(), monitoringService.getSystemHealth()])
if (!effectiveUserId) return;
Promise.allSettled([billingService.getDashboardData(effectiveUserId), monitoringService.getSystemHealth()])
.then((results) => {
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
@@ -176,25 +187,26 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
.catch(() => {/* ignore */});
});
return unsubscribe;
}, []);
}, [effectiveUserId]);
// Refetch when tab becomes visible again (cheap, avoids polling)
useEffect(() => {
const onVisible = () => {
if (document.visibilityState === 'visible') {
if (document.visibilityState === 'visible' && effectiveUserId) {
fetchDashboardData();
}
};
document.addEventListener('visibilitychange', onVisible);
return () => document.removeEventListener('visibilitychange', onVisible);
}, []);
}, [effectiveUserId]);
// Listen for billing refresh requests (e.g., when subscription limits are exceeded)
useEffect(() => {
const handleBillingRefresh = () => {
console.log('EnhancedBillingDashboard: Billing refresh requested, refreshing data...');
// Use allSettled to prevent health check from blocking refresh
Promise.allSettled([billingService.getDashboardData(), monitoringService.getSystemHealth()])
if (!effectiveUserId) return;
Promise.allSettled([billingService.getDashboardData(effectiveUserId), monitoringService.getSystemHealth()])
.then((results) => {
if (results[0].status === 'fulfilled') {
setDashboardData(results[0].value);
@@ -233,7 +245,7 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
return () => {
window.removeEventListener('billing-refresh-requested', handleBillingRefresh);
};
}, []); // Empty deps - handler doesn't depend on component state
}, [effectiveUserId]); // Empty deps - handler doesn't depend on component state
const handleViewModeChange = (
event: React.MouseEvent<HTMLElement>,
@@ -463,7 +475,11 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<CompactBillingDashboard userId={userId} terminalTheme={terminalTheme} />
{effectiveUserId ? (
<CompactBillingDashboard userId={effectiveUserId} terminalTheme={terminalTheme} />
) : (
<AlertComponent severity="warning">Unable to load billing data: missing authenticated user context.</AlertComponent>
)}
</motion.div>
) : (
<motion.div

View File

@@ -42,7 +42,7 @@ import {
import { showToastNotification } from '../../utils/toastNotifications';
interface SubscriptionRenewalHistoryProps {
userId?: string;
userId: string;
terminalTheme?: boolean;
initialLimit?: number;
}

View File

@@ -24,7 +24,7 @@ interface UseOAuthTokenAlertsOptions {
enabled?: boolean;
/**
* User ID - if not provided, will use localStorage or skip polling
* Authenticated user ID from Clerk; polling is skipped when unavailable
*/
userId?: string;
}
@@ -54,8 +54,7 @@ export function useOAuthTokenAlerts(options: UseOAuthTokenAlertsOptions = {}) {
return;
}
const actualUserId = userId || localStorage.getItem('user_id');
if (!actualUserId) {
if (!userId) {
console.debug('useOAuthTokenAlerts: No user ID available, skipping polling');
return;
}
@@ -70,7 +69,7 @@ export function useOAuthTokenAlerts(options: UseOAuthTokenAlertsOptions = {}) {
isPollingRef.current = true;
// Fetch unread alerts only
const alerts = await billingService.getUsageAlerts(actualUserId, true);
const alerts = await billingService.getUsageAlerts(userId, true);
// Filter for OAuth token alerts
const oauthAlerts = alerts.filter(

View File

@@ -4,6 +4,7 @@
*/
import React, { useEffect } from 'react';
import { useAuth } from '@clerk/clerk-react';
import {
Box,
Container,
@@ -73,6 +74,7 @@ const TerminalIconButton = styled(IconButton)({
const BillingPage: React.FC = () => {
const { subscription, checkSubscription } = useSubscription();
const { userId } = useAuth();
// Monitor subscription status and show toast notifications
useEffect(() => {
@@ -232,14 +234,18 @@ const BillingPage: React.FC = () => {
{/* Billing Dashboard Content */}
<Box>
<EnhancedBillingDashboard terminalTheme={true} />
{userId ? (
<EnhancedBillingDashboard userId={userId} terminalTheme={true} />
) : null}
{/* Subscription Renewal History Section */}
<SubscriptionRenewalHistory
userId={undefined}
{userId ? (
<SubscriptionRenewalHistory
userId={userId}
terminalTheme={true}
initialLimit={20}
/>
initialLimit={20}
/>
) : null}
{/* Usage Logs Section */}
<Box sx={{ mt: 4 }}>

View File

@@ -294,9 +294,9 @@ export const billingService = {
/**
* Get comprehensive dashboard data for a user
*/
getDashboardData: async (userId?: string): Promise<DashboardData> => {
getDashboardData: async (userId: string): Promise<DashboardData> => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
// Debug logs removed to reduce console noise
const response = await billingAPI.get<DashboardAPIResponse>(`/dashboard/${actualUserId}`);
@@ -383,9 +383,9 @@ export const billingService = {
/**
* Get current usage statistics for a user
*/
getUsageStats: async (userId?: string, period?: string): Promise<UsageStats> => {
getUsageStats: async (userId: string, period?: string): Promise<UsageStats> => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
const params = period ? { billing_period: period } : {};
const response = await billingAPI.get<UsageAPIResponse>(`/usage/${actualUserId}`, { params });
@@ -409,9 +409,9 @@ export const billingService = {
/**
* Get usage trends over time
*/
getUsageTrends: async (userId?: string, months: number = 6): Promise<UsageTrends> => {
getUsageTrends: async (userId: string, months: number = 6): Promise<UsageTrends> => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
const response = await billingAPI.get(`/usage/${actualUserId}/trends`, {
params: { months }
});
@@ -469,9 +469,9 @@ export const billingService = {
/**
* Get usage alerts for a user
*/
getUsageAlerts: async (userId?: string, unreadOnly: boolean = false): Promise<UsageAlert[]> => {
getUsageAlerts: async (userId: string, unreadOnly: boolean = false): Promise<UsageAlert[]> => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
const response = await billingAPI.get<AlertsAPIResponse>(`/alerts/${actualUserId}`, {
params: { unread_only: unreadOnly }
});
@@ -507,9 +507,9 @@ export const billingService = {
/**
* Get user's current subscription information
*/
getUserSubscription: async (userId?: string) => {
getUserSubscription: async (userId: string) => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
const response = await billingAPI.get(`/user/${actualUserId}/subscription`);
if (!response.data.success) {
@@ -555,12 +555,12 @@ export const billingService = {
* Get subscription renewal history for the current user
*/
getRenewalHistory: async (
userId?: string,
userId: string,
limit: number = 50,
offset: number = 0
): Promise<RenewalHistoryResponse> => {
try {
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
const actualUserId = userId;
const params: any = { limit, offset };
const response = await billingAPI.get<RenewalHistoryAPIResponse>(