OpChan/src/contexts/AuthContext.tsx

270 lines
8.9 KiB
TypeScript
Raw Normal View History

import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
2025-04-15 16:28:03 +05:30
import { useToast } from '@/components/ui/use-toast';
2025-04-16 14:45:27 +05:30
import { User } from '@/types';
2025-07-30 13:22:06 +05:30
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
import { OpchanMessage } from '@/types';
2025-08-05 09:56:32 +05:30
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
2025-04-15 16:28:03 +05:30
2025-04-24 14:31:00 +05:30
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
2025-04-15 16:28:03 +05:30
interface AuthContextType {
currentUser: User | null;
isAuthenticated: boolean;
isAuthenticating: boolean;
2025-04-24 14:31:00 +05:30
verificationStatus: VerificationStatus;
2025-04-15 16:28:03 +05:30
verifyOrdinal: () => Promise<boolean>;
delegateKey: () => Promise<boolean>;
isDelegationValid: () => boolean;
delegationTimeRemaining: () => number;
2025-07-30 15:55:13 +05:30
isWalletAvailable: () => boolean;
2025-07-30 13:22:06 +05:30
messageSigning: {
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();
2025-08-05 09:56:32 +05:30
// 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-07-30 13:22:06 +05:30
// Create ref for AuthService so it persists between renders
const authServiceRef = useRef(new AuthService());
2025-08-05 09:56:32 +05:30
// Sync with AppKit wallet state
2025-04-15 16:28:03 +05:30
useEffect(() => {
2025-08-05 09:56:32 +05:30
if (isConnected && address) {
// Check if we have a stored user for this address
const storedUser = authServiceRef.current.loadStoredUser();
2025-08-05 09:56:32 +05:30
if (storedUser && storedUser.address === address) {
// Use stored user data
setCurrentUser(storedUser);
if ('ordinalOwnership' in storedUser) {
setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
} else {
setVerificationStatus('unverified');
}
} else {
2025-08-05 09:56:32 +05:30
// Create new user from AppKit wallet
const newUser: User = {
address,
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
ordinalOwnership: false,
delegationExpiry: null,
verificationStatus: 'unverified',
};
setCurrentUser(newUser);
setVerificationStatus('unverified');
2025-08-05 09:56:32 +05:30
authServiceRef.current.saveUser(newUser);
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
toast({
2025-08-05 09:56:32 +05:30
title: "Wallet Connected",
description: `Connected to ${chainName} with address ${address.slice(0, 6)}...${address.slice(-4)}`,
});
toast({
title: "Action Required",
description: "Please verify your Ordinal ownership and delegate a signing key for better UX.",
});
}
2025-08-05 09:56:32 +05:30
} else {
// Wallet disconnected
setCurrentUser(null);
2025-04-24 14:31:00 +05:30
setVerificationStatus('unverified');
2025-04-15 16:28:03 +05:30
}
2025-08-05 09:56:32 +05:30
}, [isConnected, address, isBitcoinConnected, toast]);
2025-04-15 16:28:03 +05:30
2025-07-30 13:22:06 +05:30
const verifyOrdinal = 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 {
2025-04-24 14:31:00 +05:30
toast({
title: "Verifying Ordinal",
description: "Checking your wallet for Ordinal Operators..."
});
2025-07-30 13:22:06 +05:30
const result: AuthResult = await authServiceRef.current.verifyOrdinal(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
2025-07-30 13:22:06 +05:30
setVerificationStatus(updatedUser.ordinalOwnership ? 'verified-owner' : 'verified-none');
2025-04-24 14:31:00 +05:30
2025-07-30 13:22:06 +05:30
if (updatedUser.ordinalOwnership) {
toast({
title: "Ordinal Verified",
description: "You now have full access. We recommend delegating a key for better UX.",
});
} else {
toast({
2025-04-24 14:31:00 +05:30
title: "Read-Only Access",
description: "No Ordinal Operators found. You have read-only access.",
variant: "default",
});
}
2025-04-15 16:28:03 +05:30
2025-07-30 13:22:06 +05:30
return Boolean(updatedUser.ordinalOwnership);
2025-04-15 16:28:03 +05:30
} catch (error) {
console.error("Error verifying Ordinal:", error);
2025-04-24 14:31:00 +05:30
setVerificationStatus('unverified');
let errorMessage = "Failed to verify Ordinal 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 (): Promise<boolean> => {
if (!currentUser || !currentUser.address) {
toast({
title: "Not Connected",
description: "Please connect your wallet first.",
variant: "destructive",
});
return false;
}
setIsAuthenticating(true);
try {
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 24 hours.",
});
2025-07-30 13:22:06 +05:30
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser);
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.";
2025-04-27 15:54:24 +05:30
if (error instanceof Error) {
2025-04-27 15:54:24 +05:30
// Provide specific guidance based on error type
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
} else if (error.message.includes("timeout")) {
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
2025-07-30 15:55:13 +05:30
} else if (error.message.includes("Failed to connect wallet")) {
2025-08-05 09:56:32 +05:30
errorMessage = "Unable to connect to wallet. Please ensure it's installed and unlocked, then try again.";
2025-07-30 15:55:13 +05:30
} else if (error.message.includes("Wallet is not connected")) {
errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again.";
2025-04-27 15:54:24 +05:30
} else {
errorMessage = error.message;
}
}
toast({
2025-04-27 15:54:24 +05:30
title: "Delegation Failed",
description: errorMessage,
variant: "destructive",
});
return false;
} finally {
setIsAuthenticating(false);
}
};
const isDelegationValid = (): boolean => {
2025-07-30 13:22:06 +05:30
return authServiceRef.current.isDelegationValid();
};
const delegationTimeRemaining = (): number => {
2025-07-30 13:22:06 +05:30
return authServiceRef.current.getDelegationTimeRemaining();
};
2025-07-30 15:55:13 +05:30
const isWalletAvailable = (): boolean => {
2025-08-05 09:56:32 +05:30
return isConnected && !!address;
2025-07-30 15:55:13 +05:30
};
2025-04-15 16:28:03 +05:30
return (
<AuthContext.Provider
value={{
currentUser,
isAuthenticated: !!currentUser?.ordinalOwnership,
isAuthenticating,
2025-04-24 14:31:00 +05:30
verificationStatus,
2025-04-15 16:28:03 +05:30
verifyOrdinal,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
2025-07-30 15:55:13 +05:30
isWalletAvailable,
2025-07-30 13:22:06 +05:30
messageSigning: {
signMessage: (message: OpchanMessage) => authServiceRef.current.signMessage(message),
verifyMessage: (message: OpchanMessage) => authServiceRef.current.verifyMessage(message),
},
2025-04-15 16:28:03 +05:30
}}
>
{children}
</AuthContext.Provider>
);
}
2025-07-30 13:22:06 +05:30