feat(payments): Remove insecure PaymentMethodForm component (ST4.2.1)
- Delete PaymentMethodForm.tsx (PCI-DSS violation) - Remove export from components/payments/index.ts - Component was NOT in use (legacy/demo code) Violation: Component handled card data directly (PAN, CVV, expiry) in state and sent raw data to backend. Compliant alternatives: - Stripe Customer Portal (add payment methods) - CardElement + Payment Intents (one-time payments) Blocker: BLOCKER-002 (ST4.2 PCI-DSS Compliance) Epic: OQI-005 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6654f27ae
commit
3f98938972
@ -1,273 +0,0 @@
|
||||
/**
|
||||
* PaymentMethodForm Component
|
||||
* Add payment methods using Stripe Elements
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
CreditCard,
|
||||
Check,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Shield,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { addPaymentMethod } from '../../services/payment.service';
|
||||
|
||||
interface PaymentMethodFormProps {
|
||||
onSuccess: (paymentMethodId: string) => void;
|
||||
onCancel?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
setAsDefault?: boolean;
|
||||
}
|
||||
|
||||
const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
||||
onSuccess,
|
||||
onCancel,
|
||||
onError,
|
||||
setAsDefault = true,
|
||||
}) => {
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [expiry, setExpiry] = useState('');
|
||||
const [cvc, setCvc] = useState('');
|
||||
const [cardholderName, setCardholderName] = useState('');
|
||||
const [makeDefault, setMakeDefault] = useState(setAsDefault);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Format card number with spaces
|
||||
const formatCardNumber = (value: string) => {
|
||||
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
|
||||
const matches = v.match(/\d{4,16}/g);
|
||||
const match = (matches && matches[0]) || '';
|
||||
const parts = [];
|
||||
for (let i = 0, len = match.length; i < len; i += 4) {
|
||||
parts.push(match.substring(i, i + 4));
|
||||
}
|
||||
return parts.length ? parts.join(' ') : value;
|
||||
};
|
||||
|
||||
// Format expiry as MM/YY
|
||||
const formatExpiry = (value: string) => {
|
||||
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
|
||||
if (v.length >= 2) {
|
||||
return v.substring(0, 2) + '/' + v.substring(2, 4);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
// Get card type based on number
|
||||
const getCardType = (number: string): string => {
|
||||
const cleanNumber = number.replace(/\s/g, '');
|
||||
if (/^4/.test(cleanNumber)) return 'Visa';
|
||||
if (/^5[1-5]/.test(cleanNumber)) return 'Mastercard';
|
||||
if (/^3[47]/.test(cleanNumber)) return 'Amex';
|
||||
if (/^6(?:011|5)/.test(cleanNumber)) return 'Discover';
|
||||
return 'Card';
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const cleanCardNumber = cardNumber.replace(/\s/g, '');
|
||||
|
||||
if (!cardholderName.trim()) {
|
||||
setError('Please enter the cardholder name');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cleanCardNumber.length < 13 || cleanCardNumber.length > 19) {
|
||||
setError('Please enter a valid card number');
|
||||
return false;
|
||||
}
|
||||
|
||||
const [month, year] = expiry.split('/');
|
||||
if (!month || !year || parseInt(month) < 1 || parseInt(month) > 12) {
|
||||
setError('Please enter a valid expiry date');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if card is not expired
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear() % 100;
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
if (parseInt(year) < currentYear || (parseInt(year) === currentYear && parseInt(month) < currentMonth)) {
|
||||
setError('This card has expired');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cvc.length < 3 || cvc.length > 4) {
|
||||
setError('Please enter a valid CVC');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// In a real implementation, this would use Stripe.js to create a PaymentMethod
|
||||
// For now, we simulate the API call
|
||||
const result = await addPaymentMethod({
|
||||
type: 'card',
|
||||
card: {
|
||||
number: cardNumber.replace(/\s/g, ''),
|
||||
exp_month: parseInt(expiry.split('/')[0]),
|
||||
exp_year: 2000 + parseInt(expiry.split('/')[1]),
|
||||
cvc,
|
||||
},
|
||||
billing_details: {
|
||||
name: cardholderName,
|
||||
},
|
||||
setAsDefault: makeDefault,
|
||||
});
|
||||
|
||||
onSuccess(result.id);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to add payment method';
|
||||
setError(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cardType = getCardType(cardNumber);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Card Number */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Card Number
|
||||
</label>
|
||||
<div className="relative">
|
||||
<CreditCard className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={cardNumber}
|
||||
onChange={(e) => setCardNumber(formatCardNumber(e.target.value))}
|
||||
placeholder="1234 5678 9012 3456"
|
||||
maxLength={19}
|
||||
className="w-full pl-11 pr-20 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-400">
|
||||
{cardType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cardholder Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Cardholder Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cardholderName}
|
||||
onChange={(e) => setCardholderName(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
className="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expiry & CVC */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Expiry Date
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={expiry}
|
||||
onChange={(e) => setExpiry(formatExpiry(e.target.value))}
|
||||
placeholder="MM/YY"
|
||||
maxLength={5}
|
||||
className="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
CVC
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={cvc}
|
||||
onChange={(e) => setCvc(e.target.value.replace(/\D/g, '').slice(0, 4))}
|
||||
placeholder="123"
|
||||
maxLength={4}
|
||||
className="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<Lock className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set as Default */}
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={makeDefault}
|
||||
onChange={(e) => setMakeDefault(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-300">Set as default payment method</span>
|
||||
</label>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
|
||||
<span className="text-sm text-red-400">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Security Note */}
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-900/50 rounded-lg">
|
||||
<Shield className="w-5 h-5 text-green-400" />
|
||||
<p className="text-sm text-gray-400">
|
||||
Your card information is encrypted and secure
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
{onCancel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 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"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Adding...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-5 h-5" />
|
||||
Add Card
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodForm;
|
||||
@ -14,7 +14,6 @@ export { WalletWithdrawModal } from './WalletWithdrawModal';
|
||||
// Form Components
|
||||
export { default as CouponForm } from './CouponForm';
|
||||
export type { CouponInfo } from './CouponForm';
|
||||
export { default as PaymentMethodForm } from './PaymentMethodForm';
|
||||
export { default as BillingInfoForm } from './BillingInfoForm';
|
||||
export type { BillingInfo } from './BillingInfoForm';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user