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 types

Generate 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
});
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);
}