chore: wizard flow for auth

This commit is contained in:
Danish Arora 2025-08-05 10:30:54 +05:30
parent e29fc8ed59
commit ea25318c02
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 976 additions and 145 deletions

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '@/contexts/useAuth';
import { useForum } from '@/contexts/useForum';
@ -8,7 +8,7 @@ import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, Refr
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useToast } from '@/components/ui/use-toast';
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
import { WalletConnectionDialog } from '@/components/ui/wallet-dialog';
import { WalletWizard } from '@/components/ui/wallet-wizard';
const Header = () => {
const {
@ -34,162 +34,75 @@ const Header = () => {
const isConnected = isBitcoinConnected || isEthereumConnected;
const address = isConnected ? (isBitcoinConnected ? bitcoinAccount.address : ethereumAccount.address) : undefined;
const [walletDialogOpen, setWalletDialogOpen] = useState(false);
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
const [hasShownWizard, setHasShownWizard] = useState(false);
// Auto-open wizard when wallet connects for the first time
React.useEffect(() => {
if (isConnected && !hasShownWizard) {
setWalletWizardOpen(true);
setHasShownWizard(true);
}
}, [isConnected, hasShownWizard]);
const handleConnect = async () => {
setWalletDialogOpen(true);
setWalletWizardOpen(true);
};
const handleDisconnect = async () => {
await disconnect();
setHasShownWizard(false); // Reset so wizard can show again on next connection
toast({
title: "Wallet Disconnected",
description: "Your wallet has been disconnected successfully.",
});
};
const handleVerify = async () => {
await verifyOwnership();
};
const handleDelegateKey = async () => {
try {
if (!isWalletAvailable()) {
toast({
title: "Wallet Not Available",
description: "Wallet is not installed or not available. Please install a compatible wallet and try again.",
variant: "destructive",
});
return;
}
await delegateKey();
} catch (error) {
console.error('Error in handleDelegateKey:', error);
const getAccountStatusText = () => {
switch (verificationStatus) {
case 'unverified':
return 'Setup Required';
case 'verifying':
return 'Verifying...';
case 'verified-none':
return 'Read-Only Access';
case 'verified-owner':
return isDelegationValid() ? 'Full Access' : 'Setup Key';
default:
return 'Setup Account';
}
};
const formatDelegationTime = () => {
if (!isDelegationValid()) return null;
const timeRemaining = delegationTimeRemaining();
const hours = Math.floor(timeRemaining / (1000 * 60 * 60));
const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
return `${hours}h ${minutes}m`;
const getAccountStatusIcon = () => {
switch (verificationStatus) {
case 'unverified':
return <AlertTriangle className="w-3 h-3" />;
case 'verifying':
return <RefreshCw className="w-3 h-3 animate-spin" />;
case 'verified-none':
return <CircleSlash className="w-3 h-3" />;
case 'verified-owner':
return isDelegationValid() ? <CheckCircle className="w-3 h-3" /> : <Key className="w-3 h-3" />;
default:
return <AlertTriangle className="w-3 h-3" />;
}
};
const renderDelegationButton = () => {
if (verificationStatus !== 'verified-owner') return null;
const hasValidDelegation = isDelegationValid();
const timeRemaining = formatDelegationTime();
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={hasValidDelegation ? "outline" : "default"}
size="sm"
className="flex items-center gap-1 text-xs px-2 h-7"
onClick={handleDelegateKey}
>
<Key className="w-3 h-3" />
{hasValidDelegation
? <span>KEY ACTIVE ({timeRemaining})</span>
: <span>DELEGATE KEY</span>}
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
{hasValidDelegation ? (
<p>Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.</p>
) : (
<p>Delegate a browser key for 24h to avoid constant wallet signing. If your wallet is disconnected, it will be reconnected automatically.</p>
)}
</TooltipContent>
</Tooltip>
);
};
const renderAccessBadge = () => {
if (verificationStatus === 'unverified') {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleVerify}
className="flex items-center gap-1 text-xs px-2 h-7 border-destructive text-destructive hover:bg-destructive/10"
>
<AlertTriangle className="w-3 h-3" />
<span>[UNVERIFIED] Verify</span>
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
<p className="font-semibold mb-1">Action Required</p>
<p>Verify your Ordinal ownership to enable posting, commenting, and voting.</p>
</TooltipContent>
</Tooltip>
);
const getAccountStatusVariant = () => {
switch (verificationStatus) {
case 'unverified':
return 'destructive';
case 'verifying':
return 'outline';
case 'verified-none':
return 'secondary';
case 'verified-owner':
return isDelegationValid() ? 'default' : 'outline';
default:
return 'outline';
}
if (verificationStatus === 'verifying') {
return (
<Badge
variant="outline"
className="flex items-center gap-1 text-xs px-2 h-7"
>
<RefreshCw className="w-3 h-3 animate-spin" />
<span>[VERIFYING...]</span>
</Badge>
);
}
if (verificationStatus === 'verified-none') {
return (
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="secondary"
className="flex items-center gap-1 cursor-help text-xs px-2 h-7"
>
<CircleSlash className="w-3 h-3" />
<span>[VERIFIED | READ-ONLY]</span>
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
<p className="font-semibold mb-1">Wallet Verified - No Ordinals</p>
<p>No Ordinal Operators found. Read-only access granted.</p>
<Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
</TooltipContent>
</Tooltip>
);
}
// Verified - Ordinal Owner
if (verificationStatus === 'verified-owner') {
return (
<Tooltip>
<TooltipTrigger asChild>
<Badge
variant="default"
className="flex items-center gap-1 cursor-help text-xs px-2 h-7 bg-primary text-primary-foreground"
>
<CheckCircle className="w-3 h-3" />
<span>[OWNER ]</span>
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
<p className="font-semibold mb-1">Ordinal Owner Verified!</p>
<p>Full forum access granted.</p>
<Button size="sm" variant="link" onClick={handleVerify} className="p-0 h-auto mt-1 text-xs">Verify Again?</Button>
</TooltipContent>
</Tooltip>
);
}
return null;
};
return (
@ -241,8 +154,23 @@ const Header = () => {
</Button>
) : (
<div className="flex gap-2 items-center">
{renderAccessBadge()}
{renderDelegationButton()}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={getAccountStatusVariant()}
size="sm"
onClick={() => setWalletWizardOpen(true)}
className="flex items-center gap-1 text-xs px-2 h-7"
>
{getAccountStatusIcon()}
<span>{getAccountStatusText()}</span>
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-[260px] text-sm">
<p className="font-semibold mb-1">Account Setup</p>
<p>Click to view and manage your wallet connection, verification status, and key delegation.</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="hidden md:flex items-center text-xs text-muted-foreground cursor-default px-2 h-7">
@ -272,10 +200,15 @@ const Header = () => {
</div>
</header>
<WalletConnectionDialog
open={walletDialogOpen}
onOpenChange={setWalletDialogOpen}
onConnect={handleConnect}
<WalletWizard
open={walletWizardOpen}
onOpenChange={setWalletWizardOpen}
onComplete={() => {
toast({
title: "Setup Complete",
description: "You can now use all OpChan features!",
});
}}
/>
</>
);

View File

@ -0,0 +1,239 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Key, Clock, Shield, Loader2, CheckCircle, AlertCircle } from "lucide-react";
import { useAuth } from "@/contexts/useAuth";
interface DelegationStepProps {
onComplete: () => void;
onBack: () => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
}
export function DelegationStep({
onComplete,
onBack,
isLoading,
setIsLoading,
}: DelegationStepProps) {
const {
currentUser,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
isAuthenticating
} = useAuth();
const [delegationResult, setDelegationResult] = React.useState<{
success: boolean;
message: string;
expiry?: string;
} | null>(null);
const handleDelegate = async () => {
if (!currentUser) return;
setIsLoading(true);
setDelegationResult(null);
try {
const success = await delegateKey();
if (success) {
const expiryDate = currentUser.delegationExpiry
? new Date(currentUser.delegationExpiry).toLocaleString()
: '24 hours from now';
setDelegationResult({
success: true,
message: "Key delegation successful! You can now interact with the forum without additional wallet approvals.",
expiry: expiryDate
});
} else {
setDelegationResult({
success: false,
message: "Key delegation failed. You can still use the forum but will need to approve each action."
});
}
} catch (error) {
setDelegationResult({
success: false,
message: "Delegation failed. Please try again."
});
} finally {
setIsLoading(false);
}
};
const handleComplete = () => {
onComplete();
};
const formatTimeRemaining = () => {
const remaining = delegationTimeRemaining();
if (remaining <= 0) return "Expired";
const hours = Math.floor(remaining / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 0) {
return `${hours}h ${minutes}m remaining`;
} else {
return `${minutes}m remaining`;
}
};
// Show delegation result
if (delegationResult) {
return (
<div className="space-y-4">
<div className={`p-4 rounded-lg border ${
delegationResult.success
? 'bg-green-900/20 border-green-500/30'
: 'bg-yellow-900/20 border-yellow-500/30'
}`}>
<div className="flex items-center gap-2 mb-2">
{delegationResult.success ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : (
<AlertCircle className="h-5 w-5 text-yellow-500" />
)}
<span className={`font-medium ${
delegationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{delegationResult.success ? 'Delegation Complete' : 'Delegation Result'}
</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
{delegationResult.message}
</p>
{delegationResult.expiry && (
<div className="text-xs text-neutral-400">
<p>Expires: {delegationResult.expiry}</p>
</div>
)}
</div>
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
"Complete Setup"
)}
</Button>
</div>
);
}
// Show existing delegation status
if (isDelegationValid()) {
return (
<div className="space-y-4">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">Key Already Delegated</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
You already have an active key delegation.
</p>
<div className="text-xs text-neutral-400">
<p>Time remaining: {formatTimeRemaining()}</p>
{currentUser?.delegationExpiry && (
<p>Expires: {new Date(currentUser.delegationExpiry).toLocaleString()}</p>
)}
</div>
</div>
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={isLoading}
>
Complete Setup
</Button>
</div>
);
}
// Show delegation form
return (
<div className="space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
<Key className="h-8 w-8 text-blue-500" />
</div>
<h3 className="text-lg font-semibold text-white">
Delegate Signing Key
</h3>
<p className="text-sm text-neutral-400">
Create a browser-based signing key for better user experience
</p>
</div>
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">What is key delegation?</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
<li> Creates a browser-based signing key for 24 hours</li>
<li> Allows posting, commenting, and voting without wallet approval</li>
<li> Automatically expires for security</li>
<li> Can be renewed anytime</li>
</ul>
</div>
{currentUser?.browserPubKey && (
<div className="p-3 bg-neutral-900/30 border border-neutral-600 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Key className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium text-white">Browser Key Generated</span>
</div>
<p className="text-xs text-neutral-400 font-mono break-all">
{currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)}
</p>
</div>
)}
<div className="space-y-3">
<Button
onClick={handleDelegate}
disabled={isLoading || isAuthenticating}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
{isLoading || isAuthenticating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Delegating Key...
</>
) : (
"Delegate Signing Key"
)}
</Button>
<Button
onClick={onBack}
variant="outline"
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating}
>
Back to Verification
</Button>
</div>
<div className="text-xs text-neutral-500 text-center space-y-1">
<p>Key delegation is optional but recommended for better UX</p>
<p>You can still use the forum without delegation</p>
</div>
</div>
);
}

View File

@ -0,0 +1,261 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Bitcoin, Coins, Shield, ShieldCheck, Loader2, AlertCircle } from "lucide-react";
import { useAuth } from "@/contexts/useAuth";
import { useAppKitAccount } from "@reown/appkit/react";
interface VerificationStepProps {
onComplete: () => void;
onBack: () => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
}
export function VerificationStep({
onComplete,
onBack,
isLoading,
setIsLoading,
}: VerificationStepProps) {
const {
currentUser,
verificationStatus,
verifyOwnership,
isAuthenticating
} = useAuth();
// Get account info to determine wallet type
const bitcoinAccount = useAppKitAccount({ namespace: "bip122" });
const ethereumAccount = useAppKitAccount({ namespace: "eip155" });
const isBitcoinConnected = bitcoinAccount.isConnected;
const isEthereumConnected = ethereumAccount.isConnected;
const walletType = isBitcoinConnected ? 'bitcoin' : 'ethereum';
const [verificationResult, setVerificationResult] = React.useState<{
success: boolean;
message: string;
details?: any;
} | null>(null);
const handleVerify = async () => {
if (!currentUser) return;
setIsLoading(true);
setVerificationResult(null);
try {
const success = await verifyOwnership();
if (success) {
setVerificationResult({
success: true,
message: walletType === 'bitcoin'
? "Ordinal ownership verified successfully!"
: "ENS ownership verified successfully!",
details: walletType === 'bitcoin'
? currentUser.ordinalOwnership
: { ensName: currentUser.ensName, ensAvatar: currentUser.ensAvatar }
});
} else {
setVerificationResult({
success: false,
message: walletType === 'bitcoin'
? "No Ordinal ownership found. You'll have read-only access."
: "No ENS ownership found. You'll have read-only access."
});
}
} catch (error) {
setVerificationResult({
success: false,
message: "Verification failed. Please try again."
});
} finally {
setIsLoading(false);
}
};
const handleNext = () => {
onComplete();
};
const getVerificationType = () => {
return walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'ENS Domain';
};
const getVerificationIcon = () => {
return walletType === 'bitcoin' ? Bitcoin : Coins;
};
const getVerificationColor = () => {
return walletType === 'bitcoin' ? 'text-orange-500' : 'text-blue-500';
};
const getVerificationDescription = () => {
if (walletType === 'bitcoin') {
return "Verify that you own Bitcoin Ordinals to get full posting and voting access.";
} else {
return "Verify that you own an ENS domain to get full posting and voting access.";
}
};
// Show verification result
if (verificationResult) {
return (
<div className="space-y-4">
<div className={`p-4 rounded-lg border ${
verificationResult.success
? 'bg-green-900/20 border-green-500/30'
: 'bg-yellow-900/20 border-yellow-500/30'
}`}>
<div className="flex items-center gap-2 mb-2">
{verificationResult.success ? (
<ShieldCheck className="h-5 w-5 text-green-500" />
) : (
<AlertCircle className="h-5 w-5 text-yellow-500" />
)}
<span className={`font-medium ${
verificationResult.success ? 'text-green-400' : 'text-yellow-400'
}`}>
{verificationResult.success ? 'Verification Complete' : 'Verification Result'}
</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
{verificationResult.message}
</p>
{verificationResult.details && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' ? (
<p>Ordinal ID: {verificationResult.details.id}</p>
) : (
<p>ENS Name: {verificationResult.details.ensName}</p>
)}
</div>
)}
</div>
<Button
onClick={handleNext}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
"Continue to Key Delegation"
)}
</Button>
</div>
);
}
// Show verification status
if (verificationStatus === 'verified-owner') {
return (
<div className="space-y-4">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<ShieldCheck className="h-5 w-5 text-green-500" />
<span className="text-green-400 font-medium">Already Verified</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
Your {getVerificationType()} ownership has been verified.
</p>
{currentUser && (
<div className="text-xs text-neutral-400">
{walletType === 'bitcoin' && currentUser.ordinalOwnership && (
<p>Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}</p>
)}
{walletType === 'ethereum' && currentUser.ensName && (
<p>ENS Name: {currentUser.ensName}</p>
)}
</div>
)}
</div>
<Button
onClick={handleNext}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={isLoading}
>
Continue to Key Delegation
</Button>
</div>
);
}
// Show verification form
return (
<div className="space-y-4">
<div className="text-center space-y-2">
<div className="flex justify-center">
{React.createElement(getVerificationIcon(), {
className: `h-8 w-8 ${getVerificationColor()}`
})}
</div>
<h3 className="text-lg font-semibold text-white">
Verify {getVerificationType()} Ownership
</h3>
<p className="text-sm text-neutral-400">
{getVerificationDescription()}
</p>
</div>
<div className="p-4 bg-neutral-900/50 border border-neutral-700 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Shield className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium text-white">What happens during verification?</span>
</div>
<ul className="text-xs text-neutral-400 space-y-1">
{walletType === 'bitcoin' ? (
<>
<li> We'll check your wallet for Bitcoin Ordinal ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
) : (
<>
<li> We'll check your wallet for ENS domain ownership</li>
<li> If found, you'll get full posting and voting access</li>
<li> If not found, you'll have read-only access</li>
</>
)}
</ul>
</div>
<div className="space-y-3">
<Button
onClick={handleVerify}
disabled={isLoading || isAuthenticating}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
{isLoading || isAuthenticating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
`Verify ${getVerificationType()} Ownership`
)}
</Button>
<Button
onClick={onBack}
variant="outline"
className="w-full border-neutral-600 text-neutral-400 hover:bg-neutral-800"
disabled={isLoading || isAuthenticating}
>
Back to Wallet Connection
</Button>
</div>
<div className="text-xs text-neutral-500 text-center">
Verification is required to access posting and voting features
</div>
</div>
);
}

View File

@ -0,0 +1,215 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Bitcoin, Coins, Loader2 } from "lucide-react";
import {
useAppKit,
useAppKitAccount,
useAppKitState
} from "@reown/appkit/react";
import { useAuth } from "@/contexts/useAuth";
interface WalletConnectionStepProps {
onComplete: () => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
}
export function WalletConnectionStep({
onComplete,
isLoading,
setIsLoading,
}: WalletConnectionStepProps) {
const { initialized } = useAppKitState();
const appKit = useAppKit();
const { isAuthenticated } = useAuth();
// Get account info for different chains
const bitcoinAccount = useAppKitAccount({ namespace: "bip122" });
const ethereumAccount = useAppKitAccount({ namespace: "eip155" });
// Determine which account is connected
const isBitcoinConnected = bitcoinAccount.isConnected;
const isEthereumConnected = ethereumAccount.isConnected;
const isConnected = isBitcoinConnected || isEthereumConnected;
// Get the active account info
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
const activeAddress = activeAccount.address;
const activeChain = isBitcoinConnected ? "Bitcoin" : "Ethereum";
const handleBitcoinConnect = async () => {
if (!initialized || !appKit) {
console.error('AppKit not initialized');
return;
}
setIsLoading(true);
try {
await appKit.open({
view: "Connect",
namespace: "bip122"
});
// The wizard will automatically advance when connection is detected
} catch (error) {
console.error('Error connecting Bitcoin wallet:', error);
} finally {
setIsLoading(false);
}
};
const handleEthereumConnect = async () => {
if (!initialized || !appKit) {
console.error('AppKit not initialized');
return;
}
setIsLoading(true);
try {
await appKit.open({
view: "Connect",
namespace: "eip155"
});
// The wizard will automatically advance when connection is detected
} catch (error) {
console.error('Error connecting Ethereum wallet:', error);
} finally {
setIsLoading(false);
}
};
const handleNext = () => {
onComplete();
};
// Show loading state if AppKit is not initialized
if (!initialized) {
return (
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-neutral-400 text-center">
Initializing wallet connection...
</p>
</div>
);
}
// Show connected state
if (isConnected) {
return (
<div className="space-y-4">
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-green-400 font-medium">Wallet Connected</span>
</div>
<p className="text-sm text-neutral-300 mb-2">
Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)}
</p>
<p className="text-xs text-neutral-400">
Proceeding to verification step...
</p>
</div>
<div className="flex justify-center">
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
</div>
</div>
);
}
// Show connection options
return (
<div className="space-y-4">
<p className="text-sm text-neutral-400 text-center">
Choose a network and wallet to connect to OpChan
</p>
{/* Bitcoin Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Bitcoin className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold text-white">Bitcoin</h3>
<Badge variant="secondary" className="text-xs">
Ordinal Verification Required
</Badge>
</div>
<Button
onClick={handleBitcoinConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Bitcoin Wallet"
)}
</Button>
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-700" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-black px-2 text-neutral-500">or</span>
</div>
</div>
{/* Ethereum Section */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Coins className="h-5 w-5 text-blue-500" />
<h3 className="font-semibold text-white">Ethereum</h3>
<Badge variant="secondary" className="text-xs">
ENS Ownership Required
</Badge>
</div>
<Button
onClick={handleEthereumConnect}
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
style={{
height: '44px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '600',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px'
}}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Connecting...
</>
) : (
"Connect Ethereum Wallet"
)}
</Button>
</div>
<div className="text-xs text-neutral-500 text-center pt-2">
Connect your wallet to use OpChan's features
</div>
</div>
);
}

View File

@ -0,0 +1,183 @@
import * as React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { CheckCircle, Circle, Loader2 } from "lucide-react";
import { useAuth } from "@/contexts/useAuth";
import { WalletConnectionStep } from "./wallet-connection-step";
import { VerificationStep } from "./verification-step";
import { DelegationStep } from "./delegation-step";
interface WalletWizardProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onComplete: () => void;
}
type WizardStep = 1 | 2 | 3;
export function WalletWizard({
open,
onOpenChange,
onComplete,
}: WalletWizardProps) {
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
const [isLoading, setIsLoading] = React.useState(false);
const { currentUser, isAuthenticated, verificationStatus } = useAuth();
// Reset wizard when opened and determine starting step
React.useEffect(() => {
if (open) {
// Only auto-advance from step 1 to 2 when wallet connects during the session
// Don't auto-advance to step 3 to allow manual navigation
if (isAuthenticated && verificationStatus !== 'verified-owner') {
setCurrentStep(2); // Start at verification step if authenticated but not verified
} else if (!isAuthenticated) {
setCurrentStep(1); // Start at connection step if not authenticated
} else {
setCurrentStep(1); // Default to step 1, let user navigate manually
}
setIsLoading(false);
}
}, [open, isAuthenticated, verificationStatus]);
// Auto-advance from step 1 to 2 only when wallet connects during the session
React.useEffect(() => {
if (open && currentStep === 1 && isAuthenticated) {
setCurrentStep(2);
}
}, [open, currentStep, isAuthenticated]);
const handleStepComplete = (step: WizardStep) => {
if (step < 3) {
setCurrentStep((step + 1) as WizardStep);
} else {
onComplete();
onOpenChange(false);
}
};
const handleClose = () => {
if (isLoading) return; // Prevent closing during operations
onOpenChange(false);
};
const getStepStatus = (step: WizardStep) => {
if (currentStep > step) return "completed";
if (currentStep === step) return "current";
return "pending";
};
const renderStepIcon = (step: WizardStep) => {
const status = getStepStatus(step);
if (status === "completed") {
return <CheckCircle className="h-5 w-5 text-green-500" />;
} else if (status === "current") {
return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
} else {
return <Circle className="h-5 w-5 text-gray-400" />;
}
};
const getStepTitle = (step: WizardStep) => {
switch (step) {
case 1: return "Connect Wallet";
case 2: return "Verify Ownership";
case 3: return "Delegate Key";
default: return "";
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md border-neutral-800 bg-black text-white">
<DialogHeader>
<DialogTitle className="text-xl">Setup Your Account</DialogTitle>
<DialogDescription className="text-neutral-400">
Complete these steps to access all OpChan features
</DialogDescription>
</DialogHeader>
{/* Progress Indicator */}
<div className="flex items-center justify-between mb-6">
{[1, 2, 3].map((step) => (
<div key={step} className="flex items-center">
<div className="flex items-center gap-2">
{renderStepIcon(step as WizardStep)}
<span className={`text-sm ${
getStepStatus(step as WizardStep) === "current"
? "text-blue-500 font-medium"
: getStepStatus(step as WizardStep) === "completed"
? "text-green-500"
: "text-gray-400"
}`}>
{getStepTitle(step as WizardStep)}
</span>
</div>
{step < 3 && (
<div className={`w-8 h-px mx-2 ${
getStepStatus(step as WizardStep) === "completed"
? "bg-green-500"
: "bg-gray-600"
}`} />
)}
</div>
))}
</div>
{/* Step Content */}
<div className="min-h-[300px]">
{currentStep === 1 && (
<WalletConnectionStep
onComplete={() => handleStepComplete(1)}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)}
{currentStep === 2 && (
<VerificationStep
onComplete={() => handleStepComplete(2)}
onBack={() => setCurrentStep(1)}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)}
{currentStep === 3 && (
<DelegationStep
onComplete={() => handleStepComplete(3)}
onBack={() => setCurrentStep(2)}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
)}
</div>
{/* Footer */}
<div className="flex justify-between items-center pt-4 border-t border-neutral-700">
<p className="text-xs text-neutral-500">
Step {currentStep} of 3
</p>
{currentStep > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentStep((currentStep - 1) as WizardStep)}
disabled={isLoading}
className="text-neutral-400 hover:text-white"
>
Back
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -14,7 +14,7 @@ export interface User {
ensAvatar?: string;
ensOwnership?: boolean;
verificationStatus: 'verified' | 'unverified';
verificationStatus: 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
signature?: string;
lastChecked?: number;