UNPKG

bigblocks

Version:

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

171 lines (170 loc) 8.42 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useState } from "react"; import { DEFAULT_ALLOWED_BACKUP_TYPES, createUnsupportedBackupError, detectBackupType, isBackupTypeAllowed, } from "../../lib/backup-utils.js"; import { cn } from "../../lib/utils.js"; import { DropZone, ErrorDisplay } from "../ui-components/index.js"; import { Card, CardContent } from "../ui/card.js"; const DEFAULT_MAX_SIZE = 10 * 1024 * 1024; // 10MB export function FileImport({ onFileSelect, onFileValidated, onError, accept = ".json,.txt", disabled = false, loading = false, maxSize = DEFAULT_MAX_SIZE, validateFile, className = "", allowedBackupTypes = DEFAULT_ALLOWED_BACKUP_TYPES, unsupportedBackupMessage, }) { const [processing, setProcessing] = useState(false); const [error, setError] = useState(""); const defaultValidateFile = useCallback((content, _file) => { try { // First, try to parse as JSON let parsedData; let isJson = false; try { parsedData = JSON.parse(content); isJson = true; } catch { // Not JSON, treat as text parsedData = content.trim(); } if (isJson && typeof parsedData === "object" && parsedData !== null) { // Check for encrypted backup structure if ("iv" in parsedData && "data" in parsedData) { return { isValid: true, fileType: "encrypted", format: "json", data: parsedData, }; } // Check for legacy encrypted format if ("encrypted" in parsedData && "encryptedMnemonic" in parsedData) { return { isValid: true, fileType: "encrypted", format: "json", data: parsedData, }; } // Check for unencrypted backup structure and validate backup type const backupType = detectBackupType(parsedData); if (backupType) { if (isBackupTypeAllowed(backupType, allowedBackupTypes)) { return { isValid: true, fileType: "unencrypted", format: "json", data: parsedData, }; } // Valid backup format but not allowed return { isValid: false, fileType: "unencrypted", format: "json", error: createUnsupportedBackupError(backupType, allowedBackupTypes, unsupportedBackupMessage), data: parsedData, }; } // JSON but unknown structure return { isValid: false, fileType: "unknown", format: "json", error: "JSON file does not contain a recognized backup structure", }; } // Check if it's a plain encrypted string (base64-like) if (typeof parsedData === "string" && parsedData.length > 100) { const trimmed = parsedData.trim(); // Check for base64-like encrypted string if (/^[A-Za-z0-9+/=]+$/.test(trimmed) && trimmed.length % 4 === 0) { return { isValid: true, fileType: "encrypted", format: "text", data: trimmed, }; } // Check for other encrypted formats if (trimmed.includes("-----BEGIN") && trimmed.includes("-----END")) { return { isValid: true, fileType: "encrypted", format: "text", data: trimmed, }; } } // Unknown format return { isValid: false, fileType: "unknown", format: isJson ? "json" : "text", error: "File does not contain a recognized backup format", }; } catch (err) { return { isValid: false, fileType: "unknown", format: "unknown", error: err instanceof Error ? err.message : "Failed to parse file content", }; } }, [allowedBackupTypes, unsupportedBackupMessage]); const processFile = useCallback(async (file) => { setProcessing(true); setError(""); try { // Validate file size if (file.size > maxSize) { throw new Error(`File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`); } // Validate file type if (accept && !accept.split(",").some((type) => { const cleanType = type.trim(); if (cleanType.startsWith(".")) { return file.name.toLowerCase().endsWith(cleanType.toLowerCase()); } return file.type === cleanType; })) { throw new Error(`Invalid file type. Please select a file with extension: ${accept}`); } // Read file content const content = await file.text(); if (!content.trim()) { throw new Error("File is empty"); } // Validate content const validator = validateFile || defaultValidateFile; const result = await validator(content, file); if (result.isValid) { onFileValidated?.(file, result); onFileSelect?.(file); } else { const errorMsg = result.error || "Invalid file format"; setError(errorMsg); onError?.(errorMsg); } } catch (err) { const errorMsg = err instanceof Error ? err.message : "Failed to process file"; setError(errorMsg); onError?.(errorMsg); } finally { setProcessing(false); } }, [ maxSize, accept, validateFile, defaultValidateFile, onFileValidated, onError, onFileSelect, ]); const isDisabled = disabled || loading || processing; return (_jsxs("div", { className: cn("space-y-4", className), children: [_jsx(DropZone, { accept: accept, disabled: isDisabled, loading: processing, height: 150, title: "Import Backup File", description: `Supports encrypted and unencrypted backups (${accept})`, dragActiveText: "Drop your backup file here", dragActiveDescription: "Release to import", loadingText: "Processing file...", maxSize: maxSize, onFileSelect: (file) => processFile(file), onError: (error) => { setError(error); onError?.(error); } }), _jsx(ErrorDisplay, { error: error }), _jsx(Card, { className: "bg-muted/50", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "space-y-2", children: [_jsx("h4", { className: "text-sm font-medium", children: "Supported Formats" }), _jsxs("div", { className: "space-y-1", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: ["\u2022 ", _jsx("strong", { children: "Encrypted JSON:" }), " Bitcoin Auth backup with encryption"] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["\u2022 ", _jsx("strong", { children: "Unencrypted JSON:" }), " Raw BAP master backup"] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["\u2022 ", _jsx("strong", { children: "Encrypted Text:" }), " Base64 encoded backup data"] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["\u2022 ", _jsx("strong", { children: "Legacy formats:" }), " Older backup file structures"] })] })] }) }) })] })); }