Phase 3 & Phase 4 - Completed
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user