Files
fourge-portal/src/pages/PayInvoice.jsx
T
Krao Hasanee 04e0911e9f fix: crash bugs in TeamInvoices + faster load
Bug fixes:
- TeamInvoices: add useAuth import/currentUser (invoice-create crash)
- TeamInvoices: setChartYear -> setExportYear (year-dropdown crash)
- TeamDashboard: drop redundant setState-in-effect (cascading renders)
- Companies: delete dead _UnusedClientCompanies (illegal hook calls)
- annotate intentional empty PDF-fallback catches

Load speed:
- preconnect/dns-prefetch to Supabase origin
- lazy-load heic-to in Converters: page chunk 2737KB -> 9KB
- split recharts into its own 'charts' vendor chunk

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:59:11 -04:00

154 lines
6.9 KiB
React

import { useState, useEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import PageLoader from '../components/PageLoader';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
export default function PayInvoice() {
const { id } = useParams();
const [searchParams] = useSearchParams();
const success = searchParams.get('success') === '1';
const cancelled = searchParams.get('cancelled') === '1';
const [invoice, setInvoice] = useState(null);
const [company, setCompany] = useState(null);
const [loading, setLoading] = useState(true);
const [paying, setPaying] = useState(false);
const [error, setError] = useState('');
const totalLabel = currencyFormatter.format(Number(invoice?.total || 0));
useEffect(() => {
async function load() {
try {
const response = await fetch(`${supabaseUrl}/functions/v1/get-public-invoice`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: supabaseAnonKey,
},
body: JSON.stringify({ invoice_ref: id }),
});
const body = await response.json();
if (!response.ok || !body?.invoice) {
setLoading(false);
return;
}
setInvoice(body.invoice);
setCompany(body.invoice.companies || null);
} catch (err) {
console.error('PayInvoice load failed:', err);
} finally {
setLoading(false);
}
}
load();
}, [id]);
const handlePay = async () => {
setPaying(true);
setError('');
try {
const response = await fetch(`${supabaseUrl}/functions/v1/create-checkout-session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: supabaseAnonKey,
},
body: JSON.stringify({ invoice_ref: id }),
});
const body = await response.json();
if (!response.ok || !body?.url) throw new Error(body?.error || 'Could not create payment session.');
window.location.href = body.url;
} catch (err) {
setError(err.message);
setPaying(false);
}
};
return (
<div style={{ minHeight: '100vh', background: '#f5f5f5', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
{loading ? <PageLoader /> : null}
<div style={{ width: '100%', maxWidth: 480 }}>
{/* Logo */}
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<img src="/fourge-logo.png" alt="Fourge Branding" style={{ height: 36, filter: 'invert(1)' }} />
</div>
{!invoice ? (
<div style={{ background: '#fff', color: '#141414', borderRadius: 4, padding: 32, textAlign: 'center', boxShadow: '0 2px 12px rgba(0,0,0,0.08)' }}>
<div style={{ fontSize: 18, fontWeight: 400, marginBottom: 8 }}>Invoice not found</div>
<div style={{ color: '#666' }}>This payment link may be invalid or expired.</div>
</div>
) : success || invoice.status === 'paid' ? (
<div style={{ background: '#fff', color: '#141414', borderRadius: 4, padding: 32, textAlign: 'center', boxShadow: '0 2px 12px rgba(0,0,0,0.08)' }}>
<div style={{ fontSize: 32, marginBottom: 12 }}></div>
<div style={{ fontSize: 20, fontWeight: 400, marginBottom: 8 }}>Payment received</div>
<div style={{ color: '#666', marginBottom: 4 }}>{invoice.invoice_number}</div>
<div style={{ fontSize: 24, fontWeight: 400, color: '#16a34a', marginTop: 16 }}>{totalLabel}</div>
<div style={{ color: '#666', marginTop: 6, fontSize: 12, letterSpacing: '0.3px' }}>Charged in USD</div>
<div style={{ color: '#666', marginTop: 8, fontSize: 13 }}>Thank you for your payment. We'll be in touch!</div>
</div>
) : (
<div style={{ background: '#fff', color: '#141414', borderRadius: 4, padding: 32, boxShadow: '0 2px 12px rgba(0,0,0,0.08)' }}>
<div style={{ fontSize: 13, fontWeight: 400, textTransform: 'uppercase', letterSpacing: '0.5px', color: '#999', marginBottom: 4 }}>Invoice</div>
<div style={{ fontSize: 22, fontWeight: 400, marginBottom: 4 }}>{invoice.invoice_number}</div>
<div style={{ color: '#666', marginBottom: 24 }}>{invoice.bill_to || company?.name}</div>
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '12px 0', borderTop: '1px solid #eee', borderBottom: '1px solid #eee', marginBottom: 24 }}>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 2 }}>Invoice Date</div>
<div style={{ fontWeight: 400, color: '#141414' }}>{new Date(invoice.invoice_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: 12, color: '#999', marginBottom: 2 }}>Due Date</div>
<div style={{ fontWeight: 400, color: '#141414' }}>{new Date(invoice.due_date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<div style={{ fontSize: 14, color: '#666' }}>Total Due</div>
<div style={{ fontSize: 28, fontWeight: 400, color: '#141414' }}>{totalLabel}</div>
</div>
{cancelled && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 4, padding: '10px 14px', fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
Payment was cancelled. You can try again below.
</div>
)}
{error && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 4, padding: '10px 14px', fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
{error}
</div>
)}
<button
onClick={handlePay}
disabled={paying}
style={{
width: '100%', padding: '14px', borderRadius: 4, border: 'none',
background: paying ? '#999' : '#141414', color: '#fff',
fontSize: 16, fontWeight: 400, cursor: paying ? 'not-allowed' : 'pointer',
}}
>
{paying ? 'Redirecting to payment...' : `Pay ${totalLabel}`}
</button>
<div style={{ textAlign: 'center', marginTop: 16, fontSize: 12, color: '#999' }}>
Secured by Stripe · Charged in USD · hello@fourgebranding.com
</div>
</div>
)}
</div>
</div>
);
}