// 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 (
{/* 01 — Plan */}
01 — Planchange anytime
{Object.entries(PLAN_INFO).map(([id, p]) => ( setSelected(id)} /> ))} {selected === 'team' && (
Additional seats · € 60 / seat / mo
{extraSeats}
)}
{/* 02 — Billing details */}
02 — Billing details
Company
VAT ID
Billing email
Address
{/* 03 — Payment */}
03 — Payment
S
Secure payment via Stripe
Card, SEPA direct debit, and invoice accepted. You will be redirected to Stripe's hosted checkout to complete payment.
{showTerms && ( { setTerms(true); setShowTerms(false); }} onDecline={() => { setTerms(false); setShowTerms(false); }} /> )}
{/* Summary */}
Order summary
{info.name} plan{info.unit !== 'custom' ? ` · ${info.unit.replace('/ ', '')}` : ''} {info.price ? `€ ${info.price.toLocaleString('en-US')}.00` : 'Custom'}
{selected === 'team' && (
Additional seats ({extraSeats}) € {seatCost.toLocaleString('en-US')}.00
)}
Subtotal{info.price ? `€ ${total.toLocaleString('en-US')}.00` : '—'}
Reverse-charge VAT (DE)€ 0.00
Total due today {info.price ? `€ ${total.toLocaleString('en-US')}.00` : 'Custom'}
{isAgency ? 'priced per engagement' : info.unit === 'one-off' ? 'one-time charge' : 'recurring · cancel anytime'}
Payments processed by Stripe · PCI-DSS compliant
What you get
{info.perks.map(x => (
{x}
))}
{/* Invoice history */}
Invoice history
{invLoading ? (
Loading invoices…
) : invoices.length === 0 ? (
No invoices yet — your first invoice will appear here after payment.
) : ( {invoices.map(inv => ( ))}
InvoiceDatePlanAmountStatus
{inv.id} {fmtInvDate(inv.date)} {inv.label} {fmtAmount(inv.amount, inv.currency)} {inv.status} {inv.pdf ? PDF → : }
)}
); } function MethodBtn({ active, onClick, children }) { return ( ); } function PlanRadio({ name, price, sub, checked, onSelect }) { return ( ); } Object.assign(window, { BillingPage });