// Billing / payment page — Stripe Checkout integration const API_BASE_BILLING = 'https://r5vefiej3l.execute-api.eu-central-1.amazonaws.com/prod'; const COGNITO_CLIENT_B = '7nffjheavp985k7b4bgug45jf4'; function getBillingToken() { const last = localStorage.getItem(`CognitoIdentityServiceProvider.${COGNITO_CLIENT_B}.LastAuthUser`); if (!last) return null; return localStorage.getItem(`CognitoIdentityServiceProvider.${COGNITO_CLIENT_B}.${last}.idToken`); } async function billingApi(method, path, body) { const token = getBillingToken(); const res = await fetch(`${API_BASE_BILLING}${path}`, { method, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, ...(body !== undefined ? { body: JSON.stringify(body) } : {}), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); return data; } function fmtInvDate(ts) { return new Date(ts * 1000).toISOString().slice(0, 10); } function fmtAmount(cents, currency = 'eur') { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: currency.toUpperCase(), minimumFractionDigits: 2, }).format(cents / 100); } // ── Plan catalogue ───────────────────────────────────────────────────────────── const PLAN_INFO = { specimen: { name: 'Specimen', price: 2400, unit: 'one-off', sub: 'single scan · 30-day retention', perks: ['1 scan, single codebase', 'Self-contained HTML report', 'AI explanations', '30-day report retention'] }, field: { name: 'Field', price: 190, unit: '/ mo', sub: '5 scans · 90-day retention', perks: ['5 scans / month', 'Up to 2 seats', 'Self-contained HTML report', 'MySQL schema mapping', '90-day retention'] }, team: { name: 'Team', price: 490, unit: '/ mo', sub: 'unlimited · 5 seats · 1yr', perks: ['Unlimited scans on unlimited codebases', '5 seats, € 60 per additional seat', 'Shared workspace & report history', 'MySQL schema mapping', 'Slack integration', 'Priority support, 8h SLA', '1-year report retention'] }, agency: { name: 'Agency', price: null, unit: 'custom', sub: 'contact sales', perks: ['White-label HTML reports', 'Unlimited seats & workspaces', 'On-premise deployment available', 'Custom ingestion adapters', 'SSO (SAML / OIDC), SCIM', 'Dedicated CSM, 4h SLA'] }, }; // ── BillingPage ──────────────────────────────────────────────────────────────── function BillingPage() { const { go } = useNav(); const { plan: storedPlan, setPlan } = useAuth(); const { toast } = useToast(); const [selected, setSelected] = useState(storedPlan && PLAN_INFO[storedPlan] ? storedPlan : 'team'); const [extraSeats, setExtraSeats] = useState(0); const [confirming, setConfirming] = useState(false); const [terms, setTerms] = useState(false); const [showTerms, setShowTerms] = useState(false); const [invoices, setInvoices] = useState([]); const [invLoading, setInvLoading] = useState(true); const companyRef = useRef(); const vatRef = useRef(); const billingEmailRef = useRef(); const addressRef = useRef(); useEffect(() => { setPlan(selected); setExtraSeats(0); }, [selected]); useEffect(() => { billingApi('GET', '/billing/invoices') .then(d => setInvoices(d.invoices || [])) .catch(() => {}) .finally(() => setInvLoading(false)); }, []); const info = PLAN_INFO[selected]; const isAgency = selected === 'agency'; const seatCost = selected === 'team' ? extraSeats * 60 : 0; const total = (info.price || 0) + seatCost; const onConfirm = async () => { if (isAgency) { toast('Agency plan is bespoke — our team will reach out.', { duration: 3000 }); return; } if (!terms) { toast('Please accept the terms first', { tone: 'error' }); return; } const companyName = companyRef.current?.value?.trim(); setConfirming(true); try { // Sync company name to workspace org_name before checkout if (companyName) { await billingApi('PUT', '/account/workspace', { org_name: companyName }).catch(() => {}); } const { url } = await billingApi('POST', '/billing/checkout', { plan: selected, extra_seats: extraSeats, billing_email: billingEmailRef.current?.value?.trim() || undefined, company: companyName || undefined, }); window.location.href = url; } catch (err) { toast(err.message || 'Could not initiate checkout', { tone: 'error' }); setConfirming(false); } }; const onManageSubscription = async () => { try { const { url } = await billingApi('POST', '/billing/portal', { return_url: 'https://getfossyl.dev' }); window.location.href = url; } catch (err) { toast(err.message || 'Could not open billing portal', { tone: 'error' }); } }; return (
| Invoice | Date | Plan | Amount | Status | |
|---|---|---|---|---|---|
| {inv.id} | {fmtInvDate(inv.date)} | {inv.label} | {fmtAmount(inv.amount, inv.currency)} | {inv.status} | {inv.pdf ? PDF → : — } |