Files
sasa-membership/frontend/src/components/SquarePaymentNew.tsx
T
James Pattinson 0c0b5fbefe Square payment fixes
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 09:16:12 +00:00

443 lines
14 KiB
TypeScript

import React, { useEffect, useState } from 'react';
interface SquarePaymentNewProps {
amount: number;
onPaymentSuccess: (paymentResult: any) => void;
onPaymentError: (error: string) => void;
tierId: number;
}
const SquarePaymentNew: React.FC<SquarePaymentNewProps> = ({
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);
const [isProcessing, setIsProcessing] = useState(false);
const [cardholderName, setCardholderName] = useState('');
const [addressLine1, setAddressLine1] = useState('');
const [addressLine2, setAddressLine2] = useState('');
const [city, setCity] = useState('');
const [postalCode, setPostalCode] = useState('');
useEffect(() => {
loadSquareConfig();
}, []);
useEffect(() => {
if (squareConfig && !payments && !isLoading) {
// Only initialize after component is fully rendered (not loading)
setTimeout(() => {
initializeSquare();
}, 100);
}
}, [squareConfig, isLoading]);
const loadSquareSDK = (environment: string): Promise<void> => {
return new Promise((resolve, reject) => {
if (window.Square) {
resolve();
return;
}
const script = document.createElement('script');
script.type = 'text/javascript';
if (environment?.toLowerCase() === 'sandbox') {
script.src = 'https://sandbox.web.squarecdn.com/v1/square.js';
} else {
script.src = 'https://web.squarecdn.com/v1/square.js';
}
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Square SDK'));
document.head.appendChild(script);
});
};
const loadSquareConfig = async () => {
try {
const response = await fetch('/api/v1/payments/config/square', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const config = await response.json();
console.log('Square config received:', config);
await loadSquareSDK(config.environment);
console.log('Square SDK loaded for environment:', config.environment);
setSquareConfig(config);
setIsLoading(false); // Set loading to false here so DOM renders
} 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 environment = squareConfig.environment?.toLowerCase() === 'sandbox' ? 'sandbox' : 'production';
console.log('Initializing Square with environment:', environment);
const paymentsInstance = window.Square.payments(
squareConfig.application_id,
squareConfig.location_id,
{
environment: environment
}
);
setPayments(paymentsInstance);
// Initialize card with minimal styling - let Square handle address collection
const cardInstance = await paymentsInstance.card({
style: {
'.input-container': {
borderColor: '#E0E0E0',
borderRadius: '4px'
},
'.input-container.is-focus': {
borderColor: '#4CAF50'
},
'.message-text': {
color: '#666'
},
'.message-icon': {
color: '#666'
},
'input': {
color: '#333'
}
}
});
// Wait for element to be available in DOM
const waitForElement = (selector: string, maxAttempts = 20): Promise<Element> => {
return new Promise((resolve, reject) => {
let attempts = 0;
console.log(`Looking for element: ${selector}`);
const checkElement = () => {
const element = document.querySelector(selector);
console.log(`Attempt ${attempts + 1}: Element ${selector} found:`, !!element);
if (element) {
console.log('Element found, resolving');
resolve(element);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(checkElement, 200);
} else {
console.error(`Element ${selector} not found after ${maxAttempts} attempts`);
console.log('Available elements:', document.querySelectorAll('div[id*="square"]'));
reject(new Error(`Element ${selector} not found after ${maxAttempts} attempts`));
}
};
checkElement();
});
};
console.log('About to wait for square-card-element');
await waitForElement('#square-card-element');
console.log('Element found, attaching card instance');
await cardInstance.attach('#square-card-element');
setCard(cardInstance);
} catch (error) {
console.error('Failed to initialize Square:', error);
onPaymentError('Failed to initialize payment form');
}
};
const handlePayment = async () => {
if (!card || !payments) {
onPaymentError('Payment form not initialized');
return;
}
if (!cardholderName.trim()) {
onPaymentError('Please enter cardholder name');
return;
}
if (!addressLine1.trim()) {
onPaymentError('Please enter address line 1');
return;
}
if (!city.trim()) {
onPaymentError('Please enter city');
return;
}
if (!postalCode.trim()) {
onPaymentError('Please enter postal code');
return;
}
setIsProcessing(true);
try {
const [givenName, ...familyNameParts] = cardholderName.trim().split(/\s+/);
const verificationDetails = {
amount: amount.toFixed(2),
currencyCode: 'GBP',
intent: 'CHARGE',
billingContact: {
givenName: givenName || cardholderName.trim(),
familyName: familyNameParts.join(' '),
addressLines: [addressLine1.trim(), addressLine2.trim()].filter(Boolean),
city: city.trim(),
postalCode: postalCode.trim(),
countryCode: 'GB'
},
customerInitiated: true,
sellerKeyedIn: false
};
const result = await card.tokenize(verificationDetails);
if (result.status === 'OK') {
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)}`,
billing_details: {
cardholder_name: cardholderName.trim(),
address_line_1: addressLine1.trim(),
address_line_2: addressLine2.trim() || undefined,
city: city.trim(),
postal_code: postalCode.trim(),
country: 'GB'
}
})
});
const paymentResult = await response.json();
if (response.ok && paymentResult.success) {
onPaymentSuccess(paymentResult);
} else {
let errorMessage = 'Payment failed. Please try again.';
if (paymentResult.errors && Array.isArray(paymentResult.errors) && paymentResult.errors.length > 0) {
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 validation failed';
onPaymentError(errors);
}
} catch (error: any) {
console.error('Payment error:', error);
onPaymentError(error.message || 'Payment processing failed');
} finally {
setIsProcessing(false);
}
};
if (isLoading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '16px', color: '#666' }}>Loading secure payment form...</div>
</div>
);
}
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<div style={{ marginBottom: '24px', textAlign: 'center' }}>
<h3 style={{ marginBottom: '8px', color: '#333' }}>Secure Payment</h3>
<p style={{ fontSize: '18px', fontWeight: 'bold', color: '#4CAF50' }}>
£{amount.toFixed(2)}
</p>
<p style={{ fontSize: '14px', color: '#666', margin: '8px 0' }}>Enter billing details to verify your card securely.</p>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500', color: '#333' }}>
Cardholder Name *
</label>
<input
type="text"
value={cardholderName}
onChange={(e) => setCardholderName(e.target.value)}
placeholder="Name as shown on card"
style={{
width: '100%',
padding: '10px 12px',
fontSize: '14px',
border: '1px solid #E0E0E0',
borderRadius: '4px',
boxSizing: 'border-box'
}}
disabled={isProcessing}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500', color: '#333' }}>
Address Line 1 *
</label>
<input
type="text"
value={addressLine1}
onChange={(e) => setAddressLine1(e.target.value)}
placeholder="Street address"
style={{
width: '100%',
padding: '10px 12px',
fontSize: '14px',
border: '1px solid #E0E0E0',
borderRadius: '4px',
boxSizing: 'border-box'
}}
disabled={isProcessing}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500', color: '#333' }}>
Address Line 2
</label>
<input
type="text"
value={addressLine2}
onChange={(e) => setAddressLine2(e.target.value)}
placeholder="Apartment, suite, etc. (optional)"
style={{
width: '100%',
padding: '10px 12px',
fontSize: '14px',
border: '1px solid #E0E0E0',
borderRadius: '4px',
boxSizing: 'border-box'
}}
disabled={isProcessing}
/>
</div>
<div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
<div style={{ flex: '1' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500', color: '#333' }}>
City *
</label>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="City"
style={{
width: '100%',
padding: '10px 12px',
fontSize: '14px',
border: '1px solid #E0E0E0',
borderRadius: '4px',
boxSizing: 'border-box'
}}
disabled={isProcessing}
/>
</div>
<div style={{ flex: '1' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500', color: '#333' }}>
Postal Code *
</label>
<input
type="text"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
placeholder="Postcode"
style={{
width: '100%',
padding: '10px 12px',
fontSize: '14px',
border: '1px solid #E0E0E0',
borderRadius: '4px',
boxSizing: 'border-box'
}}
disabled={isProcessing}
/>
</div>
</div>
<div style={{ marginBottom: '24px' }}>
<label style={{
display: 'block',
marginBottom: '8px',
fontSize: '14px',
fontWeight: '500',
color: '#333'
}}>
Payment Details
</label>
<div
id="square-card-element"
style={{
minHeight: '200px',
border: '1px solid #E0E0E0',
borderRadius: '8px',
backgroundColor: '#fff'
}}
/>
</div>
<button
onClick={handlePayment}
disabled={!card || isProcessing}
style={{
width: '100%',
padding: '16px',
fontSize: '16px',
fontWeight: '600',
color: 'white',
backgroundColor: isProcessing ? '#ccc' : '#4CAF50',
border: 'none',
borderRadius: '8px',
cursor: isProcessing ? 'not-allowed' : 'pointer',
transition: 'background-color 0.2s'
}}
>
{isProcessing ? 'Processing Payment...' : `Pay £${amount.toFixed(2)}`}
</button>
<div style={{ marginTop: '16px', fontSize: '12px', color: '#666', textAlign: 'center' }}>
<p>🔒 Secure payment powered by Square</p>
<p>Your payment information is encrypted and secure</p>
{squareConfig?.environment === 'sandbox' && (
<p style={{ color: '#ff9800', marginTop: '8px', fontWeight: 'bold' }}>
TEST MODE: Use test card 4111 1111 1111 1111 with any future expiry date
</p>
)}
</div>
</div>
);
};
// Extend Window interface for TypeScript
declare global {
interface Window {
Square: any;
}
}
export default SquarePaymentNew;