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:
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import Badge from '../../components/ui/badge/Badge';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import { DollarLineIcon } from '../../icons';
|
||||
import { useBillingStore } from '../../store/billingStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
export default function BillingBalancePanel() {
|
||||
const { balance, loading, error, loadBalance } = useBillingStore();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadBalance();
|
||||
@@ -64,7 +66,7 @@ export default function BillingBalancePanel() {
|
||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">Subscription Plan</h3>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{(balance as any)?.subscription_plan || 'None'}
|
||||
(balance as any)?.subscription_plan || user?.account?.plan?.name || 'None'
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
{(balance?.plan_credits_per_month ?? 0) ? `${(balance?.plan_credits_per_month ?? 0).toLocaleString()} credits/month` : 'No subscription'}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactNode, useEffect } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { isModuleEnabled } from '../../config/modules.config';
|
||||
import { isUpgradeError } from '../../utils/upgrade';
|
||||
|
||||
interface ModuleGuardProps {
|
||||
module: string;
|
||||
|
||||
@@ -21,7 +21,9 @@ export default function CreditBalanceWidget() {
|
||||
if (error && !balance) {
|
||||
return (
|
||||
<ComponentCard title="Credit Balance" desc="Balance unavailable">
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mb-3">{error}</div>
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mb-3">
|
||||
Balance unavailable. Please retry.
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadBalance}>
|
||||
Retry
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user