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 },