Enhance billing and subscription management: Added payment method checks in ProtectedRoute, improved error handling in billing components, and optimized API calls to reduce throttling. Updated user account handling in various components to ensure accurate plan and subscription data display.

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-07 10:07:28 +00:00
parent 46fc6dcf04
commit 508b6b4220
26 changed files with 518 additions and 69 deletions

View File

@@ -3,6 +3,7 @@ import { Navigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../../store/authStore";
import { useErrorHandler } from "../../hooks/useErrorHandler";
import { trackLoading } from "../common/LoadingStateMonitor";
import { fetchAPI } from "../../services/api";
interface ProtectedRouteProps {
children: ReactNode;
@@ -18,6 +19,11 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const { addError } = useErrorHandler('ProtectedRoute');
const [showError, setShowError] = useState(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const [paymentCheck, setPaymentCheck] = useState<{
loading: boolean;
hasDefault: boolean;
hasAny: boolean;
}>({ loading: true, hasDefault: false, hasAny: false });
const PLAN_ALLOWED_PATHS = [
'/account/plans',
@@ -38,6 +44,40 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
trackLoading('auth-loading', loading);
}, [loading]);
// Fetch payment methods to confirm default method availability
useEffect(() => {
if (!isAuthenticated) {
setPaymentCheck({ loading: false, hasDefault: false, hasAny: false });
return;
}
let cancelled = false;
const loadPaymentMethods = async () => {
setPaymentCheck((prev) => ({ ...prev, loading: true }));
try {
const data = await fetchAPI('/v1/billing/payment-methods/');
const methods = data?.results || [];
const hasAny = methods.length > 0;
// Treat id 14 as the intended default, or any method marked default
const hasDefault = methods.some((m: any) => m.is_default) || methods.some((m: any) => String(m.id) === '14');
if (!cancelled) {
setPaymentCheck({ loading: false, hasDefault, hasAny });
}
} catch (err) {
if (!cancelled) {
setPaymentCheck({ loading: false, hasDefault: false, hasAny: false });
console.warn('ProtectedRoute: failed to fetch payment methods', err);
}
}
};
loadPaymentMethods();
return () => {
cancelled = true;
};
}, [isAuthenticated]);
// Validate account + plan whenever auth/user changes
useEffect(() => {
if (!isAuthenticated) {
@@ -119,7 +159,22 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) {
}
// If authenticated but missing an active plan, keep user inside billing/onboarding
if (user?.account && !user.account.plan && !isPlanAllowedPath) {
const accountStatus = user?.account?.status;
const accountInactive = accountStatus && ['suspended', 'cancelled'].includes(accountStatus);
const missingPlan = user?.account && !user.account.plan;
const missingPayment = !paymentCheck.loading && (!paymentCheck.hasDefault || !paymentCheck.hasAny);
if ((missingPlan || accountInactive || missingPayment) && !isPlanAllowedPath) {
if (paymentCheck.loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center max-w-md px-4">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mb-4"></div>
<p className="text-lg font-medium text-gray-800 dark:text-white mb-2">Checking billing status...</p>
</div>
</div>
);
}
return <Navigate to="/account/plans" state={{ from: location }} replace />;
}

View File

@@ -53,6 +53,12 @@ export default function SignUpForm() {
// Redirect to plan selection after successful registration
navigate("/account/plans", { replace: true });
// Hard fallback in case navigation is blocked by router state
setTimeout(() => {
if (window.location.pathname !== "/account/plans") {
window.location.assign("/account/plans");
}
}, 500);
} catch (err: any) {
setError(err.message || "Registration failed. Please try again.");
}