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,7 +3,7 @@
|
||||
* Tabs: Current Plan, Upgrade/Downgrade, Credits Overview, Purchase Credits, Billing History, Payment Methods
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle,
|
||||
Loader2, AlertCircle, CheckCircle, Download
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
type Plan,
|
||||
type Subscription,
|
||||
} from '../../services/billing.api';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
type TabType = 'plan' | 'upgrade' | 'credits' | 'purchase' | 'invoices' | 'payments' | 'payment-methods';
|
||||
|
||||
@@ -67,6 +68,9 @@ export default function PlansAndBillingPage() {
|
||||
display_name: '',
|
||||
instructions: '',
|
||||
});
|
||||
const { user } = useAuthStore.getState();
|
||||
const hasLoaded = useRef(false);
|
||||
const isAwsAdmin = user?.account?.slug === 'aws-admin';
|
||||
const handleBillingError = (err: any, fallback: string) => {
|
||||
const message = err?.message || fallback;
|
||||
setError(message);
|
||||
@@ -76,38 +80,116 @@ export default function PlansAndBillingPage() {
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded.current) return;
|
||||
hasLoaded.current = true;
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
const loadData = async (allowRetry = true) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [balanceData, packagesData, invoicesData, paymentsData, methodsData, plansData, subsData] = await Promise.all([
|
||||
getCreditBalance(),
|
||||
getCreditPackages(),
|
||||
getInvoices({}),
|
||||
getPayments({}),
|
||||
getAvailablePaymentMethods(),
|
||||
getPlans(),
|
||||
getSubscriptions(),
|
||||
// Fetch in controlled sequence to avoid burst 429s on auth/system scopes
|
||||
const balanceData = await getCreditBalance();
|
||||
|
||||
// Small gap between auth endpoints to satisfy tight throttles
|
||||
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
const packagesPromise = getCreditPackages();
|
||||
const invoicesPromise = getInvoices({});
|
||||
const paymentsPromise = getPayments({});
|
||||
const methodsPromise = getAvailablePaymentMethods();
|
||||
|
||||
const plansData = await getPlans();
|
||||
await wait(400);
|
||||
|
||||
// Subscriptions: retry once on 429 after short backoff; do not hard-fail page
|
||||
let subsData: { results: Subscription[] } = { results: [] };
|
||||
try {
|
||||
subsData = await getSubscriptions();
|
||||
} catch (subErr: any) {
|
||||
if (subErr?.status === 429 && allowRetry) {
|
||||
await wait(2500);
|
||||
try {
|
||||
subsData = await getSubscriptions();
|
||||
} catch {
|
||||
subsData = { results: [] };
|
||||
}
|
||||
} else {
|
||||
subsData = { results: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([
|
||||
packagesPromise,
|
||||
invoicesPromise,
|
||||
paymentsPromise,
|
||||
methodsPromise,
|
||||
]);
|
||||
|
||||
setCreditBalance(balanceData);
|
||||
setPackages(packagesData.results || []);
|
||||
setInvoices(invoicesData.results || []);
|
||||
setPayments(paymentsData.results || []);
|
||||
|
||||
// Prefer manual payment method id 14 as default (tenant-facing)
|
||||
const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false);
|
||||
setPaymentMethods(methods);
|
||||
if (methods.length > 0) {
|
||||
const defaultMethod = methods.find((m) => m.is_default);
|
||||
const firstMethod = defaultMethod || methods[0];
|
||||
setSelectedPaymentMethod((prev) => prev || firstMethod.type || firstMethod.id);
|
||||
// Preferred ordering: bank_transfer (default), then manual
|
||||
const bank = methods.find((m) => m.type === 'bank_transfer');
|
||||
const manual = methods.find((m) => m.type === 'manual');
|
||||
const selected =
|
||||
bank ||
|
||||
manual ||
|
||||
methods.find((m) => m.is_default) ||
|
||||
methods[0];
|
||||
setSelectedPaymentMethod((prev) => prev || selected.type || selected.id);
|
||||
}
|
||||
setPlans((plansData.results || []).filter((p) => p.is_active !== false));
|
||||
setSubscriptions(subsData.results || []);
|
||||
|
||||
// Surface all active plans (avoid hiding plans and showing empty state)
|
||||
const activePlans = (plansData.results || []).filter((p) => p.is_active !== false);
|
||||
// Exclude Enterprise plan for non aws-admin accounts
|
||||
const filteredPlans = activePlans.filter((p) => {
|
||||
const name = (p.name || '').toLowerCase();
|
||||
const slug = (p.slug || '').toLowerCase();
|
||||
const isEnterprise = name.includes('enterprise') || slug === 'enterprise';
|
||||
return isAwsAdmin ? true : !isEnterprise;
|
||||
});
|
||||
|
||||
// Ensure the user's assigned plan is included even if subscriptions list is empty
|
||||
const accountPlan = user?.account?.plan;
|
||||
const isAccountEnterprise = (() => {
|
||||
if (!accountPlan) return false;
|
||||
const name = (accountPlan.name || '').toLowerCase();
|
||||
const slug = (accountPlan.slug || '').toLowerCase();
|
||||
return name.includes('enterprise') || slug === 'enterprise';
|
||||
})();
|
||||
|
||||
const shouldIncludeAccountPlan = accountPlan && (!isAccountEnterprise || isAwsAdmin);
|
||||
if (shouldIncludeAccountPlan && !filteredPlans.find((p) => p.id === accountPlan.id)) {
|
||||
filteredPlans.push(accountPlan as any);
|
||||
}
|
||||
setPlans(filteredPlans);
|
||||
const subs = subsData.results || [];
|
||||
if (subs.length === 0 && shouldIncludeAccountPlan && accountPlan) {
|
||||
subs.push({
|
||||
id: accountPlan.id || 0,
|
||||
plan: accountPlan,
|
||||
status: 'active',
|
||||
} as any);
|
||||
}
|
||||
setSubscriptions(subs);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load billing data');
|
||||
console.error('Billing load error:', err);
|
||||
// Handle throttling gracefully: don't block the page on subscriptions throttle
|
||||
if (err?.status === 429 && allowRetry) {
|
||||
setError('Request was throttled. Retrying...');
|
||||
setTimeout(() => loadData(false), 2500);
|
||||
} else if (err?.status === 429) {
|
||||
setError(''); // suppress lingering banner
|
||||
} else {
|
||||
setError(err.message || 'Failed to load billing data');
|
||||
console.error('Billing load error:', err);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -250,6 +332,7 @@ export default function PlansAndBillingPage() {
|
||||
const hasActivePlan = Boolean(currentPlanId);
|
||||
const hasPaymentMethods = paymentMethods.length > 0;
|
||||
const subscriptionStatus = currentSubscription?.status || (hasActivePlan ? 'active' : 'none');
|
||||
const hasPendingManualPayment = payments.some((p) => p.status === 'pending_approval');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'plan' as TabType, label: 'Current Plan', icon: <Package className="w-4 h-4" /> },
|
||||
@@ -270,6 +353,18 @@ export default function PlansAndBillingPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Activation / pending payment notice */}
|
||||
{!hasActivePlan && (
|
||||
<div className="mb-4 p-4 rounded-lg border border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
No active plan. Choose a plan below to activate your account.
|
||||
</div>
|
||||
)}
|
||||
{hasPendingManualPayment && (
|
||||
<div className="mb-4 p-4 rounded-lg border border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-100">
|
||||
We received your manual payment. It’s pending admin approval; activation will complete once approved.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
|
||||
Reference in New Issue
Block a user