diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts index c91b3f26..525abd5d 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts @@ -283,7 +283,7 @@ export class WhmcsHttpClientService { // For successful responses, WHMCS API returns data directly at the root level // 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 { result: "success", data: parsedResponse as T diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts index 095b57ea..66d910f6 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts @@ -57,8 +57,20 @@ export class WhmcsSubscriptionService { 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) { - 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 { subscriptions: [], totalCount: 0, diff --git a/apps/portal/src/components/atoms/LoadingOverlay.tsx b/apps/portal/src/components/atoms/LoadingOverlay.tsx index 592f81d2..2cf93c79 100644 --- a/apps/portal/src/components/atoms/LoadingOverlay.tsx +++ b/apps/portal/src/components/atoms/LoadingOverlay.tsx @@ -29,11 +29,13 @@ export function LoadingOverlay({ return (
-
- +
+
+ +

{title}

{subtitle && ( -

{subtitle}

+

{subtitle}

)}
diff --git a/apps/portal/src/components/organisms/AppShell/AppShell.tsx b/apps/portal/src/components/organisms/AppShell/AppShell.tsx index d7f631cd..94139dd1 100644 --- a/apps/portal/src/components/organisms/AppShell/AppShell.tsx +++ b/apps/portal/src/components/organisms/AppShell/AppShell.tsx @@ -63,10 +63,10 @@ export function AppShell({ children }: AppShellProps) { }, [hasCheckedAuth, checkAuth]); useEffect(() => { - if (hasCheckedAuth && !isAuthenticated) { + if (hasCheckedAuth && !isAuthenticated && !loading) { router.push("/auth/login"); } - }, [hasCheckedAuth, isAuthenticated, router]); + }, [hasCheckedAuth, isAuthenticated, loading, router]); // Hydrate full profile once after auth so header name is consistent across pages useEffect(() => { diff --git a/apps/portal/src/features/auth/services/auth.store.ts b/apps/portal/src/features/auth/services/auth.store.ts index 8e5a14db..86732b59 100644 --- a/apps/portal/src/features/auth/services/auth.store.ts +++ b/apps/portal/src/features/auth/services/auth.store.ts @@ -297,8 +297,12 @@ export const useAuthStore = create()((set, get) => { }, checkAuth: async () => { - set({ hasCheckedAuth: true }); - await get().refreshUser(); + set({ loading: true }); + try { + await get().refreshUser(); + } finally { + set({ hasCheckedAuth: true, loading: false }); + } }, clearError: () => set({ error: null }), diff --git a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts index 9df7ad14..24f491b4 100644 --- a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts +++ b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts @@ -78,7 +78,7 @@ export function useActiveSubscriptions() { return useQuery({ queryKey: queryKeys.subscriptions.active(), queryFn: async () => { - const response = await apiClient.GET("/subscriptions/active"); + const response = await apiClient.GET("/api/subscriptions/active"); return getDataOrDefault(response, []); }, staleTime: 5 * 60 * 1000, @@ -114,7 +114,7 @@ export function useSubscription(subscriptionId: number) { return useQuery({ queryKey: queryKeys.subscriptions.detail(String(subscriptionId)), queryFn: async () => { - const response = await apiClient.GET("/subscriptions/{id}", { + const response = await apiClient.GET("/api/subscriptions/{id}", { params: { path: { id: subscriptionId } }, }); return getDataOrThrow(response, "Subscription not found"); @@ -138,7 +138,7 @@ export function useSubscriptionInvoices( return useQuery({ queryKey: queryKeys.subscriptions.invoices(subscriptionId, { page, limit }), queryFn: async () => { - const response = await apiClient.GET("/subscriptions/{id}/invoices", { + const response = await apiClient.GET("/api/subscriptions/{id}/invoices", { params: { path: { id: subscriptionId }, query: { page, limit },