bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
106 lines (105 loc) • 7.69 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useState } from "react";
import { useAuthMessages } from "../../lib/auth-messages.js";
import { CONTAINER_WIDTHS } from "../../lib/layout-constants.js";
import { ErrorDisplay } from "../ui-components/ErrorDisplay.js";
import { LoadingButton } from "../ui-components/LoadingButton.js";
import { QRCodeRenderer } from "../ui-components/QRCodeRenderer.js";
import { Alert, AlertDescription } from "../ui/alert.js";
import { Button } from "../ui/button.js";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card.js";
export function DeviceLinkQR({ onLinkGenerated, onError }) {
const messages = useAuthMessages();
const [qrCodeUrl, setQrCodeUrl] = useState(null);
const [linkUrl, setLinkUrl] = useState(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [timeRemaining, setTimeRemaining] = useState(0);
const [copied, setCopied] = useState(false);
const formatTime = useCallback((seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
}, []);
const generateQRCode = useCallback(async () => {
setLoading(true);
setError(null);
let intervalId = null;
try {
// For now, simulate the API call since backend isn't implemented
// In a real implementation, this would call your backend
const mockResponse = {
url: `${window.location.origin}/device-link/mock-token-${Date.now()}`,
token: `token-${Date.now().toString().slice(-8)}`,
expiresIn: 600, // 10 minutes
};
setLinkUrl(mockResponse.url);
setToken(mockResponse.token);
setTimeRemaining(mockResponse.expiresIn);
onLinkGenerated?.(mockResponse.url, mockResponse.token);
// Generate QR code (using a simple text representation for now)
// In a real implementation, you'd use a QR code library
const qrText = `QR Code would be generated here for: ${mockResponse.url}`;
setQrCodeUrl(qrText);
// Start countdown
intervalId = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
if (intervalId)
clearInterval(intervalId);
setQrCodeUrl(null);
setLinkUrl(null);
setToken(null);
return 0;
}
return prev - 1;
});
}, 1000);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : messages.errorDeviceLinkFailed;
setError(errorMessage);
onError?.(errorMessage);
}
finally {
setLoading(false);
}
// Return cleanup function
return () => {
if (intervalId)
clearInterval(intervalId);
};
}, [onLinkGenerated, onError, messages.errorDeviceLinkFailed]);
const copyToClipboard = useCallback(async () => {
if (!linkUrl)
return;
try {
await navigator.clipboard.writeText(linkUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
catch (err) {
console.error("Failed to copy to clipboard:", err);
}
}, [linkUrl]);
const resetQRCode = useCallback(() => {
setQrCodeUrl(null);
setLinkUrl(null);
setToken(null);
setTimeRemaining(0);
setError(null);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
setTimeRemaining(0);
};
}, []);
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-center", children: messages.deviceLinkTitle }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: messages.deviceLinkSubtitle })] }), _jsx(CardContent, { children: _jsxs("div", { className: "flex flex-col gap-6", children: [error && _jsx(ErrorDisplay, { error: error }), !qrCodeUrl ? (_jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Generate a QR code to quickly sign in on another device" }), _jsx(LoadingButton, { onClick: generateQRCode, loading: loading, size: "lg", variant: "default", children: messages.deviceLinkGenerateButton })] })) : (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx("div", { className: "flex flex-col gap-2", children: messages.deviceLinkInstructions.map((instruction, index) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-xs font-bold text-primary", children: [index + 1, "."] }), _jsx("span", { className: "text-sm text-muted-foreground", children: instruction })] }, instruction))) }), _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-center gap-3 p-6", children: [linkUrl ? (_jsx(QRCodeRenderer, { data: linkUrl, size: 200, qrStyle: "dots", themeAware: true, bgColor: "var(--background)", fgColor: "var(--primary)" })) : (_jsx("div", { className: "flex items-center justify-center border-2 border-dashed border-muted-foreground rounded-lg", style: {
width: CONTAINER_WIDTHS.POPOVER_SMALL,
height: CONTAINER_WIDTHS.POPOVER_SMALL,
}, children: _jsx("span", { className: "text-xs text-muted-foreground text-center", children: "Generating..." }) })), timeRemaining > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-primary", children: messages.deviceLinkExpires }), _jsx("code", { className: "px-2 py-1 bg-primary/10 text-primary rounded text-xs", children: formatTime(timeRemaining) })] }))] }) }), linkUrl && (_jsx(Card, { className: "bg-muted/50", children: _jsxs(CardContent, { className: "flex flex-col gap-3 p-4", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Or visit manually:" }), _jsx("code", { className: "text-xs bg-background p-2 rounded border break-all", children: linkUrl }), _jsxs("div", { className: "flex justify-between items-center", children: [token && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["Token: ", token.substring(0, 8), "..."] })), _jsx(Button, { size: "sm", variant: "ghost", onClick: copyToClipboard, className: copied ? "text-green-600" : "", children: copied
? messages.deviceLinkCopied
: messages.deviceLinkCopyButton })] })] }) })), _jsxs("div", { className: "flex gap-3 justify-center", children: [_jsx(Button, { variant: "ghost", onClick: resetQRCode, children: messages.cancel }), _jsx(Button, { variant: "ghost", onClick: generateQRCode, disabled: loading, children: messages.deviceLinkNewCode })] }), _jsx(Alert, { className: "bg-amber-50 border-amber-200", children: _jsx(AlertDescription, { className: "text-amber-900", children: "The QR code contains a one-time link that expires in 10 minutes. You'll need to enter your encryption password on the new device." }) })] }))] }) })] }));
}