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