Square Payments

This commit is contained in:
James Pattinson
2025-11-12 16:09:38 +00:00
parent be2426c078
commit 0f74333a22
19 changed files with 1828 additions and 85 deletions

View 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;