UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

15 lines 6.27 kB
{ "name": "backup-recovery-devicelinkqr", "type": "registry:component", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/backup-recovery/DeviceLinkQR.tsx", "type": "registry:component", "content": "import {\n\tCheckIcon,\n\tComponentInstanceIcon,\n\tCopyIcon,\n\tReloadIcon,\n} from \"@radix-ui/react-icons\";\nimport React, { useState } from \"react\";\nimport { useAuthMessages } from \"../../lib/auth-messages.js\";\nimport { LoadingButton } from \"../ui-components/LoadingButton.js\";\nimport { QRCodeRenderer } from \"../ui-components/QRCodeRenderer.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card.js\";\n\nexport interface DeviceLinkQRProps {\n\tonGenerateQR: () => Promise<{\n\t\tqrData: string;\n\t\ttoken: string;\n\t\texpiresAt: Date;\n\t}>;\n\tbaseUrl?: string;\n}\n\nexport function DeviceLinkQR({\n\tonGenerateQR,\n\tbaseUrl = window.location.origin,\n}: DeviceLinkQRProps) {\n\tconst messages = useAuthMessages();\n\tconst [qrData, setQrData] = useState<string | null>(null);\n\tconst [linkUrl, setLinkUrl] = useState<string | null>(null);\n\tconst [expiresAt, setExpiresAt] = useState<Date | null>(null);\n\tconst [loading, setLoading] = useState(false);\n\tconst [error, setError] = useState<string | null>(null);\n\tconst [copied, setCopied] = useState(false);\n\n\tconst handleGenerateQR = async () => {\n\t\tsetLoading(true);\n\t\tsetError(null);\n\t\ttry {\n\t\t\tconst result = await onGenerateQR();\n\t\t\tsetQrData(result.qrData);\n\t\t\tsetLinkUrl(`${baseUrl}/link-device?token=${result.token}`);\n\t\t\tsetExpiresAt(result.expiresAt);\n\t\t} catch (err) {\n\t\t\tsetError(\n\t\t\t\terr instanceof Error ? err.message : \"Failed to generate QR code\",\n\t\t\t);\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t};\n\n\tconst handleCopyLink = async () => {\n\t\tif (!linkUrl) return;\n\n\t\ttry {\n\t\t\tawait navigator.clipboard.writeText(linkUrl);\n\t\t\tsetCopied(true);\n\t\t\tsetTimeout(() => setCopied(false), 2000);\n\t\t} catch {\n\t\t\tsetError(\"Failed to copy link\");\n\t\t}\n\t};\n\n\tconst handleReset = React.useCallback(() => {\n\t\tsetQrData(null);\n\t\tsetLinkUrl(null);\n\t\tsetExpiresAt(null);\n\t\tsetError(null);\n\t}, []);\n\n\tconst getTimeRemaining = React.useCallback(() => {\n\t\tif (!expiresAt) return null;\n\t\tconst now = new Date();\n\t\tconst diff = expiresAt.getTime() - now.getTime();\n\t\tif (diff <= 0) return \"Expired\";\n\t\tconst minutes = Math.floor(diff / 60000);\n\t\tconst seconds = Math.floor((diff % 60000) / 1000);\n\t\treturn `${minutes}:${seconds.toString().padStart(2, \"0\")}`;\n\t}, [expiresAt]);\n\n\tReact.useEffect(() => {\n\t\tif (!expiresAt) return;\n\n\t\tconst interval = setInterval(() => {\n\t\t\tconst remaining = getTimeRemaining();\n\t\t\tif (remaining === \"Expired\") {\n\t\t\t\thandleReset();\n\t\t\t\tclearInterval(interval);\n\t\t\t}\n\t\t}, 1000);\n\n\t\treturn () => clearInterval(interval);\n\t}, [expiresAt, getTimeRemaining, handleReset]);\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<div className=\"flex flex-col items-center gap-1\">\n\t\t\t\t<h2 className=\"text-xl font-semibold\">{messages.deviceLinkTitle}</h2>\n\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t{messages.deviceLinkSubtitle}\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t{error && (\n\t\t\t\t<Card className=\"bg-destructive/10\">\n\t\t\t\t\t<CardContent className=\"p-3\">\n\t\t\t\t\t\t<p className=\"text-sm text-destructive\">{error}</p>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t)}\n\n\t\t\t{!qrData ? (\n\t\t\t\t<LoadingButton\n\t\t\t\t\tonClick={handleGenerateQR}\n\t\t\t\t\tloading={loading}\n\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\tvariant=\"default\"\n\t\t\t\t>\n\t\t\t\t\t<ComponentInstanceIcon className=\"h-4 w-4 mr-2\" />\n\t\t\t\t\tGenerate QR Code\n\t\t\t\t</LoadingButton>\n\t\t\t) : (\n\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t<Card className=\"bg-muted/50 border\">\n\t\t\t\t\t\t<CardContent className=\"p-4\">\n\t\t\t\t\t\t\t<QRCodeRenderer\n\t\t\t\t\t\t\t\tdata={qrData}\n\t\t\t\t\t\t\t\tsize={256}\n\t\t\t\t\t\t\t\tqrStyle=\"dots\"\n\t\t\t\t\t\t\t\tthemeAware={true}\n\t\t\t\t\t\t\t\tbgColor=\"var(--background)\"\n\t\t\t\t\t\t\t\tfgColor=\"var(--primary)\"\n\t\t\t\t\t\t\t\tstyle={{ width: \"100%\", height: \"auto\" }}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</Card>\n\n\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">Expires in:</span>\n\t\t\t\t\t\t\t<span className=\"text-sm font-mono\">{getTimeRemaining()}</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={handleCopyLink}\n\t\t\t\t\t\t\tsize=\"default\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{copied ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<CheckIcon className=\"h-4 w-4 mr-2\" />\n\t\t\t\t\t\t\t\t\tCopied!\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<CopyIcon className=\"h-4 w-4 mr-2\" />\n\t\t\t\t\t\t\t\t\tCopy Link\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Button>\n\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={handleReset}\n\t\t\t\t\t\t\tsize=\"default\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ReloadIcon className=\"h-4 w-4 mr-2\" />\n\t\t\t\t\t\t\tGenerate New Code\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"flex flex-col gap-1\">\n\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t1. Scan this QR code on your other device\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t2. Enter your password to decrypt your backup\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t3. Your identity will be linked to both devices\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n", "target": "<%- config.aliases.components %>/devicelinkqr.tsx" } ] }