fix: remove orphaned canvas code from RiderCardPage causing ctx/Printer is not defined crash

The drawCardToCanvas function had an old version of the card rendering logic
that leaked outside the function body (lines 505-731 were orphaned code).
This code referenced local variables (ctx, p, data, qrX, qrSize, PAD, etc.)
that only existed inside the function, causing ReferenceError at module load time
and crashing the entire app with a blank screen.

Also added missing lucide-react imports (Printer, Loader2, Share2, Download,
Check, Copy, Award, Zap, Info, Palette, Shield, ChevronUp, ChevronDown) that
were used in the JSX but not imported, causing the secondary Printer is not
defined ReferenceError.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-06-16 17:02:59 +07:00
parent 125bce3637
commit ebbf2d6326
8 changed files with 50 additions and 243 deletions

View file

@ -6,7 +6,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { toast } from '@/hooks/useToast';
import QRCode from 'qrcode';
export function SecuritySettings() {
const { user } = useCurrentUser();

View file

@ -7,7 +7,7 @@ import { useNWC } from '@/hooks/useNWCContext';
import { getInvoiceFromLightningAddress } from '@/lib/lightningAddress';
import { LISTING_FEE_SATS, PLATFORM_LIGHTNING_ADDRESS } from '@/hooks/useListingRestrictions';
import { Zap, Loader2, CheckCircle, Copy, ExternalLink, ShieldAlert, TrendingUp, MapPin } from 'lucide-react';
import QRCode from 'qrcode';
// qrcode imported lazily in async function
interface SpamFeePaymentProps {
feeRecipient: string;
@ -52,6 +52,7 @@ export function SpamFeePayment({
setInvoice(bolt11);
// Generate QR code
const { default: QRCode } = await import('qrcode');
const qr = await QRCode.toDataURL(bolt11.toUpperCase(), {
width: 256,
margin: 2,

View file

@ -35,7 +35,7 @@ import { useCashu } from '@/hooks/wallet/useCashu';
import { useLightningOperations } from '@/hooks/wallet/useLightningOperations';
import { WalletModal } from '@/components/WalletModal';
import type { Event } from 'nostr-tools';
import QRCode from 'qrcode';
// qrcode imported lazily in async function to avoid canvas crash at module load
import type { WebLNProvider } from "@webbtc/webln-types";
interface ZapDialogProps {
@ -443,6 +443,7 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
}
try {
const { default: QRCode } = await import('qrcode');
const url = await QRCode.toDataURL(invoice.toUpperCase(), {
width: 512,
margin: 2,

View file

@ -14,7 +14,7 @@ import {
mnemonicToWords,
wordsToMnemonic
} from '@/lib/nip06';
import QRCode from 'qrcode';
// qrcode imported lazily in async function
interface MnemonicSignupDialogProps {
isOpen: boolean;
@ -95,6 +95,7 @@ const MnemonicSignupDialog: React.FC<MnemonicSignupDialogProps> = ({
// Generate QR code
const handleQR = async () => {
try {
const { default: QRCode } = await import('qrcode');
const qrDataUrl = await QRCode.toDataURL(mnemonic, {
width: 300,
margin: 2,

View file

@ -1,5 +1,4 @@
import QRCode from 'qrcode';
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
interface QRCodeCanvasProps {
value: string;
@ -14,11 +13,21 @@ export function QRCodeCanvas({ value, size = 256, level = 'M', className }: QRCo
useEffect(() => {
if (!canvasRef.current || !value || value.length < 4) return;
const el = canvasRef.current;
try {
QRCode.toCanvas(el, value, { width: size, margin: 1, errorCorrectionLevel: level });
} catch {
// silently skip on invalid input
}
// Lazy import to avoid crashing at module load in restricted environments
import('qrcode').then((mod) => {
try {
mod.default.toCanvas(el, value, {
width: size,
margin: 1,
errorCorrectionLevel: level,
});
} catch {
// silently skip
}
}).catch(() => {
// qrcode unavailable in this environment
});
}, [value, size, level]);
return <canvas ref={canvasRef} width={size} height={size} className={className} />;

View file

@ -1,5 +1,6 @@
import { useState, useCallback, useRef } from 'react';
import { Html5Qrcode } from 'html5-qrcode';
// Html5Qrcode imported lazily inside startScanning to avoid canvas crash at module load
type Html5QrcodeType = import('html5-qrcode').Html5Qrcode;
export type QRScanResult =
| { type: 'lightning_invoice'; value: string }
@ -49,7 +50,7 @@ export function classifyQRCode(decodedText: string): QRScanResult {
export function useQRCodeScanner() {
const [isScanning, setIsScanning] = useState(false);
const [error, setError] = useState<string | null>(null);
const scannerRef = useRef<Html5Qrcode | null>(null);
const scannerRef = useRef<Html5QrcodeType | null>(null);
const isStartingRef = useRef(false);
const shouldCancelRef = useRef(false);
@ -61,6 +62,9 @@ export function useQRCodeScanner() {
shouldCancelRef.current = false;
setError(null);
// Lazy load to avoid canvas crash at module load time
const { Html5Qrcode } = await import('html5-qrcode');
// Find available cameras
const devices = await Html5Qrcode.getCameras();
if (devices.length === 0) {

View file

@ -1,13 +1,12 @@
import QRCode from 'qrcode';
/**
* Generate a QR code data URL from text
*
* qrcode imported lazily to avoid canvas crash at module load
* @param text - Text to encode in QR code
* @returns Promise resolving to data URL
*/
export async function generateQRCode(text: string): Promise<string> {
try {
const { default: QRCode } = await import('qrcode');
const dataUrl = await QRCode.toDataURL(text, {
width: 300,
margin: 2,

View file

@ -16,7 +16,8 @@
import { useState, useRef, useCallback } from 'react';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
import QRCode from 'qrcode';
// QRCode is imported lazily inside drawCardToCanvas() to avoid canvas API
// access at module load time (crashes the preview environment)
import { Layout } from '@/components/Layout';
import { LoginArea } from '@/components/auth/LoginArea';
@ -41,6 +42,21 @@ import { useQuery } from '@tanstack/react-query';
import { genUserName } from '@/lib/genUserName';
import { cn } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
import {
Printer,
Loader2,
Share2,
Download,
Check,
Copy,
Award,
Zap,
Info,
Palette,
Shield,
ChevronUp,
ChevronDown,
} from 'lucide-react';
// ─── Card canvas renderer ─────────────────────────────────────────────────────
// Draws the card entirely via Canvas 2D API — 100% reliable export,
// no CSS capture, no scaling issues, no background-stripping.
@ -192,6 +208,8 @@ async function drawCardToCanvas(data: CanvasCardData): Promise<string> {
const qrY = 28;
let qrImg: HTMLImageElement | null = null;
try {
// Dynamic import — avoids canvas API access at module load time
const QRCode = (await import('qrcode')).default;
const qrDataUrl = await QRCode.toDataURL(qrUrl, {
width: qrSize * 2,
margin: 2,
@ -499,232 +517,6 @@ async function drawCardToCanvas(data: CanvasCardData): Promise<string> {
return canvas.toDataURL('image/png');
}
ctx.fillStyle = p.muted;
ctx.font = '15px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('SCAN TO VERIFY', qrX + qrSize / 2, qrY + qrSize + 22);
ctx.textAlign = 'left';
if (data.type === 'rider') {
// ── Header: brand left, npub right ──────────────────────────────────────
ctx.fillStyle = p.accent;
ctx.font = 'bold 30px system-ui, sans-serif';
ctx.fillText('🛹 SKATESPOTS', PAD, 54);
ctx.fillStyle = p.muted;
ctx.font = '17px system-ui, sans-serif';
ctx.fillText('SKATER LICENSE', PAD, 76);
// Full npub split into two lines to fit (left of QR)
const npubMaxW = qrX - PAD - 20;
const npub1 = data.npub.slice(0, 32);
const npub2 = data.npub.slice(32);
ctx.fillStyle = p.muted;
ctx.font = '14px monospace';
ctx.fillText(npub1, PAD, 100);
ctx.fillStyle = p.muted;
ctx.fillText(npub2, PAD, 118);
// ── Avatar (square, left column) ────────────────────────────────────────
const avatarSize = 180;
const avatarX = PAD;
const avatarY = 135;
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
ctx.fillStyle = p.chipBg;
ctx.fill();
if (data.avatarUrl) {
const img = await loadImage(data.avatarUrl);
if (img) {
ctx.save();
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
ctx.clip();
ctx.drawImage(img, avatarX, avatarY, avatarSize, avatarSize);
ctx.restore();
}
}
// ── Identity column (right of avatar, left of QR) ───────────────────────
const idX = PAD + avatarSize + 28;
const idMaxW = qrX - idX - 16;
let cy = 155; // current Y cursor
// Name
ctx.fillStyle = p.text;
ctx.font = 'bold 46px system-ui, sans-serif';
fillTextFit(ctx, data.name.toUpperCase(), idX, cy, idMaxW);
cy += 52;
// nip05
if (data.nip05) {
ctx.fillStyle = p.accent;
ctx.font = '19px system-ui, sans-serif';
fillTextFit(ctx, `@${data.nip05.split('@')[0]}`, idX, cy, idMaxW);
cy += 26;
}
// Style chip
if (data.style) {
ctx.font = 'bold 17px system-ui, sans-serif';
const chipTxt = data.style.toUpperCase();
const chipW = ctx.measureText(chipTxt).width + 22;
roundRect(ctx, idX, cy, chipW, 28, 6);
ctx.fillStyle = p.chipBg;
ctx.fill();
ctx.fillStyle = p.accent;
ctx.fillText(chipTxt, idX + 11, cy + 19);
cy += 38;
}
// ── SHOP section (explicit labeled) ─────────────────────────────────────
if (data.ridesFor) {
ctx.fillStyle = p.muted;
ctx.font = '13px system-ui, sans-serif';
ctx.fillText('SKATE SHOP', idX, cy);
cy += 4;
ctx.fillStyle = p.accent;
ctx.font = 'bold 19px system-ui, sans-serif';
fillTextFit(ctx, data.ridesFor, idX, cy + 18, idMaxW);
cy += 32;
}
// ── SPONSORS section (explicit labeled) ─────────────────────────────────
if (data.sponsors) {
ctx.fillStyle = p.muted;
ctx.font = '13px system-ui, sans-serif';
ctx.fillText('SPONSORS', idX, cy);
cy += 4;
ctx.fillStyle = p.text;
ctx.font = '18px system-ui, sans-serif';
fillTextFit(ctx, data.sponsors, idX, cy + 18, idMaxW);
cy += 32;
}
// ── Stats row ────────────────────────────────────────────────────────────
const statItems = [
data.showSpots ? { label: 'SPOTS', value: String(data.spotsAdded), color: p.accent } : null,
data.showZaps ? { label: 'ZAPS ⚡', value: String(data.zapsReceived), color: p.accent2 } : null,
data.showEvents ? { label: 'EVENTS', value: String(data.eventsCreated), color: '#ff6600' } : null,
].filter(Boolean) as { label: string; value: string; color: string }[];
if (statItems.length > 0) {
cy = Math.max(cy, 330);
const statW = idMaxW / statItems.length;
statItems.forEach((s, i) => {
const sx = idX + i * statW;
ctx.fillStyle = p.muted;
ctx.font = '13px system-ui, sans-serif';
ctx.fillText(s.label, sx, cy);
ctx.fillStyle = s.color;
ctx.font = 'bold 28px system-ui, sans-serif';
ctx.fillText(s.value, sx, cy + 30);
});
cy += 50;
}
// ── Skill badges ─────────────────────────────────────────────────────────
const skillsToShow = [
...data.skills.slice(0, 5).map(id => {
const s = SKILL_OPTIONS.find(o => o.id === id);
return s ? { label: `${s.label}`, gold: false } : null;
}).filter(Boolean),
...data.receivedBadges.slice(0, 3).map(b => {
const s = SKILL_OPTIONS.find(o => o.id === b.skill);
return s ? { label: `★ ${s.label}`, gold: true } : null;
}).filter(Boolean),
] as { label: string; gold: boolean }[];
if (skillsToShow.length > 0) {
cy = Math.max(cy, 390);
ctx.font = 'bold 15px system-ui, sans-serif';
let chipX = idX;
skillsToShow.forEach(chip => {
const chipW = ctx.measureText(chip.label).width + 18;
if (chipX + chipW > qrX - 8) { chipX = idX; cy += 30; }
roundRect(ctx, chipX, cy, chipW, 24, 5);
ctx.fillStyle = chip.gold ? 'rgba(255,215,0,0.2)' : p.chipBg;
ctx.fill();
ctx.fillStyle = chip.gold ? '#ffd700' : p.accent;
ctx.fillText(chip.label, chipX + 9, cy + 17);
chipX += chipW + 7;
});
}
// ── Since year (under avatar) ────────────────────────────────────────────
const sinceY = avatarY + avatarSize + 22;
ctx.fillStyle = p.muted;
ctx.font = '14px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`SINCE ${data.memberYear}`, PAD + avatarSize / 2, sinceY);
ctx.textAlign = 'left';
} else {
// ── PARK ACCESS card ─────────────────────────────────────────────────────
ctx.fillStyle = p.accent;
ctx.font = 'bold 30px system-ui, sans-serif';
ctx.fillText(`🏟️ ${(data.issuerName || 'SKATEPARK').toUpperCase()}`, PAD, 54);
ctx.fillStyle = p.muted;
ctx.font = '17px system-ui, sans-serif';
ctx.fillText('ACCESS PASS', PAD, 76);
// Access level chip
ctx.font = 'bold 19px system-ui, sans-serif';
const alTxt = (data.accessLevel || 'FULL ACCESS').toUpperCase();
const alW = ctx.measureText(alTxt).width + 28;
roundRect(ctx, PAD, 90, alW, 32, 7);
ctx.fillStyle = p.chipBg;
ctx.fill();
ctx.fillStyle = p.accent;
ctx.fillText(alTxt, PAD + 14, 111);
// Avatar
const avatarSize = 180;
const avatarX = PAD;
const avatarY = 140;
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
ctx.fillStyle = p.chipBg;
ctx.fill();
if (data.avatarUrl) {
const img = await loadImage(data.avatarUrl);
if (img) {
ctx.save();
roundRect(ctx, avatarX, avatarY, avatarSize, avatarSize, 14);
ctx.clip();
ctx.drawImage(img, avatarX, avatarY, avatarSize, avatarSize);
ctx.restore();
}
}
const idX = PAD + avatarSize + 28;
const idMaxW = qrX - idX - 16;
let cy = 170;
ctx.fillStyle = p.text;
ctx.font = 'bold 44px system-ui, sans-serif';
fillTextFit(ctx, (data.recipientName || 'MEMBER NAME').toUpperCase(), idX, cy, idMaxW);
cy += 50;
// Full npub split into two lines
ctx.fillStyle = p.muted;
ctx.font = '14px monospace';
ctx.fillText(data.npub.slice(0, 32), idX, cy);
cy += 20;
ctx.fillText(data.npub.slice(32), idX, cy);
cy += 28;
ctx.fillStyle = p.muted;
ctx.font = '16px system-ui, sans-serif';
ctx.fillText('Issued by park owner · Nostr-signed', idX, cy);
cy += 28;
if (data.validThru) {
ctx.fillStyle = p.muted;
ctx.font = '14px system-ui, sans-serif';
ctx.fillText('VALID THRU', idX, cy);
cy += 4;
ctx.fillStyle = p.accent;
ctx.font = 'bold 28px system-ui, sans-serif';
ctx.fillText(data.validThru, idX, cy + 26);
}
}
// ─── Nostr kinds ──────────────────────────────────────────────────────────────
export const PARK_ACCESS_CARD_KIND = 35679; // Park issues to rider