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 (
+
+
+
+
+ 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 */}
+
+
+ {/* 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 (
+
+ );
+}
\ 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;