How the Web Checkout Operates
The verified checkout flow operates entirely on the web without requiring any native mobile SDK. When a customer checks out, the web application executes the following sequence:
Web client submits cart items to POST /checkout, receiving payment.qr_code and expiration timestamp.
Renders the base64 QR code inside the `#f8f7fc` modal with formatted expiration date and trust badges.
Draws the QR image onto an offscreen HTML5 <canvas> to extract the URL and populate a clickable link.
Runs an automated 3-second interval polling /orders/status/:ref until status updates to paid.
Production Web QR Checkout Component
Directly replicates the UI, jsQR canvas decoding, and 3-second status polling in app/checkout/page.js.
Scan or Tap the QR to Pay
Scan with your FuturePayNG app or double-tap the QR code
EXPIRES:
06 Nov 2026, 11:15 AM
Secure
Bank-grade
security
Instant
Payment in
seconds
Private
Your data is
protected
Full Production Checkout Page Component
Below is the complete, self-contained Next.js App Router checkout page component powering a production e-commerce web checkout. It includes automated checkout initiation, client-side HTML5 canvas QR decoding with jsQR, 3-second status interval polling, the Web Share API with PNG download fallback, and the verified post-payment confirmation screen.
'use client';
import { useState, useEffect } from 'react';
import { api } from '@/lib/api';
import { useAuthStore } from '@/lib/store/authStore';
import styles from './page.module.css';
import jsQR from 'jsqr';
export default function Checkout() {
const { isAuthenticated, user } = useAuthStore();
const [shippingMethod, setShippingMethod] = useState('standard');
const [cartItems, setCartItems] = useState([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [paymentData, setPaymentData] = useState(null);
const [showAuthPopup, setShowAuthPopup] = useState(false);
const [authForm, setAuthForm] = useState({ name: '', email: '', password: '', phone: '' });
const [authError, setAuthError] = useState('');
const [orderReference, setOrderReference] = useState(null);
const [paymentStatus, setPaymentStatus] = useState('pending');
const [qrCodeUrl, setQrCodeUrl] = useState('');
useEffect(() => {
if (paymentData && paymentData.qr_code) {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
try {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code && code.data) {
setQrCodeUrl(code.data);
}
} catch (e) {
console.error("Failed to decode QR code:", e);
}
};
img.src = paymentData.qr_code;
}
}, [paymentData]);
useEffect(() => {
async function loadCart() {
try {
const res = await api.get('/cart');
const items = res.items || [];
setCartItems(items);
if (items.length > 0) {
handleCheckoutAuto(items);
} else {
setLoading(false);
}
} catch (err) {
console.error(err);
setLoading(false);
}
}
loadCart();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
let interval;
if (orderReference && paymentStatus === 'pending') {
interval = setInterval(async () => {
try {
const res = await api.get(`/orders/status/${orderReference}`);
if (res.status === 'paid') {
setPaymentStatus('paid');
clearInterval(interval);
}
} catch (e) {
// ignore
}
}, 3000);
}
return () => clearInterval(interval);
}, [orderReference, paymentStatus]);
const handleAuthSubmit = async (e) => {
e.preventDefault();
setAuthError('');
setSubmitting(true);
try {
// Basic register, assuming the endpoint exists
const res = await api.post('/auth/register', authForm);
if (res.token) {
useAuthStore.getState().login(res.user, res.token);
setShowAuthPopup(false);
// Retry checkout
setTimeout(() => handleCheckoutAuto(cartItems), 500);
}
} catch (err) {
setAuthError(err.message || 'Signup failed');
setSubmitting(false);
}
};
const handleCheckoutAuto = async (itemsToCheckout) => {
setSubmitting(true);
try {
const items = itemsToCheckout.map(item => ({
product_id: item.product_id,
qty: item.qty,
}));
const res = await api.post('/checkout', {
items,
email: user?.email || 'guest@example.com',
phone: user?.phone || '0000000000',
country_code: 'NG',
region: 'N/A',
shipping_method: 'standard',
shipping_address: {
fullName: user?.name || 'Guest User',
email: user?.email || 'guest@example.com',
phoneNumber: user?.phone || '0000000000',
streetAddress: 'N/A',
city: 'N/A',
state: 'N/A'
}
});
if (res.payment && res.payment.qr_code) {
setPaymentData(res.payment);
setOrderReference(res.order_reference);
} else {
alert('Payment gateway error. Please try again.');
}
} catch (err) {
console.error(err);
alert('Checkout failed: ' + (err.message || 'Unknown error'));
} finally {
setSubmitting(false);
setLoading(false);
}
};
if (loading || submitting) {
return (
<div className={styles.main} style={{textAlign: 'center', padding: '60px 20px', display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
<span className="material-symbols-outlined" style={{fontSize: '48px', color: 'var(--primary)', animation: 'spin 2s linear infinite'}}>refresh</span>
<h3 className="font-headline-md" style={{marginTop: '20px', marginBottom: '8px'}}>Generating your FuturePay QR Code...</h3>
<p className="font-body-md" style={{color: 'var(--on-surface-variant)'}}>Please wait while we secure your payment session.</p>
<style dangerouslySetInnerHTML={{__html: `
@keyframes spin { 100% { transform: rotate(360deg); } }
`}} />
</div>
);
}
if (paymentStatus === 'paid') {
return (
<div className={styles.main} style={{textAlign: 'center', padding: '60px 20px', display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
<span className="material-symbols-outlined" style={{fontSize: '80px', color: '#10B981', marginBottom: '20px'}}>check_circle</span>
<h2 className="font-headline-xl" style={{marginBottom: '16px'}}>Payment Successful!</h2>
<div style={{ background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0', maxWidth: '500px', width: '100%', marginBottom: '24px' }}>
<p className="font-body-lg" style={{ marginBottom: '16px' }}>Your order has been confirmed.</p>
<p className="font-title-lg" style={{ fontSize: '24px', fontWeight: 'bold', color: 'var(--primary)', letterSpacing: '2px', marginBottom: '16px' }}>
{orderReference}
</p>
<p className="font-body-md" style={{ color: '#475569', fontSize: '14px' }}>
<strong>Important:</strong> Please save this Order Reference. Since you checked out as a guest, you will need this exact reference number to track your shipping status.
</p>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<button
onClick={() => window.location.href = '/track'}
style={{ padding: '14px 24px', background: 'var(--primary)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold', fontSize: '15px' }}
>
Track Order Status
</button>
<button
onClick={() => window.location.href = '/catalog'}
style={{ padding: '14px 24px', background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold', fontSize: '15px' }}
>
Continue Shopping
</button>
</div>
</div>
);
}
if (paymentData) {
return (
<div style={{
backgroundColor: '#f8f7fc',
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '40px 20px',
fontFamily: 'var(--font-sans, system-ui, sans-serif)',
width: '100%'
}}>
{/* Logo */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '32px' }}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="3" width="14" height="4" rx="2" fill="#F49A25"/>
<path d="M5 10C5 8.89543 5.89543 8 7 8H14C15.1046 8 16 8.89543 16 10C16 11.1046 15.1046 12 14 12H9V20C9 21.1046 8.10457 22 7 22C5.89543 22 5 21.1046 5 20V10Z" fill="#1C4ED8"/>
</svg>
<span style={{ fontSize: '20px', fontWeight: '800', color: '#190d3e', letterSpacing: '-0.3px' }}>
FuturePay<span style={{ color: '#F49A25' }}>NG</span>
</span>
</div>
{/* Title */}
<h2 style={{ fontSize: '26px', fontWeight: '800', color: '#190d3e', textAlign: 'center', marginBottom: '8px', maxWidth: '320px', lineHeight: '1.2' }}>
Scan or Tap the QR to Pay
</h2>
<p style={{ fontSize: '14px', color: '#6b6685', textAlign: 'center', marginBottom: '24px' }}>
Scan with your FuturePayNG app or double-tap the QR code
</p>
{/* Expires Box */}
{paymentData.expires_at && (
<div style={{
backgroundColor: '#efedf9',
border: '1px solid #dedcef',
borderRadius: '12px',
padding: '12px 24px',
width: '100%',
maxWidth: '340px',
textAlign: 'center',
marginBottom: '24px'
}}>
<p style={{ fontSize: '11px', fontWeight: '700', color: '#6b6685', letterSpacing: '1px', marginBottom: '4px' }}>EXPIRES:</p>
<p style={{ fontSize: '15px', fontWeight: '800', color: '#190d3e' }}>
{(() => {
const date = new Date(paymentData.expires_at);
const day = date.getDate().toString().padStart(2, '0');
const month = date.toLocaleString('en-GB', { month: 'short' });
const year = date.getFullYear();
const time = date.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
return `${day} ${month} ${year}, ${time}`;
})()}
</p>
</div>
)}
{/* QR Code Container */}
<div style={{
backgroundColor: '#ffffff',
borderRadius: '16px',
padding: '24px',
width: '100%',
maxWidth: '340px',
border: '1px solid #dedcef',
marginBottom: '24px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<a href={qrCodeUrl || paymentData.checkout_url || `futurepay://checkout?invoice=${paymentData.invoice_reference}`} target="_blank" rel="noopener noreferrer">
<img src={paymentData.qr_code} alt="FuturePay QR Code" style={{ width: '100%', maxWidth: '250px', height: 'auto', display: 'block' }} />
</a>
</div>
{/* Share Button */}
<button
onClick={async () => {
try {
const fetchRes = await fetch(paymentData.qr_code);
const blob = await fetchRes.blob();
const file = new File([blob], 'futurepay-qr.png', { type: 'image/png' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
title: 'FuturePay QR Code',
text: 'Scan to pay this FuturePay invoice',
files: [file]
});
} else {
// Fallback to download if sharing file is not supported
const link = document.createElement('a');
link.href = paymentData.qr_code;
link.download = 'futurepay-qr.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
} catch (error) {
console.error('Error sharing or downloading QR code:', error);
// Fallback to basic share if file sharing fails
if (navigator.share) {
navigator.share({
title: 'FuturePay QR Code',
text: 'Scan to pay',
url: paymentData.qr_code
}).catch(console.error);
} else {
// Final fallback to download
const link = document.createElement('a');
link.href = paymentData.qr_code;
link.download = 'futurepay-qr.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}}
className={styles.shareButton}
>
Share QR Code <span className="material-symbols-outlined" style={{fontSize: '18px'}}>share</span>
</button>
{/* Features Box */}
<div style={{
backgroundColor: '#efedf9',
border: '1px solid #dedcef',
borderRadius: '16px',
padding: '20px 12px',
width: '100%',
maxWidth: '340px',
display: 'flex',
justifyContent: 'space-between',
marginBottom: '16px'
}}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 1, borderRight: '1px solid #dedcef' }}>
<div style={{ width: '36px', height: '36px', borderRadius: '50%', backgroundColor: '#ffffff', display: 'flex', justifyContent: 'center', alignItems: 'center', marginBottom: '12px', color: '#190d3e' }}>
<span className="material-symbols-outlined" style={{fontSize: '18px'}}>verified_user</span>
</div>
<p style={{ fontSize: '13px', fontWeight: '800', color: '#190d3e', marginBottom: '4px' }}>Secure</p>
<p style={{ fontSize: '11px', color: '#6b6685', textAlign: 'center', padding: '0 4px', lineHeight: '1.4' }}>Bank-grade<br/>security</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 1, borderRight: '1px solid #dedcef' }}>
<div style={{ width: '36px', height: '36px', borderRadius: '50%', backgroundColor: '#ffffff', display: 'flex', justifyContent: 'center', alignItems: 'center', marginBottom: '12px', color: '#190d3e' }}>
<span className="material-symbols-outlined" style={{fontSize: '18px'}}>bolt</span>
</div>
<p style={{ fontSize: '13px', fontWeight: '800', color: '#190d3e', marginBottom: '4px' }}>Instant</p>
<p style={{ fontSize: '11px', color: '#6b6685', textAlign: 'center', padding: '0 4px', lineHeight: '1.4' }}>Payment in<br/>seconds</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 1 }}>
<div style={{ width: '36px', height: '36px', borderRadius: '50%', backgroundColor: '#ffffff', display: 'flex', justifyContent: 'center', alignItems: 'center', marginBottom: '12px', color: '#190d3e' }}>
<span className="material-symbols-outlined" style={{fontSize: '18px'}}>lock</span>
</div>
<p style={{ fontSize: '13px', fontWeight: '800', color: '#190d3e', marginBottom: '4px' }}>Private</p>
<p style={{ fontSize: '11px', color: '#6b6685', textAlign: 'center', padding: '0 4px', lineHeight: '1.4' }}>Your data is<br/>protected</p>
</div>
</div>
{/* Footer Box */}
<div style={{
backgroundColor: '#efedf9',
border: '1px solid #dedcef',
borderRadius: '12px',
padding: '16px',
width: '100%',
maxWidth: '340px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span className="material-symbols-outlined" style={{fontSize: '18px', color: '#6b6685'}}>gpp_good</span>
<p style={{ fontSize: '11px', color: '#6b6685', lineHeight: '1.4', maxWidth: '120px' }}>Your payment is protected by bank-grade security.</p>
</div>
<p style={{ fontSize: '12px', fontWeight: '800', color: '#190d3e' }}>futurepayng.com</p>
</div>
<p className="font-body-sm" style={{marginTop: '24px', color: '#6b6685', fontSize: '13px'}}>Waiting for payment confirmation...</p>
</div>
);
}
return (
<div className={styles.main}>
<div style={{padding: '40px', textAlign: 'center'}}>
<h3>Your cart is empty</h3>
<p>Please add items to your cart before checking out.</p>
</div>
</div>
);
}
npm install jsqr in your Next.js project. You can copy the code above directly into your App Router route (e.g. app/checkout/page.js) and adapt the cart loading and API endpoints to your backend.Modal Markup & Expiration Date Formatting
The verified checkout renders the QR code with an expiration banner styled in #efedf9. The date formatter converts the ISO-8601 string into a clear date and time display:
{/* Formatted Expiration Banner */}
{paymentData.expires_at && (
<div style={{
backgroundColor: '#efedf9',
border: '1px solid #dedcef',
borderRadius: '12px',
padding: '12px 24px',
width: '100%',
maxWidth: '340px',
textAlign: 'center',
marginBottom: '24px'
}}>
<p style={{ fontSize: '11px', fontWeight: '700', color: '#6b6685', letterSpacing: '1px', marginBottom: '4px' }}>
EXPIRES:
</p>
<p style={{ fontSize: '15px', fontWeight: '800', color: '#190d3e' }}>
{(() => {
const date = new Date(paymentData.expires_at);
const day = date.getDate().toString().padStart(2, '0');
const month = date.toLocaleString('en-GB', { month: 'short' });
const year = date.getFullYear();
const time = date.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
return `${day} ${month} ${year}, ${time}`;
})()}
</p>
</div>
)}
{/* QR Code Container */}
<div style={{
backgroundColor: '#ffffff',
borderRadius: '16px',
padding: '24px',
width: '100%',
maxWidth: '340px',
border: '1px solid #dedcef',
marginBottom: '24px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<a
href={qrCodeUrl || paymentData.checkout_url || `https://stage-futurepay.yellow.team/checkout/${paymentData.invoice_reference}`}
target="_blank"
rel="noopener noreferrer"
>
<img
src={paymentData.qr_code}
alt="FuturePay QR Code"
style={{ width: '100%', maxWidth: '250px', height: 'auto', display: 'block' }}
/>
</a>
</div>Decoding the QR Payload in the Web Browser
Rather than making an extra server round-trip, the web client loads the base64 image onto an in-memory HTML5 <canvas> element and passes the raw pixel buffer to jsQR:
import jsQR from 'jsqr';
useEffect(() => {
if (paymentData && paymentData.qr_code) {
const img = new Image();
img.onload = () => {
// 1. Create offscreen canvas matching image dimensions
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
// 2. Draw base64 image onto canvas context
ctx.drawImage(img, 0, 0);
try {
// 3. Extract raw RGBA pixel data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 4. Decode QR code with jsQR
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code && code.data) {
setQrCodeUrl(code.data);
}
} catch (e) {
console.error("Failed to decode QR code:", e);
}
};
img.src = paymentData.qr_code;
}
}, [paymentData]);Automated 3-Second Status Interval
While the QR modal is displayed, an active React useEffect interval queries the order status every 3,000 milliseconds. When the status changes to paid, the interval is cleared and the UI immediately transitions to the order confirmation view:
useEffect(() => {
let interval;
if (orderReference && paymentStatus === 'pending') {
interval = setInterval(async () => {
try {
const res = await api.get(`/orders/status/${orderReference}`);
if (res.status === 'paid') {
setPaymentStatus('paid');
clearInterval(interval);
}
} catch (e) {
// Ignore temporary network glitches and continue polling
}
}, 3000);
}
return () => clearInterval(interval);
}, [orderReference, paymentStatus]);Sharing or Saving the QR Code on Web Browsers
The "Share QR Code" button fetches the base64 string, converts it to a standard PNG File object, and attempts to invoke navigator.share(). If file sharing is unsupported, it automatically falls back to an anchor download:
<button
onClick={async () => {
try {
const fetchRes = await fetch(paymentData.qr_code);
const blob = await fetchRes.blob();
const file = new File([blob], 'futurepay-qr.png', { type: 'image/png' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
title: 'FuturePay QR Code',
text: 'Scan to pay this FuturePayNG invoice',
files: [file]
});
} else {
// Fallback: direct browser file download
const link = document.createElement('a');
link.href = paymentData.qr_code;
link.download = 'futurepay-qr.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
} catch (error) {
console.error('Error sharing or downloading QR code:', error);
// Fallback to basic link download
const link = document.createElement('a');
link.href = paymentData.qr_code;
link.download = 'futurepay-qr.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
className={styles.shareButton}
>
Share QR Code <span className="material-symbols-outlined" style={{fontSize: '18px'}}>share</span>
</button>Post-Payment Success Screen
When the polling interval receives status: 'paid', the checkout renders the confirmation card with the order reference:
if (paymentStatus === 'paid') {
return (
<div style={{ textAlign: 'center', padding: '60px 20px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span className="material-symbols-outlined" style={{ fontSize: '80px', color: '#10B981', marginBottom: '20px' }}>
check_circle
</span>
<h2 style={{ fontSize: '28px', fontWeight: '800', marginBottom: '16px', color: '#0F172A' }}>
Payment Successful!
</h2>
<div style={{ background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0', maxWidth: '500px', width: '100%', marginBottom: '24px' }}>
<p style={{ marginBottom: '16px', color: '#334155', fontSize: '15px' }}>
Your order has been confirmed.
</p>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#0e53c8', letterSpacing: '2px', marginBottom: '16px' }}>
{orderReference}
</p>
<p style={{ color: '#475569', fontSize: '14px' }}>
<strong>Important:</strong> Please save this Order Reference. Since you checked out as a guest, you will need this exact reference number to track your shipping status.
</p>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<button onClick={() => window.location.href = '/track'} style={{ padding: '14px 24px', background: '#0e53c8', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold' }}>
Track Order Status
</button>
<button onClick={() => window.location.href = '/catalog'} style={{ padding: '14px 24px', background: 'transparent', color: '#0e53c8', border: '1px solid #0e53c8', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold' }}>
Continue Shopping
</button>
</div>
</div>
);
}