Phase 3 & Phase 4 - Completed
This commit is contained in:
192
frontend/src/components/billing/PaymentGatewaySelector.tsx
Normal file
192
frontend/src/components/billing/PaymentGatewaySelector.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* PaymentGatewaySelector Component
|
||||
* Allows users to select between Stripe, PayPal, and Manual payment methods
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CreditCard, Building2, Wallet, Loader2, Check } from 'lucide-react';
|
||||
import { getAvailablePaymentGateways, PaymentGateway } from '@/services/billing.api';
|
||||
|
||||
interface PaymentGatewayOption {
|
||||
id: PaymentGateway;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
available: boolean;
|
||||
recommended?: boolean;
|
||||
}
|
||||
|
||||
interface PaymentGatewaySelectorProps {
|
||||
selectedGateway: PaymentGateway | null;
|
||||
onSelectGateway: (gateway: PaymentGateway) => void;
|
||||
showManual?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PaymentGatewaySelector({
|
||||
selectedGateway,
|
||||
onSelectGateway,
|
||||
showManual = true,
|
||||
className = '',
|
||||
}: PaymentGatewaySelectorProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [gateways, setGateways] = useState<PaymentGatewayOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadGateways() {
|
||||
try {
|
||||
const available = await getAvailablePaymentGateways();
|
||||
|
||||
const options: PaymentGatewayOption[] = [
|
||||
{
|
||||
id: 'stripe',
|
||||
name: 'Credit/Debit Card',
|
||||
description: 'Pay securely with your credit or debit card via Stripe',
|
||||
icon: <CreditCard className="h-6 w-6" />,
|
||||
available: available.stripe,
|
||||
recommended: available.stripe,
|
||||
},
|
||||
{
|
||||
id: 'paypal',
|
||||
name: 'PayPal',
|
||||
description: 'Pay with your PayPal account or PayPal Credit',
|
||||
icon: (
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797H9.3L7.076 21.337z" />
|
||||
</svg>
|
||||
),
|
||||
available: available.paypal,
|
||||
},
|
||||
];
|
||||
|
||||
if (showManual) {
|
||||
options.push({
|
||||
id: 'manual',
|
||||
name: 'Bank Transfer',
|
||||
description: 'Pay via bank transfer with manual confirmation',
|
||||
icon: <Building2 className="h-6 w-6" />,
|
||||
available: available.manual,
|
||||
});
|
||||
}
|
||||
|
||||
setGateways(options);
|
||||
|
||||
// Auto-select first available gateway if none selected
|
||||
if (!selectedGateway) {
|
||||
const firstAvailable = options.find((g) => g.available);
|
||||
if (firstAvailable) {
|
||||
onSelectGateway(firstAvailable.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load payment gateways:', error);
|
||||
// Fallback to manual only
|
||||
setGateways([
|
||||
{
|
||||
id: 'manual',
|
||||
name: 'Bank Transfer',
|
||||
description: 'Pay via bank transfer with manual confirmation',
|
||||
icon: <Building2 className="h-6 w-6" />,
|
||||
available: true,
|
||||
},
|
||||
]);
|
||||
if (!selectedGateway) {
|
||||
onSelectGateway('manual');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadGateways();
|
||||
}, [selectedGateway, onSelectGateway, showManual]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center py-8 ${className}`}>
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-gray-500">Loading payment options...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const availableGateways = gateways.filter((g) => g.available);
|
||||
|
||||
if (availableGateways.length === 0) {
|
||||
return (
|
||||
<div className={`rounded-lg border border-yellow-200 bg-yellow-50 p-4 ${className}`}>
|
||||
<p className="text-sm text-yellow-800">
|
||||
No payment methods are currently available. Please contact support.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Select Payment Method
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{availableGateways.map((gateway) => (
|
||||
<button
|
||||
key={gateway.id}
|
||||
type="button"
|
||||
onClick={() => onSelectGateway(gateway.id)}
|
||||
className={`relative flex items-start rounded-lg border-2 p-4 text-left transition-all ${
|
||||
selectedGateway === gateway.id
|
||||
? 'border-indigo-600 bg-indigo-50 ring-1 ring-indigo-600'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full ${
|
||||
selectedGateway === gateway.id
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{gateway.icon}
|
||||
</div>
|
||||
<div className="ml-4 flex-1">
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={`font-medium ${
|
||||
selectedGateway === gateway.id ? 'text-indigo-900' : 'text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{gateway.name}
|
||||
</span>
|
||||
{gateway.recommended && (
|
||||
<span className="ml-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
Recommended
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className={`mt-1 text-sm ${
|
||||
selectedGateway === gateway.id ? 'text-indigo-700' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{gateway.description}
|
||||
</p>
|
||||
</div>
|
||||
{selectedGateway === gateway.id && (
|
||||
<div className="absolute right-4 top-4">
|
||||
<Check className="h-5 w-5 text-indigo-600" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{selectedGateway === 'manual' && (
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
After submitting, you'll receive bank details to complete the transfer.
|
||||
Your account will be activated once we confirm the payment (usually within 1-2 business days).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PaymentGatewaySelector;
|
||||
Reference in New Issue
Block a user