UNPKG

bigblocks

Version:

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

15 lines 14.1 kB
{ "name": "authentication-signupflow", "type": "registry:component", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/authentication/SignupFlow.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { useBitcoinAuth } from \"../../hooks/useBitcoinAuth.js\";\nimport { isBapMasterBackupLegacy } from \"../../lib/types.js\";\nimport type { AuthError, AuthUser, BapMasterBackup } from \"../../lib/types.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport { BackupDownload } from \"../backup-recovery/BackupDownload.js\";\nimport { IdentityGeneration } from \"../backup-recovery/IdentityGeneration.js\";\nimport { MnemonicDisplay } from \"../backup-recovery/MnemonicDisplay.js\";\nimport { ErrorDisplay } from \"../ui-components/ErrorDisplay.js\";\nimport { LoadingButton } from \"../ui-components/LoadingButton.js\";\nimport { PasswordInput } from \"../ui-components/PasswordInput.js\";\nimport { type Step, StepIndicator } from \"../ui-components/StepIndicator.js\";\nimport { Button } from \"../ui/button.js\";\n\nexport interface SignupFlowProps {\n\tonSuccess?: (user: AuthUser) => void;\n\tonError?: (error: AuthError) => void;\n\tclassName?: string;\n}\n\ntype SignupStep = \"generate\" | \"password\" | \"mnemonic\" | \"backup\" | \"complete\";\n\nexport function SignupFlow({\n\tonSuccess,\n\tonError,\n\tclassName = \"\",\n}: SignupFlowProps) {\n\tconst { user, generateBackup, signUp, isSessionBackupImported } =\n\t\tuseBitcoinAuth();\n\tconst [currentStep, setCurrentStep] = useState<SignupStep>(\"generate\");\n\tconst [loading, setLoading] = useState(false);\n\tconst [error, setError] = useState(\"\");\n\tconst [password, setPassword] = useState(\"\");\n\tconst [confirmPassword, setConfirmPassword] = useState(\"\");\n\tconst [backup, setBackup] = useState<BapMasterBackup | null>(null);\n\tconst [backupDownloaded, setBackupDownloaded] = useState(false);\n\tconst [isImportedBackup, setIsImportedBackup] = useState(false);\n\n\t// Check if we started with an imported backup\n\tuseEffect(() => {\n\t\tisSessionBackupImported().then(setIsImportedBackup);\n\t}, [isSessionBackupImported]);\n\n\tconst steps: Step[] = [\n\t\t{\n\t\t\tid: \"generate\",\n\t\t\tlabel: \"Generate Identity\",\n\t\t\tstatus:\n\t\t\t\tcurrentStep === \"generate\"\n\t\t\t\t\t? \"active\"\n\t\t\t\t\t: [\"password\", \"mnemonic\", \"backup\", \"complete\"].includes(currentStep)\n\t\t\t\t\t\t? \"complete\"\n\t\t\t\t\t\t: \"pending\",\n\t\t},\n\t\t{\n\t\t\tid: \"password\",\n\t\t\tlabel: \"Create Password\",\n\t\t\tstatus:\n\t\t\t\tcurrentStep === \"password\"\n\t\t\t\t\t? \"active\"\n\t\t\t\t\t: [\"mnemonic\", \"backup\", \"complete\"].includes(currentStep)\n\t\t\t\t\t\t? \"complete\"\n\t\t\t\t\t\t: \"pending\",\n\t\t},\n\t\t{\n\t\t\tid: \"backup\",\n\t\t\tlabel: \"Save Recovery\",\n\t\t\tstatus:\n\t\t\t\tcurrentStep === \"mnemonic\" || currentStep === \"backup\"\n\t\t\t\t\t? \"active\"\n\t\t\t\t\t: currentStep === \"complete\"\n\t\t\t\t\t\t? \"complete\"\n\t\t\t\t\t\t: \"pending\",\n\t\t},\n\t\t{\n\t\t\tid: \"complete\",\n\t\t\tlabel: \"Finish Setup\",\n\t\t\tstatus: currentStep === \"complete\" ? \"active\" : \"pending\",\n\t\t},\n\t];\n\n\tconst handleGenerateIdentity = useCallback(() => {\n\t\tsetLoading(true);\n\t\tsetError(\"\");\n\n\t\ttry {\n\t\t\tconst newBackup = generateBackup();\n\t\t\tsetBackup(newBackup);\n\t\t\tsetCurrentStep(\"password\");\n\t\t} catch (err) {\n\t\t\tsetError(\n\t\t\t\terr instanceof Error ? err.message : \"Failed to generate identity\",\n\t\t\t);\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, [generateBackup]);\n\n\tconst handleImportBackup = useCallback(async (file: File) => {\n\t\tsetLoading(true);\n\t\tsetError(\"\");\n\n\t\ttry {\n\t\t\tconst text = await file.text();\n\t\t\tconst parsed = JSON.parse(text);\n\n\t\t\t// Check if it's an encrypted backup\n\t\t\tif (parsed.iv && parsed.data) {\n\t\t\t\tsetError(\n\t\t\t\t\t\"This backup is already encrypted. Please use the Sign In page to restore it.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if it's a valid unencrypted backup\n\t\t\tif (parsed.xprv && parsed.mnemonic) {\n\t\t\t\tsetBackup(parsed as BapMasterBackup);\n\t\t\t\tsetCurrentStep(\"password\");\n\t\t\t} else {\n\t\t\t\tsetError(\"Invalid backup format\");\n\t\t\t}\n\t\t} catch {\n\t\t\tsetError(\"Invalid backup file\");\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t}, []);\n\n\tconst handlePasswordSubmit = (e: React.FormEvent) => {\n\t\te.preventDefault();\n\n\t\tif (password.length < 8) {\n\t\t\tsetError(\"Password must be at least 8 characters\");\n\t\t\treturn;\n\t\t}\n\n\t\tif (password !== confirmPassword) {\n\t\t\tsetError(\"Passwords do not match\");\n\t\t\treturn;\n\t\t}\n\n\t\tsetError(\"\");\n\t\tsetCurrentStep(\"mnemonic\");\n\t};\n\n\tconst handleMnemonicContinue = () => {\n\t\tsetCurrentStep(\"backup\");\n\t};\n\n\tconst handleBackupDownloaded = () => {\n\t\tsetBackupDownloaded(true);\n\t};\n\n\tconst handleComplete = async () => {\n\t\tif (!backup || !password || (!backupDownloaded && !isImportedBackup))\n\t\t\treturn;\n\n\t\tsetLoading(true);\n\t\tsetError(\"\");\n\n\t\ttry {\n\t\t\tconst result = await signUp(password);\n\t\t\tif (result.success) {\n\t\t\t\tsetCurrentStep(\"complete\");\n\t\t\t\t// User is available from the hook after successful sign up\n\t\t\t\tif (user) {\n\t\t\t\t\tonSuccess?.(user);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsetError(result.error?.message || \"Signup failed\");\n\t\t\t\tif (result.error) {\n\t\t\t\t\tonError?.(result.error);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst errorMessage =\n\t\t\t\terr instanceof Error ? err.message : \"Failed to complete signup\";\n\t\t\tsetError(errorMessage);\n\t\t\tonError?.({ code: \"UNKNOWN_ERROR\", message: errorMessage });\n\t\t} finally {\n\t\t\tsetLoading(false);\n\t\t}\n\t};\n\n\tconst renderStep = () => {\n\t\tswitch (currentStep) {\n\t\t\tcase \"generate\":\n\t\t\t\treturn (\n\t\t\t\t\t<IdentityGeneration\n\t\t\t\t\t\tonGenerate={handleGenerateIdentity}\n\t\t\t\t\t\tonImport={handleImportBackup}\n\t\t\t\t\t\tloading={loading}\n\t\t\t\t\t\terror={error}\n\t\t\t\t\t/>\n\t\t\t\t);\n\n\t\t\tcase \"password\":\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-4\">\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"w-16 h-16 rounded-full bg-green-500 flex\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\twidth=\"32\"\n\t\t\t\t\t\t\t\t\theight=\"32\"\n\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\tstroke=\"white\"\n\t\t\t\t\t\t\t\t\taria-label=\"Success\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<title>Success</title>\n\t\t\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\t\td=\"M5 13l4 4L19 7\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2\">\n\t\t\t\t\t\t\t\t<h2 className=\"text-xl font-semibold text-center\">\n\t\t\t\t\t\t\t\t\tIdentity Generated!\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\t\tNow let's secure it with a password\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<ErrorDisplay error={error} />\n\n\t\t\t\t\t\t<form onSubmit={handlePasswordSubmit}>\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t\t\t<PasswordInput\n\t\t\t\t\t\t\t\t\tlabel=\"Create Password\"\n\t\t\t\t\t\t\t\t\tvalue={password}\n\t\t\t\t\t\t\t\t\tonChange={setPassword}\n\t\t\t\t\t\t\t\t\tplaceholder=\"Enter a strong password\"\n\t\t\t\t\t\t\t\t\tshowHint={true}\n\t\t\t\t\t\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t<PasswordInput\n\t\t\t\t\t\t\t\t\tlabel=\"Confirm Password\"\n\t\t\t\t\t\t\t\t\tvalue={confirmPassword}\n\t\t\t\t\t\t\t\t\tonChange={setConfirmPassword}\n\t\t\t\t\t\t\t\t\tplaceholder=\"Re-enter your password\"\n\t\t\t\t\t\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\t\t\tdisabled={!password || !confirmPassword}\n\t\t\t\t\t\t\t\t\tloading={loading}\n\t\t\t\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tContinue\n\t\t\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</form>\n\t\t\t\t\t</div>\n\t\t\t\t);\n\n\t\t\tcase \"mnemonic\":\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2\">\n\t\t\t\t\t\t\t<h2 className=\"text-xl font-semibold text-center\">\n\t\t\t\t\t\t\t\tSave Your Recovery Phrase\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\tWrite down these words in the exact order shown\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{backup && isBapMasterBackupLegacy(backup) && (\n\t\t\t\t\t\t\t<MnemonicDisplay\n\t\t\t\t\t\t\t\tmnemonic={backup.mnemonic}\n\t\t\t\t\t\t\t\tonContinue={handleMnemonicContinue}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{backup && !isBapMasterBackupLegacy(backup) && (\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\tType 42 backup format does not use mnemonic phrases. Your backup\n\t\t\t\t\t\t\t\tis ready for download.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t);\n\n\t\t\tcase \"backup\":\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t{isImportedBackup ? (\n\t\t\t\t\t\t\t// Imported backup case - no download needed\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2\">\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-xl font-semibold text-center\">\n\t\t\t\t\t\t\t\t\t\tSetup Complete\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\t\t\tYour imported backup is ready to use\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\t\t\tonClick={handleComplete}\n\t\t\t\t\t\t\t\t\tloading={loading}\n\t\t\t\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tComplete Setup\n\t\t\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t// Generated backup case - download required\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2\">\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-xl font-semibold text-center\">\n\t\t\t\t\t\t\t\t\t\tDownload Your Backup\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\t\t\tSave your encrypted backup file in a safe place\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t{backup && (\n\t\t\t\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t<BackupDownload\n\t\t\t\t\t\t\t\t\t\t\tbackup={backup}\n\t\t\t\t\t\t\t\t\t\t\tpassword={password}\n\t\t\t\t\t\t\t\t\t\t\tonDownloaded={handleBackupDownloaded}\n\t\t\t\t\t\t\t\t\t\t\trequireDownload={true}\n\t\t\t\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\t\t\t\t\tonClick={handleComplete}\n\t\t\t\t\t\t\t\t\t\t\tdisabled={!backupDownloaded}\n\t\t\t\t\t\t\t\t\t\t\tloading={loading}\n\t\t\t\t\t\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tComplete Setup\n\t\t\t\t\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t);\n\n\t\t\tcase \"complete\":\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex flex-col items-center gap-6\">\n\t\t\t\t\t\t<div className=\"w-20 h-20 rounded-full bg-green-500 flex items-center justify-center\">\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\twidth=\"40\"\n\t\t\t\t\t\t\t\theight=\"40\"\n\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\tstroke=\"white\"\n\t\t\t\t\t\t\t\taria-label=\"Success\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<title>Success</title>\n\t\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\td=\"M5 13l4 4L19 7\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2\">\n\t\t\t\t\t\t\t<h2 className=\"text-xl font-semibold text-center\">\n\t\t\t\t\t\t\t\tSetup Complete!\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\t\t\tYour Bitcoin identity is ready to use\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t);\n\t\t}\n\t};\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-6\", className)}>\n\t\t\t{currentStep !== \"generate\" && currentStep !== \"complete\" && (\n\t\t\t\t<StepIndicator steps={steps} />\n\t\t\t)}\n\n\t\t\t{renderStep()}\n\n\t\t\t{currentStep !== \"generate\" && currentStep !== \"complete\" && (\n\t\t\t\t<div className=\"flex justify-center mt-4\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"default\"\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tif (currentStep === \"password\") setCurrentStep(\"generate\");\n\t\t\t\t\t\t\tif (currentStep === \"mnemonic\") setCurrentStep(\"password\");\n\t\t\t\t\t\t\tif (currentStep === \"backup\") setCurrentStep(\"mnemonic\");\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t← Back\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n", "target": "<%- config.aliases.components %>/signupflow.tsx" } ] }