04e0911e9f
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>
154 lines
6.9 KiB
React
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>
|
|
);
|
|
}
|