UNPKG

bigblocks

Version:

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

210 lines (209 loc) 19.1 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import React, { useState, useCallback, useEffect, useMemo } from "react"; import { useBitcoinAuth } from "../../hooks/useBitcoinAuth.js"; import { CONTAINER_WIDTHS } from "../../lib/layout-constants.js"; import { BackupImport } from "../backup-recovery/BackupImport.js"; import { useBapProfileSync } from "../bap-identity/hooks/useBapProfileSync.js"; import { AuthLayout, CenteredLayout } from "../layouts/AuthLayout.js"; import { ErrorDisplay } from "../ui-components/ErrorDisplay.js"; import { HandCashIcon } from "../ui-components/HandCashIcon.js"; import { LoadingButton } from "../ui-components/LoadingButton.js"; import { PasswordInput } from "../ui-components/PasswordInput.js"; import { YoursWalletIcon } from "../ui-components/YoursWalletIcon.js"; import { Button } from "../ui/button.js"; import { Card } from "../ui/card.js"; import { Separator } from "../ui/separator.js"; import { DeviceLinkQR } from "./DeviceLinkQR.js"; import { LoginForm } from "./LoginForm.js"; import { OAuthRestoreFlow } from "./OAuthRestoreFlow.js"; import { SignupFlow } from "./SignupFlow.js"; export const AuthFlowOrchestrator = ({ flowType = "unified", initialStep = "initial", enableOAuth = true, enableDeviceLink = false, enableFileImport = true, enableLocalBackup = true, oauthProviders = [ { id: "google", name: "Google", icon: (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", "aria-label": "Google logo", children: [_jsx("title", { children: "Google logo" }), _jsx("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), _jsx("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), _jsx("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), _jsx("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })] })), }, { id: "handcash", name: "HandCash", icon: _jsx(HandCashIcon, {}), }, { id: "yourswallet", name: "Yours Wallet", icon: _jsx(YoursWalletIcon, {}), }, ], onSuccess, onError, onFlowChange, onStepChange, onOAuthConflictResolved, customLayout, renderCustomFlow, showHeader = true, showFooter = true, layout = "none", title = "Welcome to BitcoinAuth", subtitle = "Your journey to self-sovereign identity starts here", className = "", enableProfileSync = true, debug = false, }) => { // Core auth state const auth = useBitcoinAuth(); const { isAuthenticated, mode, currentStep, hasLocalBackup, isLoading } = auth; const profileSync = useBapProfileSync(); // Local flow state const [flowState, setFlowState] = useState({ currentFlow: flowType, currentStep: initialStep, oauthConflict: false, }); // State for password input (import flow) const [password, setPassword] = useState(""); // Update flow state helper const updateFlowState = useCallback((updates) => { setFlowState((prev) => ({ ...prev, ...updates })); }, []); // Handle flow changes const handleFlowChange = useCallback((newFlow) => { updateFlowState({ currentFlow: newFlow, currentStep: "initial", error: undefined, }); onFlowChange?.(newFlow); }, [updateFlowState, onFlowChange]); // Handle step changes const handleStepChange = useCallback((newStep) => { updateFlowState({ currentStep: newStep }); onStepChange?.(newStep); }, [updateFlowState, onStepChange]); // Handle OAuth conflict resolution const handleOAuthConflictResolved = useCallback(() => { updateFlowState({ oauthConflict: false }); onOAuthConflictResolved?.(); }, [updateFlowState, onOAuthConflictResolved]); // Effect: Sync with auth state useEffect(() => { if (isAuthenticated && auth.user) { onSuccess?.(auth.user); } }, [isAuthenticated, auth.user, onSuccess]); // Effect: Handle auth step changes useEffect(() => { if (currentStep !== flowState.currentStep) { handleStepChange(currentStep); } }, [currentStep, flowState.currentStep, handleStepChange]); // Effect: Initialize profile sync if enabled useEffect(() => { if (enableProfileSync && isAuthenticated && !profileSync.isScanning) { // Delay sync to avoid race conditions const timer = setTimeout(() => { profileSync.syncProfiles(); }, 1000); return () => clearTimeout(timer); } }, [enableProfileSync, isAuthenticated, profileSync]); // Custom flow rendering if (flowType === "custom" && renderCustomFlow) { return _jsx(_Fragment, { children: renderCustomFlow() }); } // Main flow rendering logic const renderFlow = () => { switch (flowState.currentFlow) { case "unified": return (_jsx("div", { className: "w-full", children: mode === "signin" ? (_jsxs(_Fragment, { children: [_jsx(LoginForm, { mode: "signin", onSuccess: (user) => onSuccess?.(user) }), _jsxs("div", { className: "mt-8 flex flex-col gap-2 items-center", children: [_jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("signup"), children: "Don't have an account? Sign up" }), enableFileImport && (_jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("import"), children: "Import backup file" }))] }), enableOAuth && (_jsxs("div", { className: "flex flex-col gap-4", style: { marginTop: "1.5rem" }, children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Separator, { className: "flex-1" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Or continue with" }), _jsx(Separator, { className: "flex-1" })] }), _jsx("div", { className: "flex flex-col gap-3", children: oauthProviders.map((provider) => (_jsx(Button, { variant: "outline", size: "lg", onClick: () => { // Use safe navigation that works in SSR const url = `/api/auth/signin/${provider.id}?callbackUrl=/dashboard`; if (typeof window !== "undefined") { window.location.href = url; } else { console.warn("OAuth redirect attempted during SSR"); } }, style: { width: "100%" }, children: _jsxs("div", { className: "flex items-center gap-3 w-full justify-center", children: [_jsx("div", { style: { flexShrink: 0, width: "20px", height: "20px", }, children: provider.icon }), _jsx("span", { className: "text-base", children: provider.name })] }) }, provider.id))) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SignupFlow, { onSuccess: (user) => onSuccess?.(user), onError: (authError) => onError?.(authError.message) }), _jsx("div", { className: "flex justify-center", style: { marginTop: "1rem" }, children: _jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("signin"), children: "Already have an account? Sign in" }) })] })) })); case "signin": return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(LoginForm, { mode: "signin", onSuccess: (user) => onSuccess?.(user) }), _jsxs("div", { className: "flex flex-col items-center gap-2", style: { marginTop: "1rem" }, children: [_jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("signup"), children: "Don't have an account? Sign up" }), enableFileImport && (_jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("import"), children: "Import backup file" }))] }), enableOAuth && (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { variant: "ghost", size: "default", color: "blue", onClick: () => handleFlowChange("oauth-restore"), children: "Restore from cloud backup" }) }))] })); case "signup": return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(SignupFlow, { onSuccess: (user) => onSuccess?.(user), onError: (authError) => onError?.(authError.message) }), _jsx("div", { className: "flex justify-center", style: { marginTop: "1rem" }, children: _jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("signin"), children: "Already have an account? Sign in" }) })] })); case "oauth-restore": return (_jsx(OAuthRestoreFlow, { providers: oauthProviders, showProviderSelection: !flowState.oauthBackup, showPasswordEntry: !!flowState.oauthBackup, onRestoreSuccess: (idKey) => { onSuccess?.({ id: idKey, address: "" }); }, onRestoreError: (error) => { updateFlowState({ error }); onError?.(error); }, mockOAuthResponse: flowState.oauthBackup ? { provider: flowState.oauthBackup.provider, encryptedBackup: flowState.oauthBackup.encryptedBackup, idKey: "restored-id-key", } : undefined })); case "import": 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-2xl font-bold text-center", children: "Import Backup" }), _jsx("span", { className: "text-sm text-muted-foreground text-center", children: "Import your wallet backup file" })] }), flowState.currentStep !== "password" ? (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(BackupImport, { onImport: async (file) => { // Use the AuthManager's importBackup method const result = await auth.importBackup(file); if (result.success) { // The AuthManager will have set the proper state // Just update our flow state to reflect the change updateFlowState({ currentStep: "password" }); } else { updateFlowState({ error: result.error?.message || "Failed to import backup", }); } }, showLabel: false }), _jsx(Button, { variant: "ghost", size: "lg", onClick: () => handleFlowChange("signin"), style: { width: "100%" }, children: "Back to sign in" })] })) : (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("span", { className: "text-center text-muted-foreground", children: "Backup file imported. Please enter your password to decrypt it." }), _jsx(PasswordInput, { value: password, onChange: setPassword, placeholder: "Enter your backup password", autoFocus: true }), flowState.error && _jsx(ErrorDisplay, { error: flowState.error }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(LoadingButton, { onClick: async () => { try { // Simply sign in with the password // The AuthManager already has the imported backup stored const result = await auth.signIn(password); if (result.success && auth.user) { onSuccess?.(auth.user); // Optional: Trigger profile sync after successful backup import if (enableProfileSync && !profileSync.isScanning) { setTimeout(() => { profileSync.syncProfiles(); }, 1000); } } else { updateFlowState({ error: result.error?.message || "Invalid password or corrupted backup", }); } } catch (err) { updateFlowState({ error: err instanceof Error ? err.message : "Failed to decrypt backup", }); } }, loading: isLoading, disabled: !password, size: "lg", className: "w-full", children: "Decrypt Backup" }), _jsx(Button, { variant: "ghost", size: "lg", onClick: () => { updateFlowState({ importedBackup: undefined }); handleStepChange("initial"); }, style: { width: "100%" }, children: "Cancel" })] })] }))] })); case "device-link": return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(DeviceLinkQR, { onLinkGenerated: (url, token) => { console.log("Device link generated:", { url, token }); }, onError: (error) => { updateFlowState({ error }); onError?.(error); } }), _jsx("div", { className: "flex justify-center", children: _jsx(Button, { variant: "ghost", size: "default", onClick: () => handleFlowChange("signin"), children: "Back to sign in" }) })] })); default: return null; } }; // Select layout component const LayoutComponent = useMemo(() => { if (customLayout) return customLayout; if (layout === "centered") return CenteredLayout; if (layout === "full") return AuthLayout; return React.Fragment; }, [customLayout, layout]); return (_jsx(_Fragment, { children: _jsx(LayoutComponent, { ...(layout === "full" ? { title, subtitle } : {}), children: _jsxs("div", { className: className, children: [showHeader && flowState.currentFlow !== "unified" && (_jsx("div", { className: "flex justify-center", style: { marginBottom: "1.5rem" }, children: _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Button, { variant: flowState.currentFlow === "signin" ? "default" : "ghost", size: "default", onClick: () => handleFlowChange("signin"), children: "Sign In" }), _jsx("span", { className: "text-muted-foreground", children: "|" }), _jsx(Button, { variant: flowState.currentFlow === "signup" ? "default" : "ghost", size: "default", onClick: () => handleFlowChange("signup"), children: "Sign Up" }), enableOAuth && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: "|" }), _jsx(Button, { variant: flowState.currentFlow === "oauth-restore" ? "default" : "ghost", size: "default", onClick: () => handleFlowChange("oauth-restore"), children: "Restore" })] })), enableDeviceLink && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground", children: "|" }), _jsx(Button, { variant: flowState.currentFlow === "device-link" ? "default" : "ghost", size: "default", onClick: () => handleFlowChange("device-link"), children: "Link Device" })] }))] }) })), flowState.error && flowState.currentFlow !== "import" && (_jsx("div", { style: { marginBottom: "1rem" }, children: _jsx(ErrorDisplay, { error: flowState.error }) })), layout === "centered" ? (_jsx("div", { className: "flex justify-center w-full", children: _jsx("div", { style: { width: "100%", maxWidth: CONTAINER_WIDTHS.MODAL_SMALL, }, children: _jsx(Card, { className: "p-6", children: _jsx("div", { className: "flex flex-col gap-6", children: renderFlow() }) }) }) })) : (renderFlow()), showFooter && (_jsx("div", { className: "flex justify-center", style: { marginTop: "2rem" }, children: _jsxs("span", { className: "text-sm text-muted-foreground text-center", children: ["Your keys, your Bitcoin.", enableLocalBackup && " Encrypted locally.", enableOAuth && " Backed up securely."] }) })), debug && (_jsx(Card, { className: "mt-8 p-4", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("span", { className: "text-xs font-mono", children: ["Flow: ", flowState.currentFlow] }), _jsxs("span", { className: "text-xs font-mono", children: ["Step: ", flowState.currentStep] }), _jsxs("span", { className: "text-xs font-mono", children: ["Auth Mode: ", mode] }), _jsxs("span", { className: "text-xs font-mono", children: ["Auth Step: ", currentStep] }), _jsxs("span", { className: "text-xs font-mono", children: ["Has Backup: ", hasLocalBackup ? "Yes" : "No"] }), _jsxs("span", { className: "text-xs font-mono", children: ["Authenticated: ", isAuthenticated ? "Yes" : "No"] })] }) }))] }) }) })); };