Square Payments
This commit is contained in:
175
frontend/src/components/SquarePayment.tsx
Normal file
175
frontend/src/components/SquarePayment.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface SquarePaymentProps {
|
||||
amount: number;
|
||||
onPaymentSuccess: (paymentResult: any) => void;
|
||||
onPaymentError: (error: string) => void;
|
||||
tierId: number;
|
||||
}
|
||||
|
||||
const SquarePayment: React.FC<SquarePaymentProps> = ({
|
||||
amount,
|
||||
onPaymentSuccess,
|
||||
onPaymentError,
|
||||
tierId
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [card, setCard] = useState<any>(null);
|
||||
const [payments, setPayments] = useState<any>(null);
|
||||
const [squareConfig, setSquareConfig] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadSquareConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (squareConfig && !payments) {
|
||||
initializeSquare();
|
||||
}
|
||||
}, [squareConfig]);
|
||||
|
||||
const loadSquareConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/payments/config/square', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
const config = await response.json();
|
||||
setSquareConfig(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to load Square config:', error);
|
||||
onPaymentError('Failed to load payment configuration');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeSquare = async () => {
|
||||
if (!window.Square) {
|
||||
console.error('Square.js failed to load');
|
||||
onPaymentError('Payment system failed to load. Please refresh the page.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paymentsInstance = window.Square.payments(
|
||||
squareConfig.application_id,
|
||||
squareConfig.location_id
|
||||
);
|
||||
setPayments(paymentsInstance);
|
||||
|
||||
const cardInstance = await paymentsInstance.card();
|
||||
await cardInstance.attach('#card-container');
|
||||
setCard(cardInstance);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Square:', error);
|
||||
onPaymentError('Failed to initialize payment form');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayment = async () => {
|
||||
if (!card || !payments) {
|
||||
onPaymentError('Payment form not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Tokenize the payment method
|
||||
const result = await card.tokenize();
|
||||
|
||||
if (result.status === 'OK') {
|
||||
// Send the token to your backend
|
||||
const response = await fetch('/api/v1/payments/square/process', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source_id: result.token,
|
||||
amount: amount,
|
||||
tier_id: tierId,
|
||||
note: `Membership payment - £${amount.toFixed(2)}`
|
||||
})
|
||||
});
|
||||
|
||||
const paymentResult = await response.json();
|
||||
|
||||
if (response.ok && paymentResult.success) {
|
||||
onPaymentSuccess(paymentResult);
|
||||
} else {
|
||||
// Handle error response gracefully
|
||||
let errorMessage = 'Payment failed. Please try again.';
|
||||
|
||||
if (paymentResult.errors && Array.isArray(paymentResult.errors) && paymentResult.errors.length > 0) {
|
||||
// Show the first error message (they're already user-friendly from backend)
|
||||
errorMessage = paymentResult.errors[0];
|
||||
} else if (paymentResult.detail) {
|
||||
errorMessage = paymentResult.detail;
|
||||
}
|
||||
|
||||
onPaymentError(errorMessage);
|
||||
}
|
||||
} else {
|
||||
const errors = result.errors?.map((e: any) => e.message).join(', ') || 'Card tokenization failed';
|
||||
onPaymentError(errors);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Payment error:', error);
|
||||
onPaymentError(error.message || 'Payment processing failed');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4 style={{ marginBottom: '10px' }}>Card Payment</h4>
|
||||
<p style={{ fontSize: '14px', color: '#666' }}>
|
||||
Amount: <strong>£{amount.toFixed(2)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="card-container"
|
||||
style={{
|
||||
minHeight: '200px',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handlePayment}
|
||||
disabled={isLoading || !card}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{isLoading ? 'Processing...' : `Pay £${amount.toFixed(2)}`}
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: '16px', fontSize: '12px', color: '#666', textAlign: 'center' }}>
|
||||
<p>Secure payment powered by Square</p>
|
||||
{squareConfig?.environment === 'sandbox' && (
|
||||
<p style={{ color: '#ff9800', marginTop: '8px' }}>
|
||||
<strong>Test Mode:</strong> Use card 4111 1111 1111 1111 with any future expiry
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Extend Window interface for TypeScript
|
||||
declare global {
|
||||
interface Window {
|
||||
Square: any;
|
||||
}
|
||||
}
|
||||
|
||||
export default SquarePayment;
|
||||
Reference in New Issue
Block a user