diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 752c98e..5b450fc 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -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 ; + case 'verifying': + return ; + case 'verified-none': + return ; + case 'verified-owner': + return isDelegationValid() ? : ; + default: + return ; + } }; - const renderDelegationButton = () => { - if (verificationStatus !== 'verified-owner') return null; - - const hasValidDelegation = isDelegationValid(); - const timeRemaining = formatDelegationTime(); - - return ( - - - - - - {hasValidDelegation ? ( -

Browser key active for ~{timeRemaining}. Wallet signatures not needed for most actions.

- ) : ( -

Delegate a browser key for 24h to avoid constant wallet signing. If your wallet is disconnected, it will be reconnected automatically.

- )} -
-
- ); - }; - - const renderAccessBadge = () => { - if (verificationStatus === 'unverified') { - return ( - - - - - -

Action Required

-

Verify your Ordinal ownership to enable posting, commenting, and voting.

-
-
- ); + 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 ( - - - [VERIFYING...] - - ); - } - - if (verificationStatus === 'verified-none') { - return ( - - - - - [VERIFIED | READ-ONLY] - - - -

Wallet Verified - No Ordinals

-

No Ordinal Operators found. Read-only access granted.

- -
-
- ); - } - - // Verified - Ordinal Owner - if (verificationStatus === 'verified-owner') { - return ( - - - - - [OWNER ✔] - - - -

Ordinal Owner Verified!

-

Full forum access granted.

- -
-
- ); - } - - return null; }; return ( @@ -241,8 +154,23 @@ const Header = () => { ) : (
- {renderAccessBadge()} - {renderDelegationButton()} + + + + + +

Account Setup

+

Click to view and manage your wallet connection, verification status, and key delegation.

+
+
@@ -272,10 +200,15 @@ const Header = () => {
- { + toast({ + title: "Setup Complete", + description: "You can now use all OpChan features!", + }); + }} /> ); diff --git a/src/components/ui/delegation-step.tsx b/src/components/ui/delegation-step.tsx new file mode 100644 index 0000000..2388454 --- /dev/null +++ b/src/components/ui/delegation-step.tsx @@ -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 ( +
+
+
+ {delegationResult.success ? ( + + ) : ( + + )} + + {delegationResult.success ? 'Delegation Complete' : 'Delegation Result'} + +
+

+ {delegationResult.message} +

+ {delegationResult.expiry && ( +
+

Expires: {delegationResult.expiry}

+
+ )} +
+ + +
+ ); + } + + // Show existing delegation status + if (isDelegationValid()) { + return ( +
+
+
+ + Key Already Delegated +
+

+ You already have an active key delegation. +

+
+

Time remaining: {formatTimeRemaining()}

+ {currentUser?.delegationExpiry && ( +

Expires: {new Date(currentUser.delegationExpiry).toLocaleString()}

+ )} +
+
+ + +
+ ); + } + + // Show delegation form + return ( +
+
+
+ +
+

+ Delegate Signing Key +

+

+ Create a browser-based signing key for better user experience +

+
+ +
+
+ + What is key delegation? +
+
    +
  • • Creates a browser-based signing key for 24 hours
  • +
  • • Allows posting, commenting, and voting without wallet approval
  • +
  • • Automatically expires for security
  • +
  • • Can be renewed anytime
  • +
+
+ + {currentUser?.browserPubKey && ( +
+
+ + Browser Key Generated +
+

+ {currentUser.browserPubKey.slice(0, 20)}...{currentUser.browserPubKey.slice(-20)} +

+
+ )} + +
+ + + +
+ +
+

Key delegation is optional but recommended for better UX

+

You can still use the forum without delegation

+
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/verification-step.tsx b/src/components/ui/verification-step.tsx new file mode 100644 index 0000000..7d1787c --- /dev/null +++ b/src/components/ui/verification-step.tsx @@ -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 ( +
+
+
+ {verificationResult.success ? ( + + ) : ( + + )} + + {verificationResult.success ? 'Verification Complete' : 'Verification Result'} + +
+

+ {verificationResult.message} +

+ {verificationResult.details && ( +
+ {walletType === 'bitcoin' ? ( +

Ordinal ID: {verificationResult.details.id}

+ ) : ( +

ENS Name: {verificationResult.details.ensName}

+ )} +
+ )} +
+ + +
+ ); + } + + // Show verification status + if (verificationStatus === 'verified-owner') { + return ( +
+
+
+ + Already Verified +
+

+ Your {getVerificationType()} ownership has been verified. +

+ {currentUser && ( +
+ {walletType === 'bitcoin' && currentUser.ordinalOwnership && ( +

Ordinal ID: {typeof currentUser.ordinalOwnership === 'object' ? currentUser.ordinalOwnership.id : 'Verified'}

+ )} + {walletType === 'ethereum' && currentUser.ensName && ( +

ENS Name: {currentUser.ensName}

+ )} +
+ )} +
+ + +
+ ); + } + + // Show verification form + return ( +
+
+
+ {React.createElement(getVerificationIcon(), { + className: `h-8 w-8 ${getVerificationColor()}` + })} +
+

+ Verify {getVerificationType()} Ownership +

+

+ {getVerificationDescription()} +

+
+ +
+
+ + What happens during verification? +
+
    + {walletType === 'bitcoin' ? ( + <> +
  • • We'll check your wallet for Bitcoin Ordinal ownership
  • +
  • • If found, you'll get full posting and voting access
  • +
  • • If not found, you'll have read-only access
  • + + ) : ( + <> +
  • • We'll check your wallet for ENS domain ownership
  • +
  • • If found, you'll get full posting and voting access
  • +
  • • If not found, you'll have read-only access
  • + + )} +
+
+ +
+ + + +
+ +
+ Verification is required to access posting and voting features +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/wallet-connection-step.tsx b/src/components/ui/wallet-connection-step.tsx new file mode 100644 index 0000000..54301b2 --- /dev/null +++ b/src/components/ui/wallet-connection-step.tsx @@ -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 ( +
+ +

+ Initializing wallet connection... +

+
+ ); + } + + // Show connected state + if (isConnected) { + return ( +
+
+
+
+ Wallet Connected +
+

+ Connected to {activeChain} with {activeAddress?.slice(0, 6)}...{activeAddress?.slice(-4)} +

+

+ Proceeding to verification step... +

+
+ +
+ +
+
+ ); + } + + // Show connection options + return ( +
+

+ Choose a network and wallet to connect to OpChan +

+ + {/* Bitcoin Section */} +
+
+ +

Bitcoin

+ + Ordinal Verification Required + +
+ +
+ + {/* Divider */} +
+
+ +
+
+ or +
+
+ + {/* Ethereum Section */} +
+
+ +

Ethereum

+ + ENS Ownership Required + +
+ +
+ +
+ Connect your wallet to use OpChan's features +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/wallet-wizard.tsx b/src/components/ui/wallet-wizard.tsx new file mode 100644 index 0000000..a10c418 --- /dev/null +++ b/src/components/ui/wallet-wizard.tsx @@ -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(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 ; + } else if (status === "current") { + return ; + } else { + return ; + } + }; + + const getStepTitle = (step: WizardStep) => { + switch (step) { + case 1: return "Connect Wallet"; + case 2: return "Verify Ownership"; + case 3: return "Delegate Key"; + default: return ""; + } + }; + + return ( + + + + Setup Your Account + + Complete these steps to access all OpChan features + + + + {/* Progress Indicator */} +
+ {[1, 2, 3].map((step) => ( +
+
+ {renderStepIcon(step as WizardStep)} + + {getStepTitle(step as WizardStep)} + +
+ {step < 3 && ( +
+ )} +
+ ))} +
+ + {/* Step Content */} +
+ {currentStep === 1 && ( + handleStepComplete(1)} + isLoading={isLoading} + setIsLoading={setIsLoading} + /> + )} + + {currentStep === 2 && ( + handleStepComplete(2)} + onBack={() => setCurrentStep(1)} + isLoading={isLoading} + setIsLoading={setIsLoading} + /> + )} + + {currentStep === 3 && ( + handleStepComplete(3)} + onBack={() => setCurrentStep(2)} + isLoading={isLoading} + setIsLoading={setIsLoading} + /> + )} +
+ + {/* Footer */} +
+

+ Step {currentStep} of 3 +

+ {currentStep > 1 && ( + + )} +
+ +
+ ); +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 0999a79..1be89a3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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;