OpChan/src/contexts/AuthContext.tsx

359 lines
12 KiB
TypeScript
Raw Normal View History

2025-08-28 19:07:26 +05:30
import React, { createContext, useState, useEffect, useRef } from 'react';
2025-04-15 16:28:03 +05:30
import { useToast } from '@/components/ui/use-toast';
2025-08-28 18:44:35 +05:30
import { User, OpchanMessage, EVerificationStatus } from '@/types/forum';
2025-08-28 19:07:26 +05:30
import { AuthService, CryptoService, DelegationDuration } from '@/lib/services';
import { AuthResult } from '@/lib/services/AuthService';
2025-08-06 17:21:56 +05:30
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
2025-04-15 16:28:03 +05:30
2025-08-18 14:07:01 +05:30
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
2025-04-24 14:31:00 +05:30
2025-04-15 16:28:03 +05:30
interface AuthContextType {
currentUser: User | null;
isAuthenticating: boolean;
isAuthenticated: boolean;
2025-04-24 14:31:00 +05:30
verificationStatus: VerificationStatus;
connectWallet: () => Promise<boolean>;
disconnectWallet: () => void;
verifyOwnership: () => Promise<boolean>;
delegateKey: (duration?: DelegationDuration) => Promise<boolean>;
isDelegationValid: () => boolean;
delegationTimeRemaining: () => number;
clearDelegation: () => void;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
2025-04-15 16:28:03 +05:30
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
2025-07-30 13:22:06 +05:30
export { AuthContext };
2025-04-15 16:28:03 +05:30
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isAuthenticating, setIsAuthenticating] = useState(false);
2025-04-24 14:31:00 +05:30
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
2025-04-15 16:28:03 +05:30
const { toast } = useToast();
// Use AppKit hooks for multi-chain support
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 address = activeAccount.address;
2025-08-28 18:44:35 +05:30
// Create service instances that persist between renders
const cryptoServiceRef = useRef(new CryptoService());
const authServiceRef = useRef(new AuthService(cryptoServiceRef.current));
2025-08-06 17:21:56 +05:30
// Set AppKit instance and accounts in AuthService
useEffect(() => {
if (modal) {
authServiceRef.current.setAppKit(modal);
}
}, []);
2025-04-15 16:28:03 +05:30
useEffect(() => {
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
}, [bitcoinAccount, ethereumAccount]);
// Sync with AppKit wallet state
useEffect(() => {
if (isConnected && address) {
// Check if we have a stored user for this address
const storedUser = authServiceRef.current.loadStoredUser();
if (storedUser && storedUser.address === address) {
// Use stored user data
setCurrentUser(storedUser);
setVerificationStatus(getVerificationStatus(storedUser));
} else {
// Create new user from AppKit wallet
const newUser: User = {
address,
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
2025-08-28 18:44:35 +05:30
verificationStatus: EVerificationStatus.VERIFIED_BASIC, // Connected wallets get basic verification by default
lastChecked: Date.now(),
};
2025-08-18 14:07:01 +05:30
// For Ethereum wallets, try to check ENS ownership immediately
if (isEthereumConnected) {
authServiceRef.current.getWalletInfo().then((walletInfo) => {
if (walletInfo?.ensName) {
const updatedUser = {
...newUser,
ensOwnership: true,
ensName: walletInfo.ensName,
2025-08-28 18:44:35 +05:30
verificationStatus: EVerificationStatus.VERIFIED_OWNER,
2025-08-18 14:07:01 +05:30
};
setCurrentUser(updatedUser);
setVerificationStatus('verified-owner');
authServiceRef.current.saveUser(updatedUser);
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
}
}).catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
});
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
}
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
toast({
title: "Wallet Connected",
description: `Connected to ${chainName} with ${displayName}`,
});
const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership';
toast({
title: "Action Required",
2025-08-18 14:07:01 +05:30
description: `You can participate in the forum now! Verify your ${verificationType} for premium features and delegate a signing key for better UX.`,
});
}
} else {
// Wallet disconnected
setCurrentUser(null);
2025-04-24 14:31:00 +05:30
setVerificationStatus('unverified');
2025-04-15 16:28:03 +05:30
}
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
2025-04-15 16:28:03 +05:30
const { disconnect } = useDisconnect();
const connectWallet = async (): Promise<boolean> => {
try {
if (modal) {
await modal.open();
return true;
}
return false;
} catch (error) {
console.error('Error connecting wallet:', error);
return false;
}
};
const disconnectWallet = (): void => {
disconnect();
};
const getVerificationStatus = (user: User): VerificationStatus => {
if (user.walletType === 'bitcoin') {
2025-08-18 14:07:01 +05:30
return user.ordinalOwnership ? 'verified-owner' : 'verified-basic';
} else if (user.walletType === 'ethereum') {
2025-08-18 14:07:01 +05:30
return user.ensOwnership ? 'verified-owner' : 'verified-basic';
}
return 'unverified';
2025-04-15 16:28:03 +05:30
};
const verifyOwnership = async (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
2025-04-15 16:28:03 +05:30
toast({
title: "Not Connected",
description: "Please connect your wallet first.",
variant: "destructive",
});
return false;
}
setIsAuthenticating(true);
2025-04-24 14:31:00 +05:30
setVerificationStatus('verifying');
2025-04-15 16:28:03 +05:30
try {
const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS';
2025-04-24 14:31:00 +05:30
toast({
title: `Verifying ${verificationType}`,
description: `Checking your wallet for ${verificationType} ownership...`
2025-04-24 14:31:00 +05:30
});
const result: AuthResult = await authServiceRef.current.verifyOwnership(currentUser);
2025-04-15 16:28:03 +05:30
2025-07-30 13:22:06 +05:30
if (!result.success) {
throw new Error(result.error);
}
const updatedUser = result.user!;
2025-04-15 16:28:03 +05:30
setCurrentUser(updatedUser);
2025-07-30 13:22:06 +05:30
authServiceRef.current.saveUser(updatedUser);
2025-04-15 16:28:03 +05:30
2025-04-24 14:31:00 +05:30
// Update verification status
setVerificationStatus(getVerificationStatus(updatedUser));
2025-04-24 14:31:00 +05:30
if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) {
toast({
title: "Ordinal Verified",
2025-08-18 14:07:01 +05:30
description: "You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.",
});
} else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) {
toast({
title: "ENS Verified",
2025-08-18 14:07:01 +05:30
description: "You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.",
});
} else {
const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain';
toast({
2025-08-18 14:07:01 +05:30
title: "Basic Access Granted",
description: `No ${verificationType} found, but you can still participate in the forum with your connected wallet.`,
2025-04-24 14:31:00 +05:30
variant: "default",
});
}
2025-04-15 16:28:03 +05:30
return Boolean(
(updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) ||
(updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership)
);
2025-04-15 16:28:03 +05:30
} catch (error) {
console.error("Error verifying ownership:", error);
2025-04-24 14:31:00 +05:30
setVerificationStatus('unverified');
let errorMessage = "Failed to verify ownership. Please try again.";
if (error instanceof Error) {
errorMessage = error.message;
}
2025-04-24 14:31:00 +05:30
2025-04-15 16:28:03 +05:30
toast({
title: "Verification Error",
description: errorMessage,
2025-04-15 16:28:03 +05:30
variant: "destructive",
});
2025-04-24 14:31:00 +05:30
2025-04-15 16:28:03 +05:30
return false;
} finally {
setIsAuthenticating(false);
}
};
const delegateKey = async (duration: DelegationDuration = '7days'): Promise<boolean> => {
if (!currentUser) {
toast({
title: "No User Found",
description: "Please connect your wallet first.",
variant: "destructive",
});
return false;
}
setIsAuthenticating(true);
try {
const durationText = duration === '7days' ? '1 week' : '30 days';
toast({
2025-04-27 15:54:24 +05:30
title: "Starting Key Delegation",
description: `This will let you post, comment, and vote without approving each action for ${durationText}.`,
});
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser, duration);
2025-07-30 13:22:06 +05:30
if (!result.success) {
throw new Error(result.error);
}
2025-07-30 13:22:06 +05:30
const updatedUser = result.user!;
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
2025-04-27 15:54:24 +05:30
// Format date for user-friendly display
2025-07-30 13:22:06 +05:30
const expiryDate = new Date(updatedUser.delegationExpiry!);
2025-04-27 15:54:24 +05:30
const formattedExpiry = expiryDate.toLocaleString();
toast({
2025-04-27 15:54:24 +05:30
title: "Key Delegation Successful",
description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
});
return true;
} catch (error) {
console.error("Error delegating key:", error);
let errorMessage = "Failed to delegate key. Please try again.";
if (error instanceof Error) {
errorMessage = error.message;
}
toast({
title: "Delegation Error",
description: errorMessage,
variant: "destructive",
});
return false;
} finally {
setIsAuthenticating(false);
}
};
const isDelegationValid = (): boolean => {
2025-08-28 18:44:35 +05:30
return cryptoServiceRef.current.isDelegationValid();
};
const delegationTimeRemaining = (): number => {
2025-08-28 18:44:35 +05:30
return cryptoServiceRef.current.getDelegationTimeRemaining();
};
2025-08-06 17:21:56 +05:30
const clearDelegation = (): void => {
2025-08-28 18:44:35 +05:30
cryptoServiceRef.current.clearDelegation();
2025-08-06 17:21:56 +05:30
// Update the current user to remove delegation info
if (currentUser) {
const updatedUser = {
...currentUser,
delegationExpiry: undefined,
browserPublicKey: undefined
};
setCurrentUser(updatedUser);
authServiceRef.current.saveUser(updatedUser);
}
toast({
title: "Delegation Cleared",
description: "Your delegated signing key has been removed. You'll need to delegate a new key to continue posting and voting.",
});
};
const messageSigning = {
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
2025-08-28 18:44:35 +05:30
return cryptoServiceRef.current.signMessage(message);
},
verifyMessage: (message: OpchanMessage): boolean => {
2025-08-28 18:44:35 +05:30
return cryptoServiceRef.current.verifyMessage(message);
}
};
const value: AuthContextType = {
currentUser,
isAuthenticating,
isAuthenticated: Boolean(currentUser && isConnected),
verificationStatus,
connectWallet,
disconnectWallet,
verifyOwnership,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
clearDelegation,
signMessage: messageSigning.signMessage,
verifyMessage: messageSigning.verifyMessage
2025-07-30 15:55:13 +05:30
};
2025-04-15 16:28:03 +05:30
return (
<AuthContext.Provider value={value}>
2025-04-15 16:28:03 +05:30
{children}
</AuthContext.Provider>
);
}
2025-07-30 13:22:06 +05:30