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>
302 lines
10 KiB
TypeScript
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;
|