fixing and creatign mess

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 10:19:34 +00:00
parent 0386d4bf33
commit ad1756c349
11 changed files with 1067 additions and 188 deletions

View File

@@ -28,10 +28,12 @@ import {
TagIcon,
LockIcon,
ShootingStarIcon,
DollarLineIcon,
} from '../../icons';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useToast } from '../../components/ui/toast/ToastContainer';
@@ -41,7 +43,7 @@ import {
getCreditBalance,
getCreditPackages,
getInvoices,
getAvailablePaymentMethods,
getAccountPaymentMethods,
purchaseCreditPackage,
downloadInvoicePDF,
getPayments,
@@ -64,6 +66,7 @@ import {
type PaymentGateway,
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
import PayInvoiceModal from '../../components/billing/PayInvoiceModal';
export default function PlansAndBillingPage() {
const { startLoading, stopLoading } = usePageLoading();
@@ -80,13 +83,15 @@ export default function PlansAndBillingPage() {
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [selectedBillingCycle, setSelectedBillingCycle] = useState<'monthly' | 'annual'>('monthly');
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
const [showPayInvoiceModal, setShowPayInvoiceModal] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
// Data States
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
const [packages, setPackages] = useState<CreditPackage[]>([]);
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [payments, setPayments] = useState<Payment[]>([]);
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
const [userPaymentMethods, setUserPaymentMethods] = useState<PaymentMethod[]>([]);
const [plans, setPlans] = useState<Plan[]>([]);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string | undefined>(undefined);
@@ -99,16 +104,51 @@ export default function PlansAndBillingPage() {
useEffect(() => {
if (hasLoaded.current) return;
hasLoaded.current = true;
loadData();
// Handle payment gateway return URLs
// Handle payment gateway return URLs BEFORE loadData
const params = new URLSearchParams(window.location.search);
const success = params.get('success');
const canceled = params.get('canceled');
const purchase = params.get('purchase');
const paypalStatus = params.get('paypal');
const paypalToken = params.get('token'); // PayPal order ID
const planIdParam = params.get('plan_id');
const packageIdParam = params.get('package_id');
const { refreshUser } = useAuthStore.getState();
if (success === 'true') {
// Handle PayPal return - MUST capture the order to complete payment
// Do this BEFORE loadData to ensure payment is processed first
if (paypalStatus === 'success' && paypalToken) {
// Import and capture PayPal order
import('../../services/billing.api').then(({ capturePayPalOrder }) => {
toast?.info?.('Completing PayPal payment...');
capturePayPalOrder(paypalToken, {
plan_id: planIdParam || undefined,
package_id: packageIdParam || undefined,
})
.then(() => {
toast?.success?.('Payment completed successfully!');
refreshUser().catch(() => {});
// Reload the page to get fresh data
window.history.replaceState({}, '', window.location.pathname);
window.location.reload();
})
.catch((err) => {
console.error('PayPal capture error:', err);
toast?.error?.(err?.message || 'Failed to complete PayPal payment');
window.history.replaceState({}, '', window.location.pathname);
});
});
return; // Don't load data yet, wait for capture to complete
} else if (paypalStatus === 'cancel') {
toast?.info?.('PayPal payment was cancelled');
window.history.replaceState({}, '', window.location.pathname);
}
// Handle Stripe success
else if (success === 'true') {
toast?.success?.('Subscription activated successfully!');
// Refresh user to get updated account status (removes pending_payment banner)
refreshUser().catch(() => {});
// Clean up URL
window.history.replaceState({}, '', window.location.pathname);
} else if (canceled === 'true') {
@@ -116,11 +156,16 @@ export default function PlansAndBillingPage() {
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'success') {
toast?.success?.('Credits purchased successfully!');
// Refresh user to get updated credit balance and account status
refreshUser().catch(() => {});
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'canceled') {
toast?.info?.('Credit purchase was cancelled');
window.history.replaceState({}, '', window.location.pathname);
}
// Load data after handling return URLs
loadData();
}, []);
const handleError = (err: any, fallback: string) => {
@@ -138,7 +183,7 @@ export default function PlansAndBillingPage() {
const packagesPromise = getCreditPackages();
const invoicesPromise = getInvoices({});
const paymentsPromise = getPayments({});
const methodsPromise = getAvailablePaymentMethods();
const userMethodsPromise = getAccountPaymentMethods();
const plansData = await getPlans();
await wait(400);
@@ -152,8 +197,8 @@ export default function PlansAndBillingPage() {
}
}
const [packagesData, invoicesData, paymentsData, methodsData] = await Promise.all([
packagesPromise, invoicesPromise, paymentsPromise, methodsPromise
const [packagesData, invoicesData, paymentsData, userMethodsData] = await Promise.all([
packagesPromise, invoicesPromise, paymentsPromise, userMethodsPromise
]);
setCreditBalance(balanceData);
@@ -161,13 +206,19 @@ export default function PlansAndBillingPage() {
setInvoices(invoicesData.results || []);
setPayments(paymentsData.results || []);
const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false);
setPaymentMethods(methods);
if (methods.length > 0) {
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);
// Load user's verified payment methods (AccountPaymentMethod)
const userMethods = (userMethodsData.results || []).filter((m: any) => m.is_enabled !== false);
setUserPaymentMethods(userMethods);
// Select the user's default/verified payment method
if (userMethods.length > 0) {
const defaultMethod = userMethods.find((m: any) => m.is_default && m.is_verified) ||
userMethods.find((m: any) => m.is_verified) ||
userMethods.find((m: any) => m.is_default) ||
userMethods[0];
if (defaultMethod) {
setSelectedPaymentMethod(defaultMethod.type);
}
}
// Filter plans
@@ -191,12 +242,32 @@ export default function PlansAndBillingPage() {
}
setSubscriptions(subs);
// Load available payment gateways
// Load available payment gateways and sync with user's payment method
try {
const gateways = await getAvailablePaymentGateways();
setAvailableGateways(gateways);
// Auto-select first available gateway
if (gateways.stripe) {
// Use user's verified payment method to set gateway
// userMethods was already loaded above
const verifiedMethod = userMethods.find(
(m: any) => m.is_verified && m.is_default
) || userMethods.find((m: any) => m.is_verified) || userMethods[0];
let userPreferredGateway: PaymentGateway | null = null;
if (verifiedMethod) {
if (verifiedMethod.type === 'stripe' && gateways.stripe) {
userPreferredGateway = 'stripe';
} else if (verifiedMethod.type === 'paypal' && gateways.paypal) {
userPreferredGateway = 'paypal';
} else if (['bank_transfer', 'local_wallet', 'manual'].includes(verifiedMethod.type)) {
userPreferredGateway = 'manual';
}
}
// Set selected gateway based on user preference or available gateways
if (userPreferredGateway) {
setSelectedGateway(userPreferredGateway);
} else if (gateways.stripe) {
setSelectedGateway('stripe');
} else if (gateways.paypal) {
setSelectedGateway('paypal');
@@ -312,6 +383,10 @@ export default function PlansAndBillingPage() {
const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan;
const hasActivePlan = Boolean(effectivePlanId);
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
// Combined check: disable Buy Credits if no active plan OR has pending invoice
const canBuyCredits = hasActivePlan && !hasPendingInvoice;
// Credit usage percentage
const creditUsage = creditBalance && creditBalance.plan_credits_per_month > 0
@@ -374,14 +449,14 @@ export default function PlansAndBillingPage() {
{/* SECTION 1: Current Plan Hero */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Plan Card */}
<Card className="lg:col-span-2 p-6 bg-gradient-to-br from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border-0">
<Card className="lg:col-span-2 p-6 bg-gradient-to-br from-brand-500/10 via-purple-500/10 to-indigo-500/10 dark:from-brand-600/20 dark:via-purple-600/20 dark:to-indigo-600/20 border border-brand-200/50 dark:border-brand-700/50">
<div className="flex items-start justify-between mb-6">
<div>
<div className="flex items-center gap-3 mb-2">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{currentPlan?.name || 'No Plan'}
</h2>
<Badge variant="soft" tone={hasActivePlan ? 'success' : 'warning'}>
<Badge variant="solid" tone={hasActivePlan ? 'success' : 'warning'}>
{hasActivePlan ? 'Active' : 'Inactive'}
</Badge>
</div>
@@ -546,71 +621,96 @@ export default function PlansAndBillingPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Buy Additional Credits */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<PlusIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">Buy Credits</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">Top up your credit balance</p>
</div>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<PlusIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
</div>
{/* Compact Payment Gateway Selector for Credits */}
{(availableGateways.stripe || availableGateways.paypal) && (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
{availableGateways.stripe && (
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">Buy Credits</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">Top up your credit balance</p>
</div>
</div>
{/* Show message if no active plan */}
{!hasActivePlan ? (
<div className="text-center py-8 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl">
<LockIcon className="w-8 h-8 mx-auto text-gray-400 mb-2" />
<p className="text-gray-500 dark:text-gray-400 text-sm">
Subscribe to a plan first to purchase additional credits
</p>
</div>
) : hasPendingInvoice ? (
<div className="text-center py-8 border-2 border-dashed border-warning-200 dark:border-warning-700 bg-warning-50 dark:bg-warning-900/20 rounded-xl">
<AlertCircleIcon className="w-8 h-8 mx-auto text-warning-500 mb-2" />
<p className="text-warning-700 dark:text-warning-300 text-sm font-medium">
Please pay your pending invoice first
</p>
<p className="text-warning-600 dark:text-warning-400 text-xs mt-1">
Credit purchases are disabled until your outstanding balance is settled
</p>
</div>
) : (
<>
{/* Payment Method Selector - Clear buttons */}
{(availableGateways.stripe || availableGateways.paypal) && (
<div className="mb-4">
<Label className="text-xs text-gray-500 dark:text-gray-400 mb-2 block">Payment Method</Label>
<div className="flex gap-2">
{availableGateways.stripe && (
<button
onClick={() => setSelectedGateway('stripe')}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
selectedGateway === 'stripe'
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<CreditCardIcon className={`w-5 h-5 ${selectedGateway === 'stripe' ? 'text-brand-600' : 'text-gray-500'}`} />
<span className="text-sm font-medium">Credit/Debit Card</span>
</button>
)}
{availableGateways.paypal && (
<button
onClick={() => setSelectedGateway('paypal')}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
selectedGateway === 'paypal'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<WalletIcon className={`w-5 h-5 ${selectedGateway === 'paypal' ? 'text-blue-600' : 'text-gray-500'}`} />
<span className="text-sm font-medium">PayPal</span>
</button>
)}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3">
{packages.slice(0, 4).map((pkg) => (
<button
onClick={() => setSelectedGateway('stripe')}
className={`p-1.5 rounded-md transition-colors ${
selectedGateway === 'stripe'
? 'bg-white dark:bg-gray-700 shadow-sm'
: 'hover:bg-white/50 dark:hover:bg-gray-700/50'
}`}
title="Pay with Card"
key={pkg.id}
onClick={() => handlePurchaseCredits(pkg.id)}
disabled={purchaseLoadingId === pkg.id}
className="p-4 border border-gray-200 dark:border-gray-700 rounded-xl hover:border-brand-500 dark:hover:border-brand-500 hover:bg-brand-50/50 dark:hover:bg-brand-900/20 transition-all text-left group"
>
<CreditCardIcon className={`w-4 h-4 ${selectedGateway === 'stripe' ? 'text-brand-600' : 'text-gray-500'}`} />
<div className="flex items-center justify-between mb-2">
<span className="text-lg font-bold text-gray-900 dark:text-white">
{pkg.credits.toLocaleString()}
</span>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">credits</div>
<div className="font-semibold text-brand-600 dark:text-brand-400 group-hover:text-brand-700 dark:group-hover:text-brand-300">
${pkg.price}
</div>
{purchaseLoadingId === pkg.id && (
<Loader2Icon className="w-4 h-4 animate-spin mt-2" />
)}
</button>
)}
{availableGateways.paypal && (
<button
onClick={() => setSelectedGateway('paypal')}
className={`p-1.5 rounded-md transition-colors ${
selectedGateway === 'paypal'
? 'bg-white dark:bg-gray-700 shadow-sm'
: 'hover:bg-white/50 dark:hover:bg-gray-700/50'
}`}
title="Pay with PayPal"
>
<WalletIcon className={`w-4 h-4 ${selectedGateway === 'paypal' ? 'text-blue-600' : 'text-gray-500'}`} />
</button>
)}
))}
</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
{packages.slice(0, 4).map((pkg) => (
<button
key={pkg.id}
onClick={() => handlePurchaseCredits(pkg.id)}
disabled={purchaseLoadingId === pkg.id}
className="p-4 border border-gray-200 dark:border-gray-700 rounded-xl hover:border-brand-500 dark:hover:border-brand-500 hover:bg-brand-50/50 dark:hover:bg-brand-900/20 transition-all text-left group"
>
<div className="flex items-center justify-between mb-2">
<span className="text-lg font-bold text-gray-900 dark:text-white">
{pkg.credits.toLocaleString()}
</span>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">credits</div>
<div className="font-semibold text-brand-600 dark:text-brand-400 group-hover:text-brand-700 dark:group-hover:text-brand-300">
${pkg.price}
</div>
{purchaseLoadingId === pkg.id && (
<Loader2Icon className="w-4 h-4 animate-spin mt-2" />
)}
</button>
))}
</div>
</>
)}
</Card>
{/* Quick Upgrade Options */}
@@ -719,15 +819,31 @@ export default function PlansAndBillingPage() {
</Badge>
</td>
<td className="px-6 py-3 text-end">
<Button
size="sm"
variant="ghost"
tone="neutral"
startIcon={<DownloadIcon className="w-4 h-4" />}
onClick={() => handleDownloadInvoice(invoice.id)}
>
PDF
</Button>
<div className="flex items-center justify-end gap-2">
{invoice.status === 'pending' && (
<Button
size="sm"
variant="primary"
tone="brand"
startIcon={<DollarLineIcon className="w-4 h-4" />}
onClick={() => {
setSelectedInvoice(invoice);
setShowPayInvoiceModal(true);
}}
>
Pay Now
</Button>
)}
<Button
size="sm"
variant="ghost"
tone="neutral"
startIcon={<DownloadIcon className="w-4 h-4" />}
onClick={() => handleDownloadInvoice(invoice.id)}
>
PDF
</Button>
</div>
</td>
</tr>
))
@@ -737,7 +853,7 @@ export default function PlansAndBillingPage() {
</div>
</Card>
{/* SECTION 4: Payment Methods */}
{/* SECTION 4: Payment Methods - User's verified payment methods */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
@@ -746,12 +862,12 @@ export default function PlansAndBillingPage() {
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">Payment Methods</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">Manage how you pay</p>
<p className="text-xs text-gray-500 dark:text-gray-400">Your saved payment methods</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{paymentMethods.map((method) => (
{userPaymentMethods.map((method: any) => (
<div
key={method.id}
className={`p-4 border rounded-xl transition-all ${
@@ -762,32 +878,48 @@ export default function PlansAndBillingPage() {
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
{method.type === 'bank_transfer' ? (
{method.type === 'bank_transfer' || method.type === 'local_wallet' ? (
<Building2Icon className="w-6 h-6 text-gray-500" />
) : method.type === 'paypal' ? (
<WalletIcon className="w-6 h-6 text-blue-500" />
) : (
<CreditCardIcon className="w-6 h-6 text-gray-500" />
<CreditCardIcon className="w-6 h-6 text-brand-500" />
)}
<div>
<div className="font-medium text-gray-900 dark:text-white">{method.display_name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 capitalize">{method.type?.replace('_', ' ')}</div>
</div>
</div>
{method.is_default && (
<Badge variant="soft" tone="success" size="sm">Default</Badge>
)}
<div className="flex flex-col gap-1 items-end">
{method.is_verified && (
<Badge variant="soft" tone="success" size="sm">Verified</Badge>
)}
{method.is_default && (
<Badge variant="soft" tone="brand" size="sm">Default</Badge>
)}
</div>
</div>
{selectedPaymentMethod !== method.type && (
{selectedPaymentMethod !== method.type ? (
<Button
size="sm"
variant="outline"
tone="neutral"
className="w-full"
onClick={() => setSelectedPaymentMethod(method.type)}
onClick={() => {
setSelectedPaymentMethod(method.type);
// Sync gateway selection
if (method.type === 'stripe' && availableGateways.stripe) {
setSelectedGateway('stripe');
} else if (method.type === 'paypal' && availableGateways.paypal) {
setSelectedGateway('paypal');
} else {
setSelectedGateway('manual');
}
}}
>
Select
</Button>
)}
{selectedPaymentMethod === method.type && (
) : (
<div className="flex items-center gap-2 text-sm text-brand-600 dark:text-brand-400">
<CheckCircleIcon className="w-4 h-4" />
Selected for payment
@@ -795,9 +927,11 @@ export default function PlansAndBillingPage() {
)}
</div>
))}
{paymentMethods.length === 0 && (
{userPaymentMethods.length === 0 && (
<div className="col-span-full text-center py-8 text-gray-500 dark:text-gray-400">
No payment methods available
<CreditCardIcon className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>No payment methods saved yet.</p>
<p className="text-xs mt-1">Complete a payment to save your method.</p>
</div>
)}
</div>
@@ -1034,6 +1168,29 @@ export default function PlansAndBillingPage() {
</div>
</div>
)}
{/* Pay Invoice Modal */}
{showPayInvoiceModal && selectedInvoice && (
<PayInvoiceModal
isOpen={showPayInvoiceModal}
onClose={() => {
setShowPayInvoiceModal(false);
setSelectedInvoice(null);
}}
onSuccess={async () => {
setShowPayInvoiceModal(false);
setSelectedInvoice(null);
// Refresh user and billing data
const { refreshUser } = useAuthStore.getState();
await refreshUser();
await loadData();
toast?.success?.('Payment processed successfully!');
}}
invoice={selectedInvoice}
userCountry={(user?.account as any)?.billing_country || 'US'}
defaultPaymentMethod={selectedPaymentMethod}
/>
)}
</>
);
}