UNPKG

bigblocks

Version:

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

125 lines (124 loc) 8.88 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { CheckCircledIcon, ExclamationTriangleIcon, InfoCircledIcon, UpdateIcon, UploadIcon, } from "@radix-ui/react-icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useBitcoinAuth } from "../../hooks/useBitcoinAuth.js"; import { cn } from "../../lib/utils.js"; import { LoadingButton } from "../ui-components/LoadingButton.js"; import { Alert, AlertDescription } from "../ui/alert.js"; import { Badge } from "../ui/badge.js"; import { Button } from "../ui/button.js"; import { Card, CardContent } from "../ui/card.js"; /** * CloudBackupManager - Manage encrypted backups stored with OAuth providers * * Features: * - Check cloud backup status * - Update outdated backups * - Show last backup time * - OAuth provider indication * - Automatic status refresh */ export function CloudBackupManager({ checkBackupStatus: checkBackupStatusProp, updateCloudBackup: updateCloudBackupProp, onStatusChange, className = "", }) { useBitcoinAuth(); const [status, setStatus] = useState({ hasCloudBackup: false, }); const [isChecking, setIsChecking] = useState(true); const [isUpdating, setIsUpdating] = useState(false); const [error, setError] = useState(null); // Default implementation if not provided const checkBackupStatus = useMemo(() => { return (checkBackupStatusProp || (async () => { // Check if user has any OAuth providers linked // For now, simulate based on localStorage const linkedProvider = localStorage.getItem("oauthProvider"); if (!linkedProvider) { return { hasCloudBackup: false }; } // In a real implementation, this would check the backend // For now, we'll simulate based on localStorage const lastBackupTime = localStorage.getItem("cloudBackupLastUpdated"); const localBackupTime = localStorage.getItem("localBackupLastUpdated"); return { hasCloudBackup: !!lastBackupTime, isOutdated: localBackupTime ? Number.parseInt(localBackupTime) > Number.parseInt(lastBackupTime || "0") : false, lastUpdated: lastBackupTime ? Number.parseInt(lastBackupTime) : undefined, provider: linkedProvider || "Unknown", }; })); }, [checkBackupStatusProp]); // Check status function (declared first to be used in useEffect) const checkStatus = useCallback(async () => { setIsChecking(true); setError(null); try { const newStatus = await checkBackupStatus(); setStatus(newStatus); onStatusChange?.(newStatus); } catch (err) { setError(err instanceof Error ? err.message : "Failed to check backup status"); } finally { setIsChecking(false); } }, [checkBackupStatus, onStatusChange]); const updateCloudBackup = updateCloudBackupProp || (async () => { // In a real implementation, this would update the backup on the server // For now, we'll simulate with a delay await new Promise((resolve) => setTimeout(resolve, 1500)); localStorage.setItem("cloudBackupLastUpdated", Date.now().toString()); }); useEffect(() => { checkStatus(); }, [checkStatus]); const handleUpdate = async () => { setIsUpdating(true); setError(null); try { await updateCloudBackup(); await checkStatus(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update cloud backup"); } finally { setIsUpdating(false); } }; const formatLastUpdated = (timestamp) => { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`; if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`; return date.toLocaleDateString(); }; if (isChecking) { return (_jsx(Card, { className: className, children: _jsxs(CardContent, { className: "flex items-center gap-3 p-6", children: [_jsx(UpdateIcon, { className: "animate-spin" }), _jsx("p", { className: "text-base", children: "Checking backup status..." })] }) })); } return (_jsx(Card, { className: className, children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(UploadIcon, { className: "w-5 h-5" }), _jsx("h3", { className: "text-lg font-bold", children: "Cloud Backup" })] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: checkStatus, disabled: isChecking, children: [_jsx(UpdateIcon, { className: "w-4 h-4 mr-1" }), "Refresh"] })] }), error && (_jsxs(Alert, { variant: "destructive", children: [_jsx(ExclamationTriangleIcon, { className: "h-4 w-4" }), _jsx(AlertDescription, { children: error })] })), !status.hasCloudBackup ? (_jsxs(Alert, { children: [_jsx(InfoCircledIcon, { className: "h-4 w-4" }), _jsx(AlertDescription, { children: "No cloud backup found. Link an OAuth provider (Google, GitHub, or X) in Settings to enable cloud backup restoration from other devices." })] })) : (_jsxs(_Fragment, { children: [_jsxs("dl", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[100px]", children: "Status" }), _jsx("dd", { children: _jsx(Badge, { variant: "secondary", className: cn(status.isOutdated ? "bg-orange-100 text-orange-700" : "bg-green-100 text-green-700"), children: status.isOutdated ? "Outdated" : "Up to date" }) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[100px]", children: "Provider" }), _jsx("dd", { className: "text-sm", children: status.provider || "Unknown" })] }), status.lastUpdated && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("dt", { className: "text-sm text-muted-foreground min-w-[100px]", children: "Last Updated" }), _jsx("dd", { className: "text-sm text-muted-foreground", children: formatLastUpdated(status.lastUpdated) })] }))] }), status.isOutdated && (_jsxs(Alert, { className: "bg-purple-50 border-purple-200", children: [_jsx(ExclamationTriangleIcon, { className: "h-4 w-4 text-purple-600" }), _jsx(AlertDescription, { className: "text-purple-700", children: "Your local backup is newer than the cloud backup. Update it to ensure you can access your latest identity from other devices." })] })), _jsx("div", { className: "flex gap-3 justify-center", children: _jsxs(LoadingButton, { loading: isUpdating, disabled: !status.isOutdated, onClick: handleUpdate, className: "w-full", children: [_jsx(UploadIcon, { className: "w-4 h-4 mr-1" }), isUpdating ? "Updating..." : status.isOutdated ? "Update Cloud Backup" : "Already Up to Date"] }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(CheckCircledIcon, { className: "text-green-600" }), _jsx("p", { className: "text-sm font-medium", children: "Backup Features" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "\u2022 Encrypted with your password" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u2022 Stored securely with OAuth provider" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u2022 Accessible from any device" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u2022 Automatic version tracking" })] })] })] }))] }) })); }