bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
185 lines (184 loc) • 13.7 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useMemo, useState } from "react";
import { useBitcoinAuth } from "../../hooks/useBitcoinAuth.js";
import { ErrorDisplay } from "../ui-components/ErrorDisplay.js";
import { LoadingButton } from "../ui-components/LoadingButton.js";
import { Modal } from "../ui-components/Modal.js";
import { PasswordInput } from "../ui-components/PasswordInput.js";
import { Button } from "../ui/button.js";
import { OAuthProviders, } from "../wallet-integrations/OAuthProviders.js";
export function OAuthRestoreFlow({ providers = [
{
id: "google",
name: "Google",
enabled: true,
icon: (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", children: [_jsx("title", { children: "Google" }), _jsx("path", { fill: "#4285F4", 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" }), _jsx("path", { fill: "#34A853", 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" }), _jsx("path", { fill: "#FBBC05", 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" }), _jsx("path", { fill: "#EA4335", 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" })] })),
},
{
id: "github",
name: "GitHub",
enabled: true,
icon: (_jsxs("svg", { width: "20", height: "20", fill: "currentColor", viewBox: "0 0 24 24", children: [_jsx("title", { children: "GitHub" }), _jsx("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" })] })),
},
{
id: "twitter",
name: "X (Twitter)",
enabled: true,
icon: (_jsxs("svg", { width: "20", height: "20", fill: "currentColor", viewBox: "0 0 24 24", children: [_jsx("title", { children: "X (Twitter)" }), _jsx("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" })] })),
},
], signupCallbackUrl = "/signup/oauth", signinCallbackUrl = "/signin/oauth-restore", showProviderSelection = true, showPasswordEntry = true, allowProviderSwitch = true, isModal = false, isOpen = true, onClose, onProviderSelected, onPasswordValidated, onRestoreSuccess, onRestoreError, onFlowComplete, className = "", title = "Restore from Cloud Backup", subtitle = "Sign in with your linked account to restore your wallet", mockOAuthResponse, }) {
const [currentStep, setCurrentStep] = useState("provider");
const [selectedProvider, setSelectedProvider] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [encryptedBackup, setEncryptedBackup] = useState("");
const auth = useBitcoinAuth();
const { isLoading } = auth;
// Handle methods that might not be available in all contexts (for backward compatibility)
const restoreFromBackup = useMemo(() => {
const authWithMethods = auth;
return (authWithMethods.restoreFromBackup ||
(async () => ({
success: false,
error: "restoreFromBackup not available in this context",
})));
}, [auth]);
const validatePassword = useMemo(() => {
const authWithMethods = auth;
return authWithMethods.validatePassword || (async () => false);
}, [auth]);
// Handle OAuth provider selection
const handleProviderClick = useCallback(async (provider) => {
try {
setError("");
setSelectedProvider(provider);
onProviderSelected?.(provider);
// If we have a mock response (for testing), use it
if (mockOAuthResponse && mockOAuthResponse.provider === provider) {
setEncryptedBackup(mockOAuthResponse.encryptedBackup);
setCurrentStep("password");
return;
}
// Start OAuth flow
setCurrentStep("processing");
// Redirect to OAuth provider
const callbackUrl = currentStep === "provider" ? signinCallbackUrl : signupCallbackUrl;
const authUrl = `/api/auth/signin/${provider}?callbackUrl=${encodeURIComponent(callbackUrl)}`;
window.location.href = authUrl;
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to start OAuth flow";
setError(errorMessage);
setCurrentStep("error");
onRestoreError?.(errorMessage);
}
}, [
currentStep,
signinCallbackUrl,
signupCallbackUrl,
mockOAuthResponse,
onProviderSelected,
onRestoreError,
]);
// Handle password validation and restore
const handlePasswordSubmit = useCallback(async () => {
if (!password.trim() || !encryptedBackup) {
setError("Password is required");
return;
}
try {
setError("");
setCurrentStep("processing");
// Validate password can decrypt the backup
const isValid = await validatePassword(encryptedBackup, password);
if (!isValid) {
throw new Error("Invalid password. Please check your password and try again.");
}
onPasswordValidated?.(password);
// Restore from backup
const result = await restoreFromBackup(encryptedBackup, password);
if (result.success && "idKey" in result && result.idKey) {
setCurrentStep("success");
onRestoreSuccess?.(result.idKey);
// Auto-close after success
setTimeout(() => {
onFlowComplete?.();
onClose?.();
}, 2000);
}
else {
throw new Error(result.error || "Failed to restore backup");
}
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to restore backup";
setError(errorMessage);
setCurrentStep("error");
onRestoreError?.(errorMessage);
}
}, [
password,
encryptedBackup,
validatePassword,
restoreFromBackup,
onPasswordValidated,
onRestoreSuccess,
onRestoreError,
onFlowComplete,
onClose,
]);
// Handle provider switch
const handleProviderSwitch = useCallback(() => {
setCurrentStep("provider");
setSelectedProvider("");
setPassword("");
setError("");
setEncryptedBackup("");
}, []);
// Handle retry
const handleRetry = useCallback(() => {
if (selectedProvider && allowProviderSwitch) {
setCurrentStep("password");
}
else {
setCurrentStep("provider");
setSelectedProvider("");
}
setPassword("");
setError("");
}, [selectedProvider, allowProviderSwitch]);
const renderContent = () => {
switch (currentStep) {
case "provider":
if (!showProviderSelection) {
return (_jsx("div", { className: "flex justify-center", children: _jsx("p", { className: "text-muted-foreground text-center", children: "Provider selection disabled" }) }));
}
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: title }), _jsx("p", { className: "text-muted-foreground text-center text-sm", children: subtitle })] }), _jsx(OAuthProviders, { providers: providers, action: "signin", onProviderClick: handleProviderClick, loading: isLoading })] }));
case "password":
if (!showPasswordEntry) {
return (_jsx("div", { className: "flex justify-center", children: _jsx("p", { className: "text-muted-foreground text-center", children: "Password entry disabled" }) }));
}
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: "Enter Your Password" }), _jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Enter the password you used to encrypt your wallet backup" }), selectedProvider && allowProviderSwitch && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: handleProviderSwitch, className: "mt-2 text-primary", children: ["Switch Provider (", selectedProvider, ")"] }))] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("div", { onKeyDown: (e) => e.key === "Enter" &&
!isLoading &&
password.trim() &&
handlePasswordSubmit(), children: _jsx(PasswordInput, { value: password, onChange: setPassword, placeholder: "Enter your wallet password", disabled: isLoading, autoFocus: true }) }), error && _jsx(ErrorDisplay, { error: error }), _jsx(LoadingButton, { onClick: handlePasswordSubmit, loading: isLoading, disabled: !password.trim(), className: "w-full", children: "Restore Wallet" })] })] }));
case "processing":
return (_jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsxs("svg", { className: "animate-spin h-10 w-10 text-primary", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", "aria-label": "Loading spinner", children: [_jsx("title", { children: "Loading spinner" }), _jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), _jsx("h2", { className: "text-xl font-semibold text-center", children: selectedProvider
? "Restoring Wallet..."
: "Connecting to Provider..." }), _jsx("p", { className: "text-muted-foreground text-center text-sm", children: selectedProvider
? "Decrypting and restoring your wallet backup"
: "Please complete the sign-in process in the popup window" })] }));
case "success":
return (_jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("div", { className: "w-12 h-12 rounded-full bg-green-500 flex items-center justify-center", children: _jsxs("svg", { width: "24", height: "24", fill: "none", stroke: "white", viewBox: "0 0 24 24", "aria-label": "Success", children: [_jsx("title", { children: "Success" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" })] }) }), _jsx("h2", { className: "text-xl font-semibold text-center", children: "Wallet Restored!" }), _jsx("p", { className: "text-muted-foreground text-center text-sm", children: "Your wallet has been successfully restored from your cloud backup." })] }));
case "error":
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-12 h-12 rounded-full bg-destructive flex items-center justify-center", children: _jsxs("svg", { width: "24", height: "24", fill: "none", stroke: "white", viewBox: "0 0 24 24", "aria-label": "Error", children: [_jsx("title", { children: "Error" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })] }) }), _jsx("h2", { className: "text-xl font-semibold text-center", children: "Restore Failed" })] }), _jsx(ErrorDisplay, { error: error }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(LoadingButton, { onClick: handleRetry, loading: false, className: "w-full", variant: "secondary", children: "Try Again" }), onClose && (_jsx(Button, { variant: "ghost", size: "lg", onClick: onClose, className: "w-full", children: "Cancel" }))] })] }));
default:
return null;
}
};
if (isModal) {
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose || (() => { }), className: "max-w-md", children: _jsx("div", { className: `p-6 ${className || ""}`, children: renderContent() }) }));
}
return (_jsx("div", { className: `w-full h-full flex items-center justify-center ${className || ""}`, children: _jsx("div", { className: "w-full max-w-md p-4", children: renderContent() }) }));
}