Enhance WHMCS API response handling with improved logging and response structure validation. Update LoadingOverlay component for better layout and spacing. Refactor AppShell to manage authentication checks more effectively, ensuring smoother user experience during redirects. Adjust API paths in subscription hooks for consistency and clarity.

This commit is contained in:
barsa 2025-09-27 18:28:35 +09:00
parent 6390749150
commit 50d8fdfdd1
6 changed files with 30 additions and 12 deletions

View File

@ -283,7 +283,7 @@ export class WhmcsHttpClientService {
// For successful responses, WHMCS API returns data directly at the root level // For successful responses, WHMCS API returns data directly at the root level
// The response structure is: { "result": "success", ...actualData } // The response structure is: { "result": "success", ...actualData }
// We need to wrap this in our expected format // We return the parsed response directly as T since it contains the actual data
return { return {
result: "success", result: "success",
data: parsedResponse as T data: parsedResponse as T

View File

@ -57,8 +57,20 @@ export class WhmcsSubscriptionService {
const response = await this.connectionService.getClientsProducts(params); const response = await this.connectionService.getClientsProducts(params);
// Debug logging to understand the response structure
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
hasResponse: !!response,
responseKeys: response ? Object.keys(response) : [],
hasProducts: !!(response as any)?.products,
productsKeys: (response as any)?.products ? Object.keys((response as any).products) : [],
hasProductArray: Array.isArray((response as any)?.products?.product),
productCount: Array.isArray((response as any)?.products?.product) ? (response as any).products.product.length : 0,
});
if (!response.products?.product) { if (!response.products?.product) {
this.logger.warn(`No products found for client ${clientId}`); this.logger.warn(`No products found for client ${clientId}`, {
responseStructure: response ? Object.keys(response) : 'null response',
});
return { return {
subscriptions: [], subscriptions: [],
totalCount: 0, totalCount: 0,

View File

@ -29,11 +29,13 @@ export function LoadingOverlay({
return ( return (
<div className={`fixed inset-0 z-50 flex items-center justify-center ${overlayClassName}`}> <div className={`fixed inset-0 z-50 flex items-center justify-center ${overlayClassName}`}>
<div className="text-center"> <div className="text-center max-w-sm mx-auto px-4">
<Spinner size={spinnerSize} className={`mb-4 ${spinnerClassName}`} /> <div className="flex justify-center mb-6">
<Spinner size={spinnerSize} className={spinnerClassName} />
</div>
<p className="text-lg font-medium text-gray-900">{title}</p> <p className="text-lg font-medium text-gray-900">{title}</p>
{subtitle && ( {subtitle && (
<p className="text-sm text-gray-600 mt-1">{subtitle}</p> <p className="text-sm text-gray-600 mt-2">{subtitle}</p>
)} )}
</div> </div>
</div> </div>

View File

@ -63,10 +63,10 @@ export function AppShell({ children }: AppShellProps) {
}, [hasCheckedAuth, checkAuth]); }, [hasCheckedAuth, checkAuth]);
useEffect(() => { useEffect(() => {
if (hasCheckedAuth && !isAuthenticated) { if (hasCheckedAuth && !isAuthenticated && !loading) {
router.push("/auth/login"); router.push("/auth/login");
} }
}, [hasCheckedAuth, isAuthenticated, router]); }, [hasCheckedAuth, isAuthenticated, loading, router]);
// Hydrate full profile once after auth so header name is consistent across pages // Hydrate full profile once after auth so header name is consistent across pages
useEffect(() => { useEffect(() => {

View File

@ -297,8 +297,12 @@ export const useAuthStore = create<AuthState>()((set, get) => {
}, },
checkAuth: async () => { checkAuth: async () => {
set({ hasCheckedAuth: true }); set({ loading: true });
await get().refreshUser(); try {
await get().refreshUser();
} finally {
set({ hasCheckedAuth: true, loading: false });
}
}, },
clearError: () => set({ error: null }), clearError: () => set({ error: null }),

View File

@ -78,7 +78,7 @@ export function useActiveSubscriptions() {
return useQuery<Subscription[]>({ return useQuery<Subscription[]>({
queryKey: queryKeys.subscriptions.active(), queryKey: queryKeys.subscriptions.active(),
queryFn: async () => { queryFn: async () => {
const response = await apiClient.GET<Subscription[]>("/subscriptions/active"); const response = await apiClient.GET<Subscription[]>("/api/subscriptions/active");
return getDataOrDefault<Subscription[]>(response, []); return getDataOrDefault<Subscription[]>(response, []);
}, },
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
@ -114,7 +114,7 @@ export function useSubscription(subscriptionId: number) {
return useQuery<Subscription>({ return useQuery<Subscription>({
queryKey: queryKeys.subscriptions.detail(String(subscriptionId)), queryKey: queryKeys.subscriptions.detail(String(subscriptionId)),
queryFn: async () => { queryFn: async () => {
const response = await apiClient.GET<Subscription>("/subscriptions/{id}", { const response = await apiClient.GET<Subscription>("/api/subscriptions/{id}", {
params: { path: { id: subscriptionId } }, params: { path: { id: subscriptionId } },
}); });
return getDataOrThrow<Subscription>(response, "Subscription not found"); return getDataOrThrow<Subscription>(response, "Subscription not found");
@ -138,7 +138,7 @@ export function useSubscriptionInvoices(
return useQuery<InvoiceList>({ return useQuery<InvoiceList>({
queryKey: queryKeys.subscriptions.invoices(subscriptionId, { page, limit }), queryKey: queryKeys.subscriptions.invoices(subscriptionId, { page, limit }),
queryFn: async () => { queryFn: async () => {
const response = await apiClient.GET<InvoiceList>("/subscriptions/{id}/invoices", { const response = await apiClient.GET<InvoiceList>("/api/subscriptions/{id}/invoices", {
params: { params: {
path: { id: subscriptionId }, path: { id: subscriptionId },
query: { page, limit }, query: { page, limit },