UNPKG

bigblocks

Version:

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

184 lines (183 loc) 11.5 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useCallback, useEffect, useState } from "react"; import { useBitcoinAuth } from "../../hooks/useBitcoinAuth.js"; import { isBapMasterBackupLegacy } from "../../lib/types.js"; import { cn } from "../../lib/utils.js"; import { BackupDownload } from "../backup-recovery/BackupDownload.js"; import { IdentityGeneration } from "../backup-recovery/IdentityGeneration.js"; import { MnemonicDisplay } from "../backup-recovery/MnemonicDisplay.js"; import { ErrorDisplay } from "../ui-components/ErrorDisplay.js"; import { LoadingButton } from "../ui-components/LoadingButton.js"; import { PasswordInput } from "../ui-components/PasswordInput.js"; import { StepIndicator } from "../ui-components/StepIndicator.js"; import { Button } from "../ui/button.js"; export function SignupFlow({ onSuccess, onError, className = "", }) { const { user, generateBackup, signUp, isSessionBackupImported } = useBitcoinAuth(); const [currentStep, setCurrentStep] = useState("generate"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [backup, setBackup] = useState(null); const [backupDownloaded, setBackupDownloaded] = useState(false); const [isImportedBackup, setIsImportedBackup] = useState(false); // Check if we started with an imported backup useEffect(() => { isSessionBackupImported().then(setIsImportedBackup); }, [isSessionBackupImported]); const steps = [ { id: "generate", label: "Generate Identity", status: currentStep === "generate" ? "active" : ["password", "mnemonic", "backup", "complete"].includes(currentStep) ? "complete" : "pending", }, { id: "password", label: "Create Password", status: currentStep === "password" ? "active" : ["mnemonic", "backup", "complete"].includes(currentStep) ? "complete" : "pending", }, { id: "backup", label: "Save Recovery", status: currentStep === "mnemonic" || currentStep === "backup" ? "active" : currentStep === "complete" ? "complete" : "pending", }, { id: "complete", label: "Finish Setup", status: currentStep === "complete" ? "active" : "pending", }, ]; const handleGenerateIdentity = useCallback(() => { setLoading(true); setError(""); try { const newBackup = generateBackup(); setBackup(newBackup); setCurrentStep("password"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate identity"); } finally { setLoading(false); } }, [generateBackup]); const handleImportBackup = useCallback(async (file) => { setLoading(true); setError(""); try { const text = await file.text(); const parsed = JSON.parse(text); // Check if it's an encrypted backup if (parsed.iv && parsed.data) { setError("This backup is already encrypted. Please use the Sign In page to restore it."); return; } // Check if it's a valid unencrypted backup if (parsed.xprv && parsed.mnemonic) { setBackup(parsed); setCurrentStep("password"); } else { setError("Invalid backup format"); } } catch { setError("Invalid backup file"); } finally { setLoading(false); } }, []); const handlePasswordSubmit = (e) => { e.preventDefault(); if (password.length < 8) { setError("Password must be at least 8 characters"); return; } if (password !== confirmPassword) { setError("Passwords do not match"); return; } setError(""); setCurrentStep("mnemonic"); }; const handleMnemonicContinue = () => { setCurrentStep("backup"); }; const handleBackupDownloaded = () => { setBackupDownloaded(true); }; const handleComplete = async () => { if (!backup || !password || (!backupDownloaded && !isImportedBackup)) return; setLoading(true); setError(""); try { const result = await signUp(password); if (result.success) { setCurrentStep("complete"); // User is available from the hook after successful sign up if (user) { onSuccess?.(user); } } else { setError(result.error?.message || "Signup failed"); if (result.error) { onError?.(result.error); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to complete signup"; setError(errorMessage); onError?.({ code: "UNKNOWN_ERROR", message: errorMessage }); } finally { setLoading(false); } }; const renderStep = () => { switch (currentStep) { case "generate": return (_jsx(IdentityGeneration, { onGenerate: handleGenerateIdentity, onImport: handleImportBackup, loading: loading, error: error })); case "password": return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("div", { className: "w-16 h-16 rounded-full bg-green-500 flex", style: { alignItems: "center", justifyContent: "center", }, children: _jsxs("svg", { width: "32", height: "32", fill: "none", viewBox: "0 0 24 24", stroke: "white", "aria-label": "Success", children: [_jsx("title", { children: "Success" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" })] }) }), _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold text-center", children: "Identity Generated!" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Now let's secure it with a password" })] })] }), _jsx(ErrorDisplay, { error: error }), _jsx("form", { onSubmit: handlePasswordSubmit, children: _jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PasswordInput, { label: "Create Password", value: password, onChange: setPassword, placeholder: "Enter a strong password", showHint: true, autoComplete: "new-password" }), _jsx(PasswordInput, { label: "Confirm Password", value: confirmPassword, onChange: setConfirmPassword, placeholder: "Re-enter your password", autoComplete: "new-password" }), _jsx(LoadingButton, { type: "submit", disabled: !password || !confirmPassword, loading: loading, size: "lg", className: "w-full", children: "Continue" })] }) })] })); case "mnemonic": return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold text-center", children: "Save Your Recovery Phrase" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Write down these words in the exact order shown" })] }), backup && isBapMasterBackupLegacy(backup) && (_jsx(MnemonicDisplay, { mnemonic: backup.mnemonic, onContinue: handleMnemonicContinue })), backup && !isBapMasterBackupLegacy(backup) && (_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Type 42 backup format does not use mnemonic phrases. Your backup is ready for download." }))] })); case "backup": return (_jsx("div", { className: "flex flex-col gap-6", children: isImportedBackup ? ( // Imported backup case - no download needed _jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold text-center", children: "Setup Complete" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Your imported backup is ready to use" })] }), _jsx(LoadingButton, { onClick: handleComplete, loading: loading, size: "lg", className: "w-full", children: "Complete Setup" })] })) : ( // Generated backup case - download required _jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold text-center", children: "Download Your Backup" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Save your encrypted backup file in a safe place" })] }), backup && (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(BackupDownload, { backup: backup, password: password, onDownloaded: handleBackupDownloaded, requireDownload: true }), _jsx(LoadingButton, { onClick: handleComplete, disabled: !backupDownloaded, loading: loading, size: "lg", className: "w-full", children: "Complete Setup" })] }))] })) })); case "complete": return (_jsxs("div", { className: "flex flex-col items-center gap-6", children: [_jsx("div", { className: "w-20 h-20 rounded-full bg-green-500 flex items-center justify-center", children: _jsxs("svg", { width: "40", height: "40", fill: "none", viewBox: "0 0 24 24", stroke: "white", "aria-label": "Success", children: [_jsx("title", { children: "Success" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" })] }) }), _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold text-center", children: "Setup Complete!" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Your Bitcoin identity is ready to use" })] })] })); } }; return (_jsxs("div", { className: cn("flex flex-col gap-6", className), children: [currentStep !== "generate" && currentStep !== "complete" && (_jsx(StepIndicator, { steps: steps })), renderStep(), currentStep !== "generate" && currentStep !== "complete" && (_jsx("div", { className: "flex justify-center mt-4", children: _jsx(Button, { variant: "ghost", size: "default", onClick: () => { if (currentStep === "password") setCurrentStep("generate"); if (currentStep === "mnemonic") setCurrentStep("password"); if (currentStep === "backup") setCurrentStep("mnemonic"); }, children: "\u2190 Back" }) }))] })); }