import { Head } from '@inertiajs/react'; import axios from 'axios'; import { FormEvent, useEffect, useRef, useState } from 'react'; type Step = 'lookup' | 'pay' | 'waiting' | 'done'; // Swish only reports the outcome to our server, so the page asks us — not Swish — // how the payment went. Give up after a few minutes so it does not poll forever. const POLL_INTERVAL_MS = 2000; const POLL_TIMEOUT_MS = 5 * 60 * 1000; export default function Swish() { const [step, setStep] = useState('lookup'); const [lanId, setLanId] = useState(''); const [customerId, setCustomerId] = useState(null); const [customerName, setCustomerName] = useState(''); const [memberName, setMemberName] = useState(null); const [amount, setAmount] = useState(''); const [giveLeftover, setGiveLeftover] = useState(false); const [reference, setReference] = useState(null); const [status, setStatus] = useState(null); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const timers = useRef([]); const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent); // Stop polling when the page goes away useEffect(() => () => timers.current.forEach(clearTimeout), []); const reset = () => { timers.current.forEach(clearTimeout); timers.current = []; setStep('lookup'); setLanId(''); setAmount(''); setGiveLeftover(false); setMemberName(null); setReference(null); setStatus(null); setError(''); }; // Server errors carry a message worth showing — a network failure does not const messageFrom = (e: unknown, fallback: string) => { if (axios.isAxiosError(e) && e.response?.status === 429) { return 'För många försök. Vänta en minut och prova igen.'; } return (axios.isAxiosError(e) && e.response?.data?.message) || fallback; }; const handleLookup = async (e: FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { const res = await axios.get('/api/swish/lookup', { params: { lan_id: lanId } }); setCustomerId(res.data.customer_id); setCustomerName(res.data.name); setMemberName(res.data.member_name ?? null); setStep('pay'); } catch (e) { setError(messageFrom(e, 'Hittade ingen deltagare med det LAN-id:t.')); } finally { setLoading(false); } }; const pollStatus = (ref: string, startedAt: number) => { const timer = window.setTimeout(async () => { try { const res = await axios.get('/api/swish/status', { params: { reference: ref } }); const current = res.data.status; if (current === 'PAID' || current === 'amount_mismatch') { setStatus(current); setStep('done'); return; } if (['DECLINED', 'ERROR', 'CANCELLED', 'failed'].includes(current)) { setStatus(current); setStep('done'); return; } } catch { // A failed poll is not a failed payment — keep trying until the timeout } if (Date.now() - startedAt < POLL_TIMEOUT_MS) { pollStatus(ref, startedAt); } else { setStatus('timeout'); setStep('done'); } }, POLL_INTERVAL_MS); timers.current.push(timer); }; const handlePay = async (e: FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { const res = await axios.post('/api/swish/initiate', { customer_id: customerId, amount: parseInt(amount, 10), give_leftover: giveLeftover, }); setReference(res.data.payment_reference); setStep('waiting'); pollStatus(res.data.payment_reference, Date.now()); window.location.href = res.data.swish_url; } catch (e) { setError(messageFrom(e, 'Något gick fel. Försök igen.')); } finally { setLoading(false); } }; const outcome = () => { if (status === 'PAID') { return { title: '✓ Betalningen är klar!', text: `${amount} kr är inlagt på ${memberName ? `gruppen ${customerName}` : 'ditt'} saldo.`, tone: 'has-text-success', }; } if (status === 'amount_mismatch') { return { title: 'Beloppet stämmer inte', text: 'Betalningen kom fram men beloppet matchade inte. Prata med en funktionär.', tone: 'has-text-danger', }; } if (status === 'timeout') { return { title: 'Vi väntar fortfarande', text: 'Betalningen har inte bekräftats än. Kolla ditt saldo om en stund, eller prata med en funktionär.', tone: 'has-text-grey', }; } return { title: 'Betalningen gick inte igenom', text: 'Inget har dragits. Du kan försöka igen.', tone: 'has-text-danger', }; }; return ( <>

Betala in med Swish

{step === 'lookup' && (

Ange ditt LAN-id för att hitta ditt konto.

Om du är med i en grupp kommer saldot skickas till din grupp.

{!isMobile && (
Betalningen öppnas i Swish-appen, så den här sidan behöver köras i mobilen.
)}
setLanId(e.target.value)} required autoFocus />
{error &&

{error}

}
)} {step === 'pay' && (
{memberName ?? customerName} — stämmer det? {memberName && (

Ingår i gruppen {customerName}

)}
setAmount(e.target.value)} required autoFocus />
{error &&

{error}

}
)} {step === 'waiting' && (

Väntar på Swish…

Godkänn betalningen i Swish-appen. Den här sidan uppdaterar sig själv.

{reference && (

Referens: {reference}

)}
)} {step === 'done' && (

{outcome().title}

{outcome().text}

{reference && (

Referens: {reference}

)}
)}
); }