bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
171 lines (170 loc) • 8.42 kB
JavaScript
"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"] })] })] }) }) })] }));
}