QR Code Integration
Generate and customize QR codes for mobile wallet payments
QR Code Integration
QR codes are the most common way to bridge the gap between web applications and mobile wallets. This guide shows you how to generate, customize, and integrate Solana Pay QR codes into your applications.
Basic QR Code Generation
Install Dependencies
pnpm add qrcode canvas
pnpm add -D @types/qrcode # TypeScript typesGenerate Basic QR Code
import QRCode from 'qrcode';
import { createTransferRequestURL } from '@solana/pay';
import { PublicKey } from '@solana/web3.js';
import BigNumber from 'bignumber.js';
async function generatePaymentQR() {
// Create payment URL
const url = createTransferRequestURL({
recipient: new PublicKey('recipient-wallet-address'),
amount: new BigNumber(0.05),
label: 'Coffee Shop',
message: 'Grande Americano',
});
// Generate QR code as data URL
const qrDataURL = await QRCode.toDataURL(url.toString(), {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff'
}
});
return qrDataURL;
}
// Display in HTML
const qrCode = await generatePaymentQR();
document.getElementById('payment-qr').src = qrCode;React Component
import React, { useState, useEffect } from 'react';
import QRCode from 'qrcode';
import { createTransferRequestURL } from '@solana/pay';
function PaymentQR({ paymentRequest }) {
const [qrCode, setQrCode] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function generateQR() {
try {
const url = createTransferRequestURL(paymentRequest);
const qrDataURL = await QRCode.toDataURL(url.toString(), {
width: 300,
margin: 2,
errorCorrectionLevel: 'M',
});
setQrCode(qrDataURL);
} catch (error) {
console.error('QR generation failed:', error);
} finally {
setLoading(false);
}
}
generateQR();
}, [paymentRequest]);
if (loading) {
return <div className="animate-pulse bg-gray-200 w-64 h-64"></div>;
}
return (
<div className="payment-qr">
<img
src={qrCode}
alt="Solana Pay QR Code"
className="border rounded-lg shadow-lg"
/>
<p className="text-center mt-2 text-sm text-gray-600">
Scan with your Solana wallet
</p>
</div>
);
}
// Usage
<PaymentQR
paymentRequest={{
recipient: new PublicKey('wallet-address'),
amount: new BigNumber(1.5),
label: 'Online Store',
message: 'Order #12345'
}}
/>Advanced Customization
Custom Styling Options
async function generateStyledQR(url, options = {}) {
const defaultOptions = {
width: 300,
margin: 2,
errorCorrectionLevel: 'M',
type: 'image/png',
quality: 0.92,
color: {
dark: '#000000',
light: '#ffffff'
}
};
const qrOptions = { ...defaultOptions, ...options };
return await QRCode.toDataURL(url.toString(), qrOptions);
}
// Brand-colored QR code
const brandedQR = await generateStyledQR(paymentUrl, {
width: 400,
margin: 3,
color: {
dark: '#512DA8', // Purple
light: '#F5F5F5', // Light gray
}
});
// High-density QR code
const hdQR = await generateStyledQR(paymentUrl, {
width: 512,
margin: 4,
errorCorrectionLevel: 'H', // High error correction
});QR Code with Logo
import { createCanvas, loadImage } from 'canvas';
async function generateQRWithLogo(url, logoPath) {
// Generate base QR code
const qrDataURL = await QRCode.toDataURL(url.toString(), {
width: 300,
margin: 2,
errorCorrectionLevel: 'H', // High error correction for logo overlay
});
// Create canvas
const canvas = createCanvas(300, 300);
const ctx = canvas.getContext('2d');
// Draw QR code
const qrImage = await loadImage(qrDataURL);
ctx.drawImage(qrImage, 0, 0);
// Load and draw logo
const logo = await loadImage(logoPath);
const logoSize = 60;
const logoX = (300 - logoSize) / 2;
const logoY = (300 - logoSize) / 2;
// White background for logo
ctx.fillStyle = 'white';
ctx.fillRect(logoX - 5, logoY - 5, logoSize + 10, logoSize + 10);
// Draw logo
ctx.drawImage(logo, logoX, logoY, logoSize, logoSize);
return canvas.toDataURL();
}
// Usage
const logoQR = await generateQRWithLogo(paymentUrl, '/logo.png');Animated QR Codes
Create animated QR codes that update with payment status:
import React, { useState, useEffect } from 'react';
function AnimatedPaymentQR({ paymentRequest, onPaymentConfirmed }) {
const [status, setStatus] = useState('pending'); // pending, confirming, confirmed
const [qrCode, setQrCode] = useState('');
useEffect(() => {
generateQR();
startPaymentMonitoring();
}, []);
async function generateQR() {
const url = createTransferRequestURL(paymentRequest);
const qr = await QRCode.toDataURL(url.toString(), {
width: 300,
color: {
dark: status === 'confirmed' ? '#10B981' : '#374151',
light: '#ffffff'
}
});
setQrCode(qr);
}
function startPaymentMonitoring() {
// Monitor blockchain for payment
const interval = setInterval(async () => {
const isConfirmed = await checkPaymentStatus(paymentRequest.reference);
if (isConfirmed) {
setStatus('confirmed');
clearInterval(interval);
onPaymentConfirmed();
}
}, 2000);
}
// Update QR when status changes
useEffect(() => {
generateQR();
}, [status]);
return (
<div className="relative">
<img src={qrCode} alt="Payment QR" className="rounded-lg" />
{status === 'confirmed' && (
<div className="absolute inset-0 flex items-center justify-center bg-green-500 bg-opacity-90 rounded-lg">
<div className="text-white text-center">
<svg className="w-12 h-12 mx-auto mb-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<p className="font-semibold">Payment Confirmed!</p>
</div>
</div>
)}
</div>
);
}Point of Sale Integration
POS Terminal QR Display
class POSTerminal {
constructor(merchantWallet) {
this.merchantWallet = merchantWallet;
this.currentOrder = null;
}
async createOrder(items, customLabel) {
const total = items.reduce((sum, item) => sum + item.price, 0);
const reference = Keypair.generate().publicKey;
this.currentOrder = {
id: Date.now().toString(),
items,
total,
reference,
timestamp: new Date(),
};
const paymentUrl = createTransferRequestURL({
recipient: this.merchantWallet,
amount: new BigNumber(total),
reference,
label: customLabel || 'Point of Sale',
message: `Receipt #${this.currentOrder.id}`,
memo: `POS-${this.currentOrder.id}`,
});
const qrCode = await QRCode.toDataURL(paymentUrl.toString(), {
width: 400,
margin: 3,
errorCorrectionLevel: 'M',
});
return {
order: this.currentOrder,
qrCode,
paymentUrl: paymentUrl.toString(),
};
}
async monitorPayment(reference, callback) {
const connection = new Connection(process.env.SOLANA_RPC_URL);
const checkPayment = async () => {
try {
const signature = await findTransactionSignature(
connection,
reference,
{ finality: 'confirmed' }
);
if (signature) {
callback({ success: true, signature, order: this.currentOrder });
return true;
}
} catch (error) {
// Payment not found yet
}
return false;
};
const interval = setInterval(async () => {
const found = await checkPayment();
if (found) clearInterval(interval);
}, 1000);
// Timeout after 5 minutes
setTimeout(() => {
clearInterval(interval);
callback({ success: false, error: 'Payment timeout' });
}, 5 * 60 * 1000);
}
}
// Usage
const pos = new POSTerminal(new PublicKey('merchant-wallet'));
const { qrCode, order } = await pos.createOrder([
{ name: 'Coffee', price: 3.50 },
{ name: 'Muffin', price: 2.25 }
], 'Local Coffee Shop');
// Display QR code
document.getElementById('pos-display').innerHTML = `
<div>
<h3>Total: $${order.total}</h3>
<img src="${qrCode}" alt="Payment QR" />
<p>Scan to pay with Solana</p>
</div>
`;
// Monitor for payment
pos.monitorPayment(order.reference, (result) => {
if (result.success) {
console.log('Payment received!', result.signature);
// Print receipt, update UI, etc.
} else {
console.log('Payment failed or timed out');
}
});Mobile-Optimized QR Codes
function generateMobileQR(url) {
// Detect if user is on mobile
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test(navigator.userAgent);
const options = {
width: isMobile ? 250 : 300,
margin: isMobile ? 1 : 2,
errorCorrectionLevel: 'M',
};
return QRCode.toDataURL(url.toString(), options);
}
// Responsive QR component
function ResponsiveQR({ paymentUrl }) {
const [qrCode, setQrCode] = useState('');
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const mobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test(navigator.userAgent);
setIsMobile(mobile);
generateMobileQR(paymentUrl).then(setQrCode);
}, [paymentUrl]);
return (
<div className={`qr-container ${isMobile ? 'mobile' : 'desktop'}`}>
<img src={qrCode} alt="Payment QR" />
{isMobile ? (
<button
onClick={() => window.open(paymentUrl.toString())}
className="mt-4 bg-purple-600 text-white px-6 py-3 rounded-lg"
>
Open in Wallet
</button>
) : (
<p className="mt-2 text-sm text-gray-600">
Scan with your mobile wallet
</p>
)}
</div>
);
}QR Code Best Practices
Error Handling
async function generateQRWithFallback(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await QRCode.toDataURL(url.toString(), {
width: 300,
errorCorrectionLevel: 'M',
});
} catch (error) {
console.warn(`QR generation attempt ${i + 1} failed:`, error);
if (i === retries - 1) {
// Last attempt failed, return fallback
return generateTextFallback(url);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
function generateTextFallback(url) {
return `data:text/plain;base64,${btoa(url.toString())}`;
}Performance Optimization
// QR code caching
class QRCodeCache {
constructor() {
this.cache = new Map();
}
async getQRCode(url, options = {}) {
const key = this.generateCacheKey(url, options);
if (this.cache.has(key)) {
return this.cache.get(key);
}
const qrCode = await QRCode.toDataURL(url.toString(), options);
this.cache.set(key, qrCode);
// Clean cache if it gets too large
if (this.cache.size > 100) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
return qrCode;
}
generateCacheKey(url, options) {
return `${url.toString()}-${JSON.stringify(options)}`;
}
}
const qrCache = new QRCodeCache();
// Usage
const qrCode = await qrCache.getQRCode(paymentUrl, { width: 300 });Accessibility
function AccessiblePaymentQR({ paymentUrl, amount, label }) {
const [qrCode, setQrCode] = useState('');
useEffect(() => {
QRCode.toDataURL(paymentUrl.toString()).then(setQrCode);
}, [paymentUrl]);
return (
<div role="img" aria-label={`Payment QR code for ${amount} SOL to ${label}`}>
<img
src={qrCode}
alt={`Solana Pay QR code: ${paymentUrl.toString()}`}
className="payment-qr"
/>
{/* Fallback for screen readers */}
<div className="sr-only">
Payment URL: {paymentUrl.toString()}
</div>
{/* Manual URL copy option */}
<details className="mt-2">
<summary className="cursor-pointer text-sm text-gray-600">
Copy payment URL manually
</summary>
<input
type="text"
value={paymentUrl.toString()}
readOnly
className="w-full mt-1 p-2 border rounded text-xs"
onClick={(e) => e.target.select()}
/>
</details>
</div>
);
}Testing QR Codes
QR Code Validation
import jsQR from 'jsqr';
import { createCanvas, loadImage } from 'canvas';
async function validateQRCode(qrDataURL) {
try {
// Load QR image
const img = await loadImage(qrDataURL);
const canvas = createCanvas(img.width, img.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// Decode QR
const qrResult = jsQR(imageData.data, img.width, img.height);
if (qrResult) {
// Validate that it's a valid Solana Pay URL
const isValidSolanaPay = qrResult.data.startsWith('solana:');
return {
valid: isValidSolanaPay,
data: qrResult.data,
error: isValidSolanaPay ? null : 'Not a valid Solana Pay URL'
};
}
return {
valid: false,
error: 'Could not decode QR code'
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
// Test QR code generation
async function testQRGeneration() {
const url = createTransferRequestURL({
recipient: new PublicKey('test-wallet-address'),
amount: new BigNumber(1),
label: 'Test',
});
const qrCode = await QRCode.toDataURL(url.toString());
const validation = await validateQRCode(qrCode);
console.log('QR Validation:', validation);
}