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. Solana Pay includes a built-in createQR function powered by @solana/qr-code-styling — no additional QR code packages needed.
Basic QR Code Generation
Generate and Display a QR Code
import { address } from '@solana/kit';
import { encodeURL, createQR } from '@solana/pay';
// Create payment URL
const url = encodeURL({
recipient: address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB'),
amount: 0.05,
label: 'Coffee Shop',
message: 'Grande Americano',
});
// Generate QR code and append to DOM
const qr = createQR(url);
qr.append(document.getElementById('payment-qr'));Customizing QR Codes
The createQR function accepts optional parameters for size, background color, and foreground color:
// createQR(url, size?, background?, color?)
const qr = createQR(url, 400, '#F5F5F5', '#512DA8');
qr.append(document.getElementById('payment-qr'));Getting QR Options for Advanced Use
Use createQROptions to get the raw configuration object for more control:
import { createQROptions } from '@solana/pay';
const options = createQROptions(url, 300, '#ffffff', '#000000');
// Pass to @solana/qr-code-styling or customize furtherUsing the Merchant Client
The merchant client also exposes QR code methods:
import { address } from '@solana/kit';
import { createMerchantClient } from '@solana/pay';
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
const recipient = address('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB');
const url = merchant.pay.encodeURL({ recipient, amount: 1.5 });
// Generate QR code
const qr = merchant.pay.createQR(url);
qr.append(document.getElementById('qr-code'));React Integration
Basic React Component
import { useEffect, useRef } from 'react';
import { address } from '@solana/kit';
import { encodeURL, createQR } from '@solana/pay';
function PaymentQR({ recipient, amount, label, message }) {
const qrRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const url = encodeURL({
recipient: address(recipient),
amount,
label,
message,
});
const qr = createQR(url, 300);
// Clear previous QR code and append new one
if (qrRef.current) {
qrRef.current.innerHTML = '';
qr.append(qrRef.current);
}
}, [recipient, amount, label, message]);
return (
<div>
<div ref={qrRef} />
<p className="text-center mt-2 text-sm text-gray-600">
Scan with your Solana wallet
</p>
</div>
);
}
// Usage
<PaymentQR
recipient="FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB"
amount={1.5}
label="Online Store"
message="Order #12345"
/>Payment QR with Status Updates
import { useEffect, useRef, useState } from 'react';
import type { Address } from '@solana/kit';
import { address, generateKeyPair, getAddressFromPublicKey } from '@solana/kit';
import { encodeURL, createQR, createMerchantClient } from '@solana/pay';
function PaymentQRWithStatus({ recipient, amount, label }) {
const qrRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<'pending' | 'confirmed' | 'timeout'>('pending');
const [reference, setReference] = useState<Address | null>(null);
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
let timeout: ReturnType<typeof setTimeout>;
let cancelled = false;
async function setup() {
// Generate unique reference
const keypair = await generateKeyPair();
const ref = await getAddressFromPublicKey(keypair.publicKey);
setReference(ref);
const url = encodeURL({
recipient: address(recipient),
amount,
reference: ref,
label,
});
const qr = createQR(url, 300);
if (qrRef.current) {
qrRef.current.innerHTML = '';
qr.append(qrRef.current);
}
// Start monitoring
const merchant = createMerchantClient({
rpcUrl: 'https://api.mainnet-beta.solana.com',
});
interval = setInterval(async () => {
try {
const found = await merchant.pay.findReference(ref);
await merchant.pay.validateTransfer(found.signature, {
recipient: address(recipient),
amount,
reference: ref,
});
if (!cancelled) {
setStatus('confirmed');
clearInterval(interval);
clearTimeout(timeout);
}
} catch {
// Not found yet
}
}, 2000);
// Timeout after 5 minutes
timeout = setTimeout(() => {
clearInterval(interval);
if (!cancelled) setStatus('timeout');
}, 5 * 60 * 1000);
}
setup();
return () => {
cancelled = true;
clearInterval(interval);
clearTimeout(timeout);
};
}, [recipient, amount, label]);
return (
<div className="relative">
<div ref={qrRef} />
{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">
<p className="font-semibold">Payment Confirmed!</p>
</div>
</div>
)}
{status === 'timeout' && (
<p className="text-center mt-2 text-sm text-red-600">
Payment timed out. Please try again.
</p>
)}
</div>
);
}Point of Sale Integration
POS Terminal QR Display
import { address, generateKeyPair, getAddressFromPublicKey } from '@solana/kit';
import { encodeURL, createQR, createMerchantClient } from '@solana/pay';
class POSTerminal {
private merchantWallet;
private merchant;
constructor(merchantWallet, rpcUrl) {
this.merchantWallet = address(merchantWallet);
this.merchant = createMerchantClient({ rpcUrl });
}
async createOrder(items, customLabel) {
const total = items.reduce((sum, item) => sum + item.price, 0);
const keypair = await generateKeyPair();
const reference = await getAddressFromPublicKey(keypair.publicKey);
const orderId = Date.now().toString();
const url = this.merchant.pay.encodeURL({
recipient: this.merchantWallet,
amount: total,
reference,
label: customLabel || 'Point of Sale',
message: `Receipt #${orderId}`,
memo: `POS-${orderId}`,
});
const qr = this.merchant.pay.createQR(url, 400);
return { orderId, total, reference, qr, url };
}
async monitorPayment(reference, amount, callback) {
const checkPayment = async () => {
try {
const found = await this.merchant.pay.findReference(reference);
await this.merchant.pay.validateTransfer(found.signature, {
recipient: this.merchantWallet,
amount,
reference,
});
callback({ success: true, signature: found.signature });
return true;
} catch {
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('FvJ8k8HhXp4a3zQyFMZd4FvEqcYdYE7gSZWxrEBRfBsB', 'https://api.mainnet-beta.solana.com');
const { qr, reference, total } = await pos.createOrder([
{ name: 'Coffee', price: 3.50 },
{ name: 'Muffin', price: 2.25 },
], 'Local Coffee Shop');
// Display QR code
qr.append(document.getElementById('pos-display'));
// Monitor for payment
pos.monitorPayment(reference, total, (result) => {
if (result.success) {
console.log('Payment received!', result.signature);
} else {
console.log('Payment failed or timed out');
}
});Mobile-Optimized Display
On mobile devices, consider providing a direct link alongside the QR code:
function MobilePayment({ paymentUrl }) {
const qrRef = useRef(null);
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
useEffect(() => {
const qr = createQR(paymentUrl, isMobile ? 250 : 300);
if (qrRef.current) {
qrRef.current.innerHTML = '';
qr.append(qrRef.current);
}
}, [paymentUrl, isMobile]);
return (
<div>
<div ref={qrRef} />
{isMobile && (
<button
onClick={() => window.open(paymentUrl.toString())}
className="mt-4 bg-purple-600 text-white px-6 py-3 rounded-lg w-full"
>
Open in Wallet
</button>
)}
</div>
);
}Accessibility
function AccessiblePaymentQR({ paymentUrl, amount, label }) {
const qrRef = useRef(null);
useEffect(() => {
const qr = createQR(paymentUrl);
if (qrRef.current) {
qrRef.current.innerHTML = '';
qr.append(qrRef.current);
}
}, [paymentUrl]);
return (
<div role="img" aria-label={`Payment QR code for ${amount} SOL to ${label}`}>
<div ref={qrRef} />
{/* 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>
);
}