Refactor billing flows to require authenticated user IDs
This commit is contained in:
@@ -24,7 +24,7 @@ interface UseCompactBillingDataReturn {
|
|||||||
/**
|
/**
|
||||||
* Custom hook for managing CompactBillingDashboard data fetching and state
|
* 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 [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||||
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { MonthlyBudgetUsage } from './components/MonthlyBudgetUsage';
|
|||||||
import { AlertsSection } from './components/AlertsSection';
|
import { AlertsSection } from './components/AlertsSection';
|
||||||
|
|
||||||
interface CompactBillingDashboardProps {
|
interface CompactBillingDashboardProps {
|
||||||
userId?: string;
|
userId: string;
|
||||||
terminalTheme?: boolean;
|
terminalTheme?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '@clerk/clerk-react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@@ -64,6 +65,8 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
// Conditional component selection based on terminal theme
|
// Conditional component selection based on terminal theme
|
||||||
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
|
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
|
||||||
const AlertComponent = terminalTheme ? TerminalAlert : Alert;
|
const AlertComponent = terminalTheme ? TerminalAlert : Alert;
|
||||||
|
const { userId: authUserId } = useAuth();
|
||||||
|
const effectiveUserId = userId || authUserId;
|
||||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||||
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -73,10 +76,16 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
const [healthError, setHealthError] = useState<string | null>(null);
|
const [healthError, setHealthError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchDashboardData = async (showSuccessToast: boolean = false) => {
|
const fetchDashboardData = async (showSuccessToast: boolean = false) => {
|
||||||
|
if (!effectiveUserId) {
|
||||||
|
setLoading(false);
|
||||||
|
setError('Unable to load billing data: missing authenticated user context.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Promise.allSettled to prevent health check timeout from blocking dashboard
|
// Use Promise.allSettled to prevent health check timeout from blocking dashboard
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
billingService.getDashboardData(),
|
billingService.getDashboardData(effectiveUserId),
|
||||||
monitoringService.getSystemHealth()
|
monitoringService.getSystemHealth()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -147,14 +156,16 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!effectiveUserId) return;
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, [userId]);
|
}, [effectiveUserId]);
|
||||||
|
|
||||||
// Event-driven refresh: refresh only when non-billing/monitoring APIs complete
|
// Event-driven refresh: refresh only when non-billing/monitoring APIs complete
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = onApiEvent((detail) => {
|
const unsubscribe = onApiEvent((detail) => {
|
||||||
if (detail.source && detail.source !== 'other') return;
|
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) => {
|
.then((results) => {
|
||||||
if (results[0].status === 'fulfilled') {
|
if (results[0].status === 'fulfilled') {
|
||||||
setDashboardData(results[0].value);
|
setDashboardData(results[0].value);
|
||||||
@@ -176,25 +187,26 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
.catch(() => {/* ignore */});
|
.catch(() => {/* ignore */});
|
||||||
});
|
});
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, [effectiveUserId]);
|
||||||
|
|
||||||
// Refetch when tab becomes visible again (cheap, avoids polling)
|
// Refetch when tab becomes visible again (cheap, avoids polling)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onVisible = () => {
|
const onVisible = () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible' && effectiveUserId) {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('visibilitychange', onVisible);
|
document.addEventListener('visibilitychange', onVisible);
|
||||||
return () => document.removeEventListener('visibilitychange', onVisible);
|
return () => document.removeEventListener('visibilitychange', onVisible);
|
||||||
}, []);
|
}, [effectiveUserId]);
|
||||||
|
|
||||||
// Listen for billing refresh requests (e.g., when subscription limits are exceeded)
|
// Listen for billing refresh requests (e.g., when subscription limits are exceeded)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleBillingRefresh = () => {
|
const handleBillingRefresh = () => {
|
||||||
console.log('EnhancedBillingDashboard: Billing refresh requested, refreshing data...');
|
console.log('EnhancedBillingDashboard: Billing refresh requested, refreshing data...');
|
||||||
// Use allSettled to prevent health check from blocking refresh
|
// 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) => {
|
.then((results) => {
|
||||||
if (results[0].status === 'fulfilled') {
|
if (results[0].status === 'fulfilled') {
|
||||||
setDashboardData(results[0].value);
|
setDashboardData(results[0].value);
|
||||||
@@ -233,7 +245,7 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('billing-refresh-requested', handleBillingRefresh);
|
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 = (
|
const handleViewModeChange = (
|
||||||
event: React.MouseEvent<HTMLElement>,
|
event: React.MouseEvent<HTMLElement>,
|
||||||
@@ -463,7 +475,11 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
|||||||
exit={{ opacity: 0, x: 20 }}
|
exit={{ opacity: 0, x: 20 }}
|
||||||
transition={{ duration: 0.3 }}
|
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>
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
import { showToastNotification } from '../../utils/toastNotifications';
|
import { showToastNotification } from '../../utils/toastNotifications';
|
||||||
|
|
||||||
interface SubscriptionRenewalHistoryProps {
|
interface SubscriptionRenewalHistoryProps {
|
||||||
userId?: string;
|
userId: string;
|
||||||
terminalTheme?: boolean;
|
terminalTheme?: boolean;
|
||||||
initialLimit?: number;
|
initialLimit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface UseOAuthTokenAlertsOptions {
|
|||||||
enabled?: boolean;
|
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;
|
userId?: string;
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,7 @@ export function useOAuthTokenAlerts(options: UseOAuthTokenAlertsOptions = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualUserId = userId || localStorage.getItem('user_id');
|
if (!userId) {
|
||||||
if (!actualUserId) {
|
|
||||||
console.debug('useOAuthTokenAlerts: No user ID available, skipping polling');
|
console.debug('useOAuthTokenAlerts: No user ID available, skipping polling');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,7 +69,7 @@ export function useOAuthTokenAlerts(options: UseOAuthTokenAlertsOptions = {}) {
|
|||||||
isPollingRef.current = true;
|
isPollingRef.current = true;
|
||||||
|
|
||||||
// Fetch unread alerts only
|
// Fetch unread alerts only
|
||||||
const alerts = await billingService.getUsageAlerts(actualUserId, true);
|
const alerts = await billingService.getUsageAlerts(userId, true);
|
||||||
|
|
||||||
// Filter for OAuth token alerts
|
// Filter for OAuth token alerts
|
||||||
const oauthAlerts = alerts.filter(
|
const oauthAlerts = alerts.filter(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useAuth } from '@clerk/clerk-react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@@ -73,6 +74,7 @@ const TerminalIconButton = styled(IconButton)({
|
|||||||
|
|
||||||
const BillingPage: React.FC = () => {
|
const BillingPage: React.FC = () => {
|
||||||
const { subscription, checkSubscription } = useSubscription();
|
const { subscription, checkSubscription } = useSubscription();
|
||||||
|
const { userId } = useAuth();
|
||||||
|
|
||||||
// Monitor subscription status and show toast notifications
|
// Monitor subscription status and show toast notifications
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -232,14 +234,18 @@ const BillingPage: React.FC = () => {
|
|||||||
|
|
||||||
{/* Billing Dashboard Content */}
|
{/* Billing Dashboard Content */}
|
||||||
<Box>
|
<Box>
|
||||||
<EnhancedBillingDashboard terminalTheme={true} />
|
{userId ? (
|
||||||
|
<EnhancedBillingDashboard userId={userId} terminalTheme={true} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Subscription Renewal History Section */}
|
{/* Subscription Renewal History Section */}
|
||||||
|
{userId ? (
|
||||||
<SubscriptionRenewalHistory
|
<SubscriptionRenewalHistory
|
||||||
userId={undefined}
|
userId={userId}
|
||||||
terminalTheme={true}
|
terminalTheme={true}
|
||||||
initialLimit={20}
|
initialLimit={20}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Usage Logs Section */}
|
{/* Usage Logs Section */}
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
|
|||||||
@@ -294,9 +294,9 @@ export const billingService = {
|
|||||||
/**
|
/**
|
||||||
* Get comprehensive dashboard data for a user
|
* Get comprehensive dashboard data for a user
|
||||||
*/
|
*/
|
||||||
getDashboardData: async (userId?: string): Promise<DashboardData> => {
|
getDashboardData: async (userId: string): Promise<DashboardData> => {
|
||||||
try {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
// Debug logs removed to reduce console noise
|
// Debug logs removed to reduce console noise
|
||||||
|
|
||||||
const response = await billingAPI.get<DashboardAPIResponse>(`/dashboard/${actualUserId}`);
|
const response = await billingAPI.get<DashboardAPIResponse>(`/dashboard/${actualUserId}`);
|
||||||
@@ -383,9 +383,9 @@ export const billingService = {
|
|||||||
/**
|
/**
|
||||||
* Get current usage statistics for a user
|
* Get current usage statistics for a user
|
||||||
*/
|
*/
|
||||||
getUsageStats: async (userId?: string, period?: string): Promise<UsageStats> => {
|
getUsageStats: async (userId: string, period?: string): Promise<UsageStats> => {
|
||||||
try {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
const params = period ? { billing_period: period } : {};
|
const params = period ? { billing_period: period } : {};
|
||||||
|
|
||||||
const response = await billingAPI.get<UsageAPIResponse>(`/usage/${actualUserId}`, { params });
|
const response = await billingAPI.get<UsageAPIResponse>(`/usage/${actualUserId}`, { params });
|
||||||
@@ -409,9 +409,9 @@ export const billingService = {
|
|||||||
/**
|
/**
|
||||||
* Get usage trends over time
|
* Get usage trends over time
|
||||||
*/
|
*/
|
||||||
getUsageTrends: async (userId?: string, months: number = 6): Promise<UsageTrends> => {
|
getUsageTrends: async (userId: string, months: number = 6): Promise<UsageTrends> => {
|
||||||
try {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
const response = await billingAPI.get(`/usage/${actualUserId}/trends`, {
|
const response = await billingAPI.get(`/usage/${actualUserId}/trends`, {
|
||||||
params: { months }
|
params: { months }
|
||||||
});
|
});
|
||||||
@@ -469,9 +469,9 @@ export const billingService = {
|
|||||||
/**
|
/**
|
||||||
* Get usage alerts for a user
|
* 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 {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
const response = await billingAPI.get<AlertsAPIResponse>(`/alerts/${actualUserId}`, {
|
const response = await billingAPI.get<AlertsAPIResponse>(`/alerts/${actualUserId}`, {
|
||||||
params: { unread_only: unreadOnly }
|
params: { unread_only: unreadOnly }
|
||||||
});
|
});
|
||||||
@@ -507,9 +507,9 @@ export const billingService = {
|
|||||||
/**
|
/**
|
||||||
* Get user's current subscription information
|
* Get user's current subscription information
|
||||||
*/
|
*/
|
||||||
getUserSubscription: async (userId?: string) => {
|
getUserSubscription: async (userId: string) => {
|
||||||
try {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
const response = await billingAPI.get(`/user/${actualUserId}/subscription`);
|
const response = await billingAPI.get(`/user/${actualUserId}/subscription`);
|
||||||
|
|
||||||
if (!response.data.success) {
|
if (!response.data.success) {
|
||||||
@@ -555,12 +555,12 @@ export const billingService = {
|
|||||||
* Get subscription renewal history for the current user
|
* Get subscription renewal history for the current user
|
||||||
*/
|
*/
|
||||||
getRenewalHistory: async (
|
getRenewalHistory: async (
|
||||||
userId?: string,
|
userId: string,
|
||||||
limit: number = 50,
|
limit: number = 50,
|
||||||
offset: number = 0
|
offset: number = 0
|
||||||
): Promise<RenewalHistoryResponse> => {
|
): Promise<RenewalHistoryResponse> => {
|
||||||
try {
|
try {
|
||||||
const actualUserId = userId || localStorage.getItem('user_id') || 'demo-user';
|
const actualUserId = userId;
|
||||||
const params: any = { limit, offset };
|
const params: any = { limit, offset };
|
||||||
|
|
||||||
const response = await billingAPI.get<RenewalHistoryAPIResponse>(
|
const response = await billingAPI.get<RenewalHistoryAPIResponse>(
|
||||||
|
|||||||
Reference in New Issue
Block a user