// Checkout → Payment → PromptPay QR → Order confirmed → Track My Pasta // ─── PromptPay EMVCo payload generator ─────────────────────────────────────── const PROMPTPAY_PHONE = '0954766235'; function ppTLV(id, val) { return id + String(val.length).padStart(2, '0') + val; } function buildPromptPayPayload(phone, amount) { const digits = phone.replace(/\D/g, ''); const acct = ('0066' + (digits.startsWith('0') ? digits.slice(1) : digits)).padStart(13, '0'); const merchantInfo = ppTLV('00', 'A000000677010111') + ppTLV('01', acct); const body = [ ppTLV('00', '01'), ppTLV('01', '12'), ppTLV('29', merchantInfo), ppTLV('53', '764'), ppTLV('54', amount.toFixed(2)), ppTLV('58', 'TH'), ppTLV('59', 'GUSTAI LAB'), ppTLV('60', 'PHUKET'), '6304', ].join(''); let crc = 0xFFFF; for (let i = 0; i < body.length; i++) { crc ^= body.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF; } return body + crc.toString(16).toUpperCase().padStart(4, '0'); } function PromptPayQR({ amount }) { const ref = React.useRef(null); React.useEffect(() => { if (!ref.current || !window.QRCode) return; ref.current.innerHTML = ''; new window.QRCode(ref.current, { text: buildPromptPayPayload(PROMPTPAY_PHONE, amount), width: 220, height: 220, colorDark: '#0A0A0A', colorLight: '#ffffff', correctLevel: window.QRCode.CorrectLevel.M, }); // qrcodejs creates a canvas; make it fill the container const canvas = ref.current.querySelector('canvas'); if (canvas) { canvas.style.width = '100%'; canvas.style.height = 'auto'; canvas.style.display = 'block'; } }, [amount]); return
; } // ───────────────────────────────────────────────────────────────────────────── function GLCheckout({ app, go }) { const [address, setAddress] = React.useState(app.address); const [place, setPlace] = React.useState('home'); const [timing, setTiming] = React.useState(0); const [payment, setPayment] = React.useState('promptpay'); const [deliveryAck, setDeliveryAck] = React.useState(false); const [locating, setLocating] = React.useState(false); const [addingLabel, setAddingLabel] = React.useState(false); const [newLabel, setNewLabel] = React.useState(''); const [customLabels, setCustomLabels] = React.useState([]); // Generate time slots based on current time const slots = React.useMemo(() => { const now = new Date(); const pad = n => String(n).padStart(2, '0'); const fmt = d => `${pad(d.getHours())}:${pad(d.getMinutes())}`; const addMin = (d, m) => new Date(d.getTime() + m * 60000); const roundUp30 = (d) => { const m = d.getMinutes(); const add = m < 30 ? 30 - m : 60 - m; return addMin(d, add); }; const asapEnd = addMin(now, 45); const slot2Start = roundUp30(addMin(now, 60)); const slot3Start = addMin(slot2Start, 30); const slot4Start = addMin(slot3Start, 30); return [ { kicker: 'ASAP', big: 'Now', sub: `${fmt(addMin(now, 30))} – ${fmt(asapEnd)}` }, { kicker: 'Today', big: fmt(slot2Start), sub: `${fmt(slot2Start)} – ${fmt(addMin(slot2Start, 30))}` }, { kicker: 'Today', big: fmt(slot3Start), sub: `${fmt(slot3Start)} – ${fmt(addMin(slot3Start, 30))}` }, { kicker: 'Tmrw', big: fmt(slot4Start), sub: `${fmt(slot4Start)} – ${fmt(addMin(slot4Start, 30))}` }, ]; }, []); const useCurrentLocation = () => { if (!navigator.geolocation) return; setLocating(true); navigator.geolocation.getCurrentPosition( pos => { fetch(`https://nominatim.openstreetmap.org/reverse?lat=${pos.coords.latitude}&lon=${pos.coords.longitude}&format=json&accept-language=en`) .then(r => r.json()) .then(d => { const a = d.address || {}; const parts = [a.road, a.village || a.suburb || a.neighbourhood, a.city || a.town || a.county].filter(Boolean); const addr = parts.join(', ') || d.display_name || `${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`; setAddress(addr); app.setAddress(addr); }) .catch(() => { const addr = `${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`; setAddress(addr); app.setAddress(addr); }) .finally(() => setLocating(false)); }, () => { setLocating(false); alert('Please enable location access in your browser settings.'); } ); }; const openInMaps = () => { const q = encodeURIComponent(address || 'Phuket, Thailand'); window.open(`https://maps.google.com/?q=${q}`, '_blank'); }; const addCustomLabel = () => { const label = newLabel.trim(); if (!label) return; setCustomLabels(prev => [...prev, label]); setPlace(label.toLowerCase()); setNewLabel(''); setAddingLabel(false); }; const allLabels = ['Home', 'Office', 'Villa 5', ...customLabels]; const subtotal = app.cart.reduce((s, i) => s + i.price * i.qty, 0); const total = subtotal + 60; return (
go('box')} center={
CHECKOUT
}/>
{/* Delivery */}

Delivery

Address
{ setAddress(e.target.value); app.setAddress(e.target.value); }} style={{ flex: 1, border: 'none', outline: 'none', padding: '14px 14px', fontSize: 14, fontWeight: 500 }}/>
Phuket Area Only
{allLabels.map(p => { const on = p.toLowerCase() === place; return ( ); })} {addingLabel ? (
setNewLabel(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addCustomLabel(); if (e.key === 'Escape') setAddingLabel(false); }} placeholder="Label…" style={{ width: 80, border: `2px solid ${GL.ink}`, padding: '6px 8px', fontSize: 11, fontWeight: 700, outline: 'none', textTransform: 'uppercase', letterSpacing: '0.1em' }}/>
) : ( )}
{/* Timing */}

Timing

{timing === 0 ? 'Est. 30–45 mins' : slots[timing].sub}
{slots.map((s, i) => { const on = i === timing; return ( ); })}
{/* Payment */}

Payment

setPayment('promptpay')} logo={
} label="PromptPay QR" sub="Scan & pay via any Thai banking app"/> setPayment('cod')} logo={
} label="Cash on Delivery" sub="Pay the driver upon arrival"/> {/* Delivery fee acknowledgment — lives in scroll area, collapses once ticked */}
{!deliveryAck ? ( ) : (
Grab delivery fee acknowledged 🛵
)}
{/* Sticky footer — total + confirm only */}
Total
Excl. Grab delivery
฿ {total.toLocaleString()}
); } function PaymentOption({ selected, onSelect, logo, label, sub }) { return (