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