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
// 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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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