import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { Check, Copy, Loader, ScanQrCode } from 'lucide-react' import { generateSecretKey, getPublicKey } from 'nostr-tools' import { createNostrConnectURI, NostrConnectParams } from 'nostr-tools/nip46' import QrScanner from 'qr-scanner' import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import QrCode from '../QrCode' export default function NostrConnectLogin({ back, onLoginSuccess }: { back: () => void onLoginSuccess: () => void }) { const { t } = useTranslation() const { nostrConnectionLogin, bunkerLogin } = useNostr() const [pending, setPending] = useState(false) const [bunkerInput, setBunkerInput] = useState('') const [copied, setCopied] = useState(false) const [errMsg, setErrMsg] = useState(null) const [nostrConnectionErrMsg, setNostrConnectionErrMsg] = useState(null) const qrContainerRef = useRef(null) const [qrCodeSize, setQrCodeSize] = useState(100) const [isScanning, setIsScanning] = useState(false) const videoRef = useRef(null) const qrScannerRef = useRef(null) const qrScannerCheckTimerRef = useRef(null) const handleInputChange = (e: React.ChangeEvent) => { setBunkerInput(e.target.value) if (errMsg) setErrMsg(null) } const handleLogin = (bunker: string = bunkerInput) => { const _bunker = bunker.trim() if (_bunker.trim() === '') return setPending(true) bunkerLogin(_bunker) .then(() => onLoginSuccess()) .catch((err) => setErrMsg(err.message || 'Login failed')) .finally(() => setPending(false)) } const [loginDetails] = useState(() => { const newPrivKey = generateSecretKey() const newMeta: NostrConnectParams = { clientPubkey: getPublicKey(newPrivKey), relays: DEFAULT_NOSTRCONNECT_RELAY, secret: Math.random().toString(36).substring(7), name: document.location.host, url: document.location.origin } const newConnectionString = createNostrConnectURI(newMeta) return { privKey: newPrivKey, connectionString: newConnectionString } }) useLayoutEffect(() => { const calculateQrSize = () => { if (qrContainerRef.current) { const containerWidth = qrContainerRef.current.offsetWidth const desiredSizeBasedOnWidth = Math.min(containerWidth - 8, containerWidth * 0.9) const newSize = Math.max(100, Math.min(desiredSizeBasedOnWidth, 360)) setQrCodeSize(newSize) } } calculateQrSize() const resizeObserver = new ResizeObserver(calculateQrSize) if (qrContainerRef.current) { resizeObserver.observe(qrContainerRef.current) } return () => { if (qrContainerRef.current) { resizeObserver.unobserve(qrContainerRef.current) } resizeObserver.disconnect() } }, []) useEffect(() => { if (!loginDetails.privKey || !loginDetails.connectionString) return setNostrConnectionErrMsg(null) nostrConnectionLogin(loginDetails.privKey, loginDetails.connectionString) .then(() => onLoginSuccess()) .catch((err) => { console.error('NostrConnectionLogin Error:', err) setNostrConnectionErrMsg( err.message ? `${err.message}. Please reload.` : 'Connection failed. Please reload.' ) }) }, [loginDetails, nostrConnectionLogin, onLoginSuccess]) const copyConnectionString = async () => { if (!loginDetails.connectionString) return navigator.clipboard.writeText(loginDetails.connectionString) setCopied(true) setTimeout(() => setCopied(false), 2000) } const startQrScan = async () => { try { setIsScanning(true) setErrMsg(null) // Wait for next render cycle to ensure video element is in DOM await new Promise((resolve) => setTimeout(resolve, 100)) if (!videoRef.current) { throw new Error('Video element not found') } const hasCamera = await QrScanner.hasCamera() if (!hasCamera) { throw new Error('No camera found') } const qrScanner = new QrScanner( videoRef.current, (result) => { setBunkerInput(result.data) stopQrScan() handleLogin(result.data) }, { highlightScanRegion: true, highlightCodeOutline: true, preferredCamera: 'environment' } ) qrScannerRef.current = qrScanner await qrScanner.start() // Check video feed after a delay qrScannerCheckTimerRef.current = setTimeout(() => { if ( videoRef.current && (videoRef.current.videoWidth === 0 || videoRef.current.videoHeight === 0) ) { setErrMsg('Camera feed not available') } }, 1000) } catch (error) { setErrMsg( `Failed to start camera: ${error instanceof Error ? error.message : 'Unknown error'}. Please check permissions.` ) setIsScanning(false) if (qrScannerCheckTimerRef.current) { clearTimeout(qrScannerCheckTimerRef.current) qrScannerCheckTimerRef.current = null } } } const stopQrScan = () => { if (qrScannerRef.current) { qrScannerRef.current.stop() qrScannerRef.current.destroy() qrScannerRef.current = null } setIsScanning(false) if (qrScannerCheckTimerRef.current) { clearTimeout(qrScannerCheckTimerRef.current) qrScannerCheckTimerRef.current = null } } useEffect(() => { return () => { stopQrScan() } }, []) return (
{nostrConnectionErrMsg && (
{nostrConnectionErrMsg}
)}
0 ? `${Math.max(150, Math.min(qrCodeSize, 320))}px` : 'auto' }} onClick={copyConnectionString} role="button" tabIndex={0} >
{loginDetails.connectionString}
{copied ? : }
OR
{errMsg &&
{errMsg}
}
) }