From ebbf2d632640c5a57a2359330f51daa9af198c35 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Tue, 16 Jun 2026 17:02:59 +0700 Subject: [PATCH] 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 --- src/components/SecuritySettings.tsx | 2 +- src/components/SpamFeePayment.tsx | 3 +- src/components/ZapDialog.tsx | 3 +- src/components/auth/MnemonicSignupDialog.tsx | 3 +- src/components/ui/qrcode.tsx | 23 +- src/hooks/useQRCodeScanner.ts | 8 +- src/lib/wallet/qrCodeGenerator.ts | 5 +- src/pages/RiderCardPage.tsx | 246 ++----------------- 8 files changed, 50 insertions(+), 243 deletions(-) diff --git a/src/components/SecuritySettings.tsx b/src/components/SecuritySettings.tsx index 59c46a5..5547d8f 100644 --- a/src/components/SecuritySettings.tsx +++ b/src/components/SecuritySettings.tsx @@ -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(); diff --git a/src/components/SpamFeePayment.tsx b/src/components/SpamFeePayment.tsx index 0d2da57..c6fc363 100644 --- a/src/components/SpamFeePayment.tsx +++ b/src/components/SpamFeePayment.tsx @@ -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, diff --git a/src/components/ZapDialog.tsx b/src/components/ZapDialog.tsx index 27dd60e..c2388d3 100644 --- a/src/components/ZapDialog.tsx +++ b/src/components/ZapDialog.tsx @@ -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, diff --git a/src/components/auth/MnemonicSignupDialog.tsx b/src/components/auth/MnemonicSignupDialog.tsx index d7b3f0b..1047f8d 100644 --- a/src/components/auth/MnemonicSignupDialog.tsx +++ b/src/components/auth/MnemonicSignupDialog.tsx @@ -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 = ({ // Generate QR code const handleQR = async () => { try { + const { default: QRCode } = await import('qrcode'); const qrDataUrl = await QRCode.toDataURL(mnemonic, { width: 300, margin: 2, diff --git a/src/components/ui/qrcode.tsx b/src/components/ui/qrcode.tsx index 81df85c..6327e1b 100644 --- a/src/components/ui/qrcode.tsx +++ b/src/components/ui/qrcode.tsx @@ -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 ; diff --git a/src/hooks/useQRCodeScanner.ts b/src/hooks/useQRCodeScanner.ts index 1460af1..40fbf96 100644 --- a/src/hooks/useQRCodeScanner.ts +++ b/src/hooks/useQRCodeScanner.ts @@ -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(null); - const scannerRef = useRef(null); + const scannerRef = useRef(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) { diff --git a/src/lib/wallet/qrCodeGenerator.ts b/src/lib/wallet/qrCodeGenerator.ts index 404c099..67577d8 100644 --- a/src/lib/wallet/qrCodeGenerator.ts +++ b/src/lib/wallet/qrCodeGenerator.ts @@ -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 { try { + const { default: QRCode } = await import('qrcode'); const dataUrl = await QRCode.toDataURL(text, { width: 300, margin: 2, diff --git a/src/pages/RiderCardPage.tsx b/src/pages/RiderCardPage.tsx index 7442e69..54c6152 100644 --- a/src/pages/RiderCardPage.tsx +++ b/src/pages/RiderCardPage.tsx @@ -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 { 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 { 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