Phase 3 & Phase 4 - Completed

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 00:57:26 +00:00
parent 4b6a03a898
commit 909ed1cb17
25 changed files with 5549 additions and 215 deletions

View File

@@ -8,7 +8,6 @@ import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import {
CreditCardIcon,
BoxIcon as PackageIcon,
TrendingUpIcon,
FileTextIcon,
WalletIcon,
@@ -56,6 +55,12 @@ import {
cancelSubscription,
type Plan,
type Subscription,
// Payment gateway methods
subscribeToPlan,
purchaseCredits,
openStripeBillingPortal,
getAvailablePaymentGateways,
type PaymentGateway,
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
@@ -73,6 +78,7 @@ export default function PlansAndBillingPage() {
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [selectedBillingCycle, setSelectedBillingCycle] = useState<'monthly' | 'annual'>('monthly');
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
// Data States
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
@@ -83,11 +89,37 @@ export default function PlansAndBillingPage() {
const [plans, setPlans] = useState<Plan[]>([]);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string | undefined>(undefined);
const [availableGateways, setAvailableGateways] = useState<{ stripe: boolean; paypal: boolean; manual: boolean }>({
stripe: false,
paypal: false,
manual: true,
});
useEffect(() => {
if (hasLoaded.current) return;
hasLoaded.current = true;
loadData();
// Handle payment gateway return URLs
const params = new URLSearchParams(window.location.search);
const success = params.get('success');
const canceled = params.get('canceled');
const purchase = params.get('purchase');
if (success === 'true') {
toast?.success?.('Subscription activated successfully!');
// Clean up URL
window.history.replaceState({}, '', window.location.pathname);
} else if (canceled === 'true') {
toast?.info?.('Payment was cancelled');
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'success') {
toast?.success?.('Credits purchased successfully!');
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'canceled') {
toast?.info?.('Credit purchase was cancelled');
window.history.replaceState({}, '', window.location.pathname);
}
}, []);
const handleError = (err: any, fallback: string) => {
@@ -157,6 +189,23 @@ export default function PlansAndBillingPage() {
subs.push({ id: accountPlan.id || 0, plan: accountPlan, status: 'active' } as any);
}
setSubscriptions(subs);
// Load available payment gateways
try {
const gateways = await getAvailablePaymentGateways();
setAvailableGateways(gateways);
// Auto-select first available gateway
if (gateways.stripe) {
setSelectedGateway('stripe');
} else if (gateways.paypal) {
setSelectedGateway('paypal');
} else {
setSelectedGateway('manual');
}
} catch {
// Non-critical - just keep defaults
console.log('Could not load payment gateways, using defaults');
}
} catch (err: any) {
if (err?.status === 429 && allowRetry) {
setError('Request was throttled. Retrying...');
@@ -172,6 +221,15 @@ export default function PlansAndBillingPage() {
const handleSelectPlan = async (planId: number) => {
try {
setPlanLoadingId(planId);
// Use payment gateway integration for Stripe/PayPal
if (selectedGateway === 'stripe' || selectedGateway === 'paypal') {
const { redirect_url } = await subscribeToPlan(planId.toString(), selectedGateway);
window.location.href = redirect_url;
return;
}
// Fall back to manual/bank transfer flow
await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
toast?.success?.('Plan upgraded successfully!');
setShowUpgradeModal(false);
@@ -201,7 +259,16 @@ export default function PlansAndBillingPage() {
const handlePurchaseCredits = async (packageId: number) => {
try {
setPurchaseLoadingId(packageId);
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'stripe' });
// Use payment gateway integration for Stripe/PayPal
if (selectedGateway === 'stripe' || selectedGateway === 'paypal') {
const { redirect_url } = await purchaseCredits(packageId.toString(), selectedGateway);
window.location.href = redirect_url;
return;
}
// Fall back to manual/bank transfer flow
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'manual' });
toast?.success?.('Credits purchased successfully!');
await loadData();
} catch (err: any) {
@@ -211,6 +278,15 @@ export default function PlansAndBillingPage() {
}
};
const handleManageSubscription = async () => {
try {
const { portal_url } = await openStripeBillingPortal();
window.location.href = portal_url;
} catch (err: any) {
handleError(err, 'Failed to open billing portal');
}
};
const handleDownloadInvoice = async (invoiceId: number) => {
try {
const blob = await downloadInvoicePDF(invoiceId);
@@ -312,14 +388,26 @@ export default function PlansAndBillingPage() {
{currentPlan?.description || 'Select a plan to unlock features'}
</p>
</div>
<Button
variant="primary"
tone="brand"
onClick={() => setShowUpgradeModal(true)}
startIcon={<ArrowUpIcon className="w-4 h-4" />}
>
Upgrade
</Button>
<div className="flex items-center gap-2">
{availableGateways.stripe && hasActivePlan && (
<Button
variant="outline"
tone="neutral"
onClick={handleManageSubscription}
startIcon={<CreditCardIcon className="w-4 h-4" />}
>
Manage Billing
</Button>
)}
<Button
variant="primary"
tone="brand"
onClick={() => setShowUpgradeModal(true)}
startIcon={<ArrowUpIcon className="w-4 h-4" />}
>
Upgrade
</Button>
</div>
</div>
{/* Quick Stats */}
@@ -467,6 +555,37 @@ export default function PlansAndBillingPage() {
<p className="text-xs text-gray-500 dark:text-gray-400">Top up your credit balance</p>
</div>
</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 && (
<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"
>
<CreditCardIcon className={`w-4 h-4 ${selectedGateway === 'stripe' ? 'text-brand-600' : 'text-gray-500'}`} />
</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) => (
@@ -701,8 +820,9 @@ export default function PlansAndBillingPage() {
</button>
</div>
{/* Billing Toggle */}
<div className="flex justify-center py-6">
{/* Billing Toggle & Payment Gateway */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 py-6">
{/* Billing Cycle Toggle */}
<div className="bg-gray-100 dark:bg-gray-800 p-1 rounded-lg flex gap-1">
<button
onClick={() => setSelectedBillingCycle('monthly')}
@@ -726,6 +846,51 @@ export default function PlansAndBillingPage() {
<Badge variant="soft" tone="success" size="sm">Save 20%</Badge>
</button>
</div>
{/* Payment Gateway Selector */}
{(availableGateways.stripe || availableGateways.paypal) && (
<div className="bg-gray-100 dark:bg-gray-800 p-1 rounded-lg flex gap-1">
{availableGateways.stripe && (
<button
onClick={() => setSelectedGateway('stripe')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'stripe'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<CreditCardIcon className="w-4 h-4" />
Card
</button>
)}
{availableGateways.paypal && (
<button
onClick={() => setSelectedGateway('paypal')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'paypal'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<WalletIcon className="w-4 h-4" />
PayPal
</button>
)}
{availableGateways.manual && (
<button
onClick={() => setSelectedGateway('manual')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'manual'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<Building2Icon className="w-4 h-4" />
Bank
</button>
)}
</div>
)}
</div>
{/* Plans Grid */}