trading-platform-frontend-v2/src/components/payments/AlternativePaymentMethods.tsx
Adrian Flores Cortes c9e2727d3b [SPRINT-3] feat(investment,payments): Dashboard, KYC, transactions and alt payments
SUBTASK-005 (Investment):
- Rewrite Investment.tsx with summary cards and performance chart
- Add pagination to Transactions.tsx (10 items per page)
- Add PDF/CSV export dropdown to Reports.tsx
- Fix quick amount buttons in DepositForm.tsx
- Fix Max button in WithdrawForm.tsx
- Add full KYC verification system (3 steps)
- Add KYCVerification page with route /investment/kyc

SUBTASK-006 (Payments):
- Add AlternativePaymentMethods component (OXXO, SPEI, Card)
- Extend payment types for regional methods
- Update PaymentMethodsList exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 00:17:46 -06:00

302 lines
10 KiB
TypeScript

/**
* AlternativePaymentMethods Component
* Display and select alternative payment methods (OXXO, SPEI, etc.)
* Epic: OQI-005 Pagos y Stripe
*/
import React, { useState } from 'react';
import {
Store,
Building2,
CreditCard,
ChevronRight,
Info,
CheckCircle,
Clock,
AlertCircle,
Loader2,
} from 'lucide-react';
import type { PaymentMethodType } from '../../types/payment.types';
export interface AlternativePaymentMethod {
id: PaymentMethodType;
name: string;
description: string;
icon: React.ReactNode;
processingTime: string;
available: boolean;
minAmount?: number;
maxAmount?: number;
fees?: string;
instructions?: string[];
regions?: string[];
}
const ALTERNATIVE_METHODS: AlternativePaymentMethod[] = [
{
id: 'oxxo',
name: 'OXXO Pay',
description: 'Pay with cash at any OXXO convenience store',
icon: <Store className="w-6 h-6" />,
processingTime: '1-3 business days',
available: true,
minAmount: 10,
maxAmount: 10000,
fees: 'No additional fees',
instructions: [
'Complete checkout to receive a payment voucher',
'Visit any OXXO store and show the voucher',
'Pay in cash at the register',
'Your payment will be confirmed within 1-3 days',
],
regions: ['Mexico'],
},
{
id: 'spei',
name: 'SPEI Transfer',
description: 'Instant bank transfer via SPEI (Mexico)',
icon: <Building2 className="w-6 h-6" />,
processingTime: 'Instant - 24 hours',
available: true,
minAmount: 1,
maxAmount: 500000,
fees: 'Bank fees may apply',
instructions: [
'Complete checkout to receive CLABE and reference number',
'Log into your bank app or website',
'Make a SPEI transfer to the provided CLABE',
'Include the reference number in your transfer',
],
regions: ['Mexico'],
},
{
id: 'card',
name: 'Credit/Debit Card',
description: 'Pay with Visa, Mastercard, or Amex',
icon: <CreditCard className="w-6 h-6" />,
processingTime: 'Instant',
available: true,
fees: 'No additional fees',
instructions: ['Enter your card details securely'],
regions: ['Worldwide'],
},
];
interface AlternativePaymentMethodsProps {
selectedMethod: PaymentMethodType | null;
onSelectMethod: (method: PaymentMethodType) => void;
onContinue: () => void;
amount?: number;
currency?: string;
isLoading?: boolean;
showCardOption?: boolean;
region?: string;
}
const AlternativePaymentMethods: React.FC<AlternativePaymentMethodsProps> = ({
selectedMethod,
onSelectMethod,
onContinue,
amount = 0,
currency = 'MXN',
isLoading = false,
showCardOption = true,
region = 'Mexico',
}) => {
const [expandedMethod, setExpandedMethod] = useState<PaymentMethodType | null>(null);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency,
}).format(value);
};
const availableMethods = ALTERNATIVE_METHODS.filter((method) => {
if (!showCardOption && method.id === 'card') return false;
if (method.regions && !method.regions.includes(region) && !method.regions.includes('Worldwide')) {
return false;
}
if (method.minAmount && amount < method.minAmount) return false;
if (method.maxAmount && amount > method.maxAmount) return false;
return method.available;
});
const toggleExpand = (methodId: PaymentMethodType) => {
setExpandedMethod(expandedMethod === methodId ? null : methodId);
};
const getMethodStatusColor = (method: AlternativePaymentMethod) => {
if (!method.available) return 'text-gray-500';
if (selectedMethod === method.id) return 'text-blue-400';
return 'text-gray-400';
};
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Select Payment Method</h3>
{amount > 0 && (
<span className="text-gray-400">
Total: <span className="text-white font-medium">{formatCurrency(amount)}</span>
</span>
)}
</div>
{/* Payment Methods List */}
<div className="space-y-3">
{availableMethods.map((method) => {
const isSelected = selectedMethod === method.id;
const isExpanded = expandedMethod === method.id;
return (
<div
key={method.id}
className={`rounded-xl border transition-all ${
isSelected
? 'border-blue-500 bg-blue-900/20'
: 'border-gray-700 bg-gray-800/50 hover:border-gray-600'
}`}
>
{/* Method Header */}
<button
onClick={() => onSelectMethod(method.id)}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-4">
<div
className={`p-3 rounded-lg ${
isSelected ? 'bg-blue-500/20' : 'bg-gray-900'
} ${getMethodStatusColor(method)}`}
>
{method.icon}
</div>
<div className="text-left">
<div className="flex items-center gap-2">
<span className="text-white font-medium">{method.name}</span>
{method.regions && method.regions[0] !== 'Worldwide' && (
<span className="px-2 py-0.5 bg-gray-700 text-gray-400 text-xs rounded">
{method.regions[0]}
</span>
)}
</div>
<p className="text-gray-400 text-sm">{method.description}</p>
</div>
</div>
<div className="flex items-center gap-3">
{isSelected && (
<CheckCircle className="w-5 h-5 text-blue-400" />
)}
<button
onClick={(e) => {
e.stopPropagation();
toggleExpand(method.id);
}}
className="p-1 text-gray-500 hover:text-white transition-colors"
>
<Info className="w-4 h-4" />
</button>
</div>
</button>
{/* Expanded Details */}
{isExpanded && (
<div className="px-4 pb-4 border-t border-gray-700/50 mt-2 pt-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500 block">Processing Time</span>
<span className="text-white flex items-center gap-1">
<Clock className="w-4 h-4 text-gray-500" />
{method.processingTime}
</span>
</div>
{method.fees && (
<div>
<span className="text-gray-500 block">Fees</span>
<span className="text-white">{method.fees}</span>
</div>
)}
{method.minAmount && (
<div>
<span className="text-gray-500 block">Min Amount</span>
<span className="text-white">{formatCurrency(method.minAmount)}</span>
</div>
)}
{method.maxAmount && (
<div>
<span className="text-gray-500 block">Max Amount</span>
<span className="text-white">{formatCurrency(method.maxAmount)}</span>
</div>
)}
</div>
{method.instructions && method.instructions.length > 0 && (
<div className="mt-4">
<span className="text-gray-500 text-sm block mb-2">How it works:</span>
<ol className="space-y-1.5">
{method.instructions.map((instruction, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-400">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-gray-700 text-gray-300 flex items-center justify-center text-xs">
{index + 1}
</span>
{instruction}
</li>
))}
</ol>
</div>
)}
</div>
)}
</div>
);
})}
</div>
{/* Amount Limits Warning */}
{amount > 0 && (
<div className="flex items-start gap-2 p-3 bg-gray-800/50 rounded-lg">
<AlertCircle className="w-4 h-4 text-amber-400 flex-shrink-0 mt-0.5" />
<p className="text-gray-400 text-sm">
Some payment methods have minimum and maximum amount limits.
{selectedMethod === 'oxxo' && amount > 10000 && (
<span className="text-amber-400 block mt-1">
OXXO Pay has a maximum limit of $10,000 MXN per transaction.
</span>
)}
</p>
</div>
)}
{/* Continue Button */}
{selectedMethod && (
<button
onClick={onContinue}
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Processing...
</>
) : (
<>
Continue with {ALTERNATIVE_METHODS.find((m) => m.id === selectedMethod)?.name}
<ChevronRight className="w-5 h-5" />
</>
)}
</button>
)}
{/* Security Note */}
<div className="flex items-center justify-center gap-2 text-gray-500 text-xs">
<CheckCircle className="w-3 h-3 text-emerald-500" />
All payments are processed securely via Stripe
</div>
</div>
);
};
export default AlternativePaymentMethods;