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
|
// Form Components
|
||||||
export { default as CouponForm } from './CouponForm';
|
export { default as CouponForm } from './CouponForm';
|
||||||
export type { CouponInfo } from './CouponForm';
|
export type { CouponInfo } from './CouponForm';
|
||||||
export { default as PaymentMethodForm } from './PaymentMethodForm';
|
|
||||||
export { default as BillingInfoForm } from './BillingInfoForm';
|
export { default as BillingInfoForm } from './BillingInfoForm';
|
||||||
export type { BillingInfo } from './BillingInfoForm';
|
export type { BillingInfo } from './BillingInfoForm';
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user