mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
chore: move all storages to indexedDB
This commit is contained in:
parent
9e6be6156f
commit
aa17bda249
@ -4,6 +4,8 @@ import { useAuth, useNetworkStatus } from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||
import { DelegationFullStatus } from '@/lib/delegation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import {
|
||||
@ -31,7 +33,8 @@ import { useUserDisplay } from '@/hooks';
|
||||
const Header = () => {
|
||||
const { verificationStatus } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
useState<DelegationFullStatus | null>(null);
|
||||
const networkStatus = useNetworkStatus();
|
||||
const location = useLocation();
|
||||
const { toast } = useToast();
|
||||
@ -57,28 +60,38 @@ const Header = () => {
|
||||
// ✅ Get display name from enhanced hook
|
||||
const { displayName } = useUserDisplay(address || '');
|
||||
|
||||
// Use sessionStorage to persist wizard state across navigation
|
||||
const getHasShownWizard = () => {
|
||||
// Load delegation status
|
||||
React.useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
// Use LocalDatabase to persist wizard state across navigation
|
||||
const getHasShownWizard = async (): Promise<boolean> => {
|
||||
try {
|
||||
return sessionStorage.getItem('hasShownWalletWizard') === 'true';
|
||||
const value = await localDatabase.loadUIState('hasShownWalletWizard');
|
||||
return value === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setHasShownWizard = (value: boolean) => {
|
||||
const setHasShownWizard = async (value: boolean): Promise<void> => {
|
||||
try {
|
||||
sessionStorage.setItem('hasShownWalletWizard', value.toString());
|
||||
} catch {
|
||||
// Fallback if sessionStorage is not available
|
||||
await localDatabase.storeUIState('hasShownWalletWizard', value);
|
||||
} catch (e) {
|
||||
console.error('Failed to store wizard state', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-open wizard when wallet connects for the first time
|
||||
React.useEffect(() => {
|
||||
if (isConnected && !getHasShownWizard()) {
|
||||
setWalletWizardOpen(true);
|
||||
setHasShownWizard(true);
|
||||
if (isConnected) {
|
||||
getHasShownWizard().then(hasShown => {
|
||||
if (!hasShown) {
|
||||
setWalletWizardOpen(true);
|
||||
setHasShownWizard(true).catch(console.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
@ -88,7 +101,7 @@ const Header = () => {
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
await disconnect();
|
||||
setHasShownWizard(false); // Reset so wizard can show again on next connection
|
||||
await setHasShownWizard(false); // Reset so wizard can show again on next connection
|
||||
toast({
|
||||
title: 'Wallet Disconnected',
|
||||
description: 'Your wallet has been disconnected successfully.',
|
||||
@ -99,7 +112,7 @@ const Header = () => {
|
||||
if (!isConnected) return 'Connect Wallet';
|
||||
|
||||
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
|
||||
return delegationInfo.isValid ? 'Ready to Post' : 'Delegation Expired';
|
||||
return delegationInfo?.isValid ? 'Ready to Post' : 'Delegation Expired';
|
||||
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
|
||||
return 'Verified (Read-only)';
|
||||
} else {
|
||||
@ -112,7 +125,7 @@ const Header = () => {
|
||||
|
||||
if (
|
||||
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||
delegationInfo.isValid
|
||||
delegationInfo?.isValid
|
||||
) {
|
||||
return 'text-green-400';
|
||||
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
|
||||
@ -131,7 +144,7 @@ const Header = () => {
|
||||
|
||||
if (
|
||||
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
|
||||
delegationInfo.isValid
|
||||
delegationInfo?.isValid
|
||||
) {
|
||||
return <CheckCircle className="w-4 h-4" />;
|
||||
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
|
||||
@ -244,7 +257,7 @@ const Header = () => {
|
||||
<div className="text-xs">
|
||||
<div>Address: {address?.slice(0, 8)}...</div>
|
||||
<div>Status: {getAccountStatusText()}</div>
|
||||
{delegationInfo.timeRemaining && (
|
||||
{delegationInfo?.timeRemaining && (
|
||||
<div>
|
||||
Delegation: {delegationInfo.timeRemaining} remaining
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
useForumData,
|
||||
useAuth,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
useForumSelectors,
|
||||
} from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { DelegationFullStatus } from '@/lib/delegation';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -24,6 +26,12 @@ export function HookDemoComponent() {
|
||||
const forumData = useForumData();
|
||||
const auth = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const [delegationStatus, setDelegationStatus] = useState<DelegationFullStatus | null>(null);
|
||||
|
||||
// Load delegation status
|
||||
useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationStatus).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
// Derived hooks for specific data
|
||||
const userVotes = useUserVotes();
|
||||
@ -137,7 +145,7 @@ export function HookDemoComponent() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Delegation Active:</strong>{' '}
|
||||
{getDelegationStatus().isValid ? 'Yes' : 'No'}
|
||||
{delegationStatus?.isValid ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from './button';
|
||||
import { useAuth, useAuthActions } from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
|
||||
import { DelegationDuration } from '@/lib/delegation';
|
||||
import { DelegationDuration, DelegationFullStatus } from '@/lib/delegation';
|
||||
|
||||
interface DelegationStepProps {
|
||||
onComplete: () => void;
|
||||
@ -20,9 +20,15 @@ export function DelegationStep({
|
||||
}: DelegationStepProps) {
|
||||
const { currentUser, isAuthenticating } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
useState<DelegationFullStatus | null>(null);
|
||||
const { delegateKey, clearDelegation } = useAuthActions();
|
||||
|
||||
// Load delegation status
|
||||
useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
const [selectedDuration, setSelectedDuration] =
|
||||
React.useState<DelegationDuration>('7days');
|
||||
const [delegationResult, setDelegationResult] = React.useState<{
|
||||
@ -128,19 +134,19 @@ export function DelegationStep({
|
||||
<div className="space-y-3">
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
{delegationInfo.isValid ? (
|
||||
{delegationInfo?.isValid ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
delegationInfo.isValid ? 'text-green-400' : 'text-yellow-400'
|
||||
delegationInfo?.isValid ? 'text-green-400' : 'text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{delegationInfo.isValid ? 'Delegated' : 'Required'}
|
||||
{delegationInfo?.isValid ? 'Delegated' : 'Required'}
|
||||
</span>
|
||||
{delegationInfo.isValid && delegationInfo.timeRemaining && (
|
||||
{delegationInfo?.isValid && delegationInfo?.timeRemaining && (
|
||||
<span className="text-xs text-neutral-400">
|
||||
{delegationInfo.timeRemaining} remaining
|
||||
</span>
|
||||
@ -148,7 +154,7 @@ export function DelegationStep({
|
||||
</div>
|
||||
|
||||
{/* Duration Selection */}
|
||||
{!delegationInfo.isValid && (
|
||||
{!delegationInfo?.isValid && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-neutral-300">
|
||||
Delegation Duration:
|
||||
@ -187,7 +193,7 @@ export function DelegationStep({
|
||||
)}
|
||||
|
||||
{/* Delegated Browser Public Key */}
|
||||
{delegationInfo.isValid && currentUser?.browserPubKey && (
|
||||
{delegationInfo?.isValid && currentUser?.browserPubKey && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
<div className="font-mono break-all bg-neutral-800 p-2 rounded">
|
||||
{currentUser.browserPubKey}
|
||||
@ -203,7 +209,7 @@ export function DelegationStep({
|
||||
)}
|
||||
|
||||
{/* Delete Button for Active Delegations */}
|
||||
{delegationInfo.isValid && (
|
||||
{delegationInfo?.isValid && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={clearDelegation}
|
||||
@ -221,7 +227,7 @@ export function DelegationStep({
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-auto space-y-2">
|
||||
{delegationInfo.isValid ? (
|
||||
{delegationInfo?.isValid ? (
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
||||
|
||||
@ -11,6 +11,7 @@ import { CheckCircle, Circle, Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { DelegationFullStatus } from '@/lib/delegation';
|
||||
import { WalletConnectionStep } from './wallet-connection-step';
|
||||
import { VerificationStep } from './verification-step';
|
||||
import { DelegationStep } from './delegation-step';
|
||||
@ -32,9 +33,15 @@ export function WalletWizard({
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const { isAuthenticated, verificationStatus } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
React.useState<DelegationFullStatus | null>(null);
|
||||
const hasInitialized = React.useRef(false);
|
||||
|
||||
// Load delegation status
|
||||
React.useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
// Reset wizard when opened and determine starting step
|
||||
React.useEffect(() => {
|
||||
if (open && !hasInitialized.current) {
|
||||
@ -50,7 +57,7 @@ export function WalletWizard({
|
||||
isAuthenticated &&
|
||||
(verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED ||
|
||||
verificationStatus === EVerificationStatus.WALLET_CONNECTED) &&
|
||||
!delegationInfo.isValid
|
||||
!delegationInfo?.isValid
|
||||
) {
|
||||
setCurrentStep(3); // Start at delegation step if verified but no valid delegation
|
||||
} else {
|
||||
@ -92,7 +99,7 @@ export function WalletWizard({
|
||||
) {
|
||||
return 'disabled';
|
||||
}
|
||||
return delegationInfo.isValid ? 'complete' : 'current';
|
||||
return delegationInfo?.isValid ? 'complete' : 'current';
|
||||
}
|
||||
return 'disabled';
|
||||
};
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
DelegationDuration,
|
||||
DelegationFullStatus,
|
||||
} from '@/lib/delegation';
|
||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
|
||||
|
||||
// Removed VerificationStatus type - using EVerificationStatus enum directly
|
||||
@ -25,8 +26,8 @@ interface AuthContextType {
|
||||
disconnectWallet: () => void;
|
||||
verifyOwnership: () => Promise<boolean>;
|
||||
delegateKey: (duration?: DelegationDuration) => Promise<boolean>;
|
||||
getDelegationStatus: () => DelegationFullStatus;
|
||||
clearDelegation: () => void;
|
||||
getDelegationStatus: () => Promise<DelegationFullStatus>;
|
||||
clearDelegation: () => Promise<void>;
|
||||
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
|
||||
verifyMessage: (message: OpchanMessage) => Promise<boolean>;
|
||||
}
|
||||
@ -73,30 +74,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [bitcoinAccount, ethereumAccount]);
|
||||
|
||||
// Helper functions for user persistence
|
||||
const loadStoredUser = (): User | null => {
|
||||
const storedUser = localStorage.getItem('opchan-user');
|
||||
if (!storedUser) return null;
|
||||
|
||||
const loadStoredUser = async (): Promise<User | null> => {
|
||||
try {
|
||||
const user = JSON.parse(storedUser);
|
||||
const lastChecked = user.lastChecked || 0;
|
||||
const expiryTime = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (Date.now() - lastChecked < expiryTime) {
|
||||
return user;
|
||||
} else {
|
||||
localStorage.removeItem('opchan-user');
|
||||
return null;
|
||||
}
|
||||
return await localDatabase.loadUser();
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stored user data', e);
|
||||
localStorage.removeItem('opchan-user');
|
||||
console.error('Failed to load stored user data', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const saveUser = (user: User): void => {
|
||||
localStorage.setItem('opchan-user', JSON.stringify(user));
|
||||
const saveUser = async (user: User): Promise<void> => {
|
||||
try {
|
||||
await localDatabase.storeUser(user);
|
||||
} catch (e) {
|
||||
console.error('Failed to save user data', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for ownership verification
|
||||
@ -188,83 +180,83 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
if (isConnected && address) {
|
||||
// Check if we have a stored user for this address
|
||||
const storedUser = loadStoredUser();
|
||||
loadStoredUser().then(async storedUser => {
|
||||
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',
|
||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED, // Connected wallets get basic verification by default
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
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',
|
||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED, // Connected wallets get basic verification by default
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
// For Ethereum wallets, try to check ENS ownership immediately
|
||||
if (isEthereumConnected) {
|
||||
try {
|
||||
const walletManager = WalletManager.getInstance();
|
||||
walletManager
|
||||
.getWalletInfo()
|
||||
.then(walletInfo => {
|
||||
if (walletInfo?.ensName) {
|
||||
const updatedUser = {
|
||||
...newUser,
|
||||
ensDetails: { ensName: walletInfo.ensName },
|
||||
verificationStatus:
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
||||
};
|
||||
setCurrentUser(updatedUser);
|
||||
setVerificationStatus(
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
);
|
||||
saveUser(updatedUser);
|
||||
} else {
|
||||
// For Ethereum wallets, try to check ENS ownership immediately
|
||||
if (isEthereumConnected) {
|
||||
try {
|
||||
const walletManager = WalletManager.getInstance();
|
||||
walletManager
|
||||
.getWalletInfo()
|
||||
.then(async walletInfo => {
|
||||
if (walletInfo?.ensName) {
|
||||
const updatedUser = {
|
||||
...newUser,
|
||||
ensDetails: { ensName: walletInfo.ensName },
|
||||
verificationStatus:
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED,
|
||||
};
|
||||
setCurrentUser(updatedUser);
|
||||
setVerificationStatus(
|
||||
EVerificationStatus.ENS_ORDINAL_VERIFIED
|
||||
);
|
||||
await saveUser(updatedUser);
|
||||
} else {
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
await saveUser(newUser);
|
||||
}
|
||||
})
|
||||
.catch(async () => {
|
||||
// Fallback to basic verification if ENS check fails
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
saveUser(newUser);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to basic verification if ENS check fails
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
saveUser(newUser);
|
||||
});
|
||||
} catch {
|
||||
// WalletManager not ready, fallback to basic verification
|
||||
await saveUser(newUser);
|
||||
});
|
||||
} catch {
|
||||
// WalletManager not ready, fallback to basic verification
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
await saveUser(newUser);
|
||||
}
|
||||
} else {
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
saveUser(newUser);
|
||||
await saveUser(newUser);
|
||||
}
|
||||
} else {
|
||||
setCurrentUser(newUser);
|
||||
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
|
||||
saveUser(newUser);
|
||||
|
||||
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
||||
// Note: We can't use useUserDisplay hook here since this is not a React component
|
||||
// This is just for toast messages, so simple truncation is acceptable
|
||||
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',
|
||||
description: `You can participate in the forum now! Verify your ${verificationType} for premium features and delegate a signing key for better UX.`,
|
||||
});
|
||||
}
|
||||
|
||||
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
||||
// Note: We can't use useUserDisplay hook here since this is not a React component
|
||||
// This is just for toast messages, so simple truncation is acceptable
|
||||
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',
|
||||
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);
|
||||
@ -327,7 +319,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const updatedUser = await verifyUserOwnership(currentUser);
|
||||
setCurrentUser(updatedUser);
|
||||
saveUser(updatedUser);
|
||||
await saveUser(updatedUser);
|
||||
|
||||
// Update verification status
|
||||
setVerificationStatus(getVerificationStatus(updatedUser));
|
||||
@ -411,7 +403,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
// Update user with delegation info
|
||||
const delegationStatus = delegationManager.getStatus(
|
||||
const delegationStatus = await delegationManager.getStatus(
|
||||
currentUser.address,
|
||||
currentUser.walletType
|
||||
);
|
||||
@ -426,7 +418,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
setCurrentUser(updatedUser);
|
||||
saveUser(updatedUser);
|
||||
await saveUser(updatedUser);
|
||||
|
||||
// Format date for user-friendly display
|
||||
const expiryDate = new Date(updatedUser.delegationExpiry!);
|
||||
@ -458,15 +450,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
const getDelegationStatus = (): DelegationFullStatus => {
|
||||
return delegationManager.getStatus(
|
||||
const getDelegationStatus = async (): Promise<DelegationFullStatus> => {
|
||||
return await delegationManager.getStatus(
|
||||
currentUser?.address,
|
||||
currentUser?.walletType
|
||||
);
|
||||
};
|
||||
|
||||
const clearDelegation = (): void => {
|
||||
delegationManager.clear();
|
||||
const clearDelegation = async (): Promise<void> => {
|
||||
await delegationManager.clear();
|
||||
|
||||
// Update the current user to remove delegation info
|
||||
if (currentUser) {
|
||||
@ -476,7 +468,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
browserPublicKey: undefined,
|
||||
};
|
||||
setCurrentUser(updatedUser);
|
||||
saveUser(updatedUser);
|
||||
await saveUser(updatedUser);
|
||||
}
|
||||
|
||||
toast({
|
||||
@ -490,7 +482,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
signMessage: async (
|
||||
message: OpchanMessage
|
||||
): Promise<OpchanMessage | null> => {
|
||||
return delegationManager.signMessage(message);
|
||||
return await delegationManager.signMessage(message);
|
||||
},
|
||||
verifyMessage: async (message: OpchanMessage): Promise<boolean> => {
|
||||
return await delegationManager.verify(message);
|
||||
|
||||
@ -245,7 +245,7 @@ export function useAuthActions(): AuthActions {
|
||||
|
||||
// Clear delegation
|
||||
const clearDelegation = useCallback(async (): Promise<boolean> => {
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const delegationInfo = await getDelegationStatus();
|
||||
if (!delegationInfo.isValid) {
|
||||
toast({
|
||||
title: 'No Active Delegation',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useContext, useState, useEffect } from 'react';
|
||||
import { AuthContext } from '@/contexts/AuthContext';
|
||||
import { DelegationDuration } from '@/lib/delegation';
|
||||
|
||||
@ -27,20 +27,36 @@ export const useDelegation = () => {
|
||||
contextClearDelegation();
|
||||
}, [contextClearDelegation]);
|
||||
|
||||
const delegationStatus = useMemo(() => {
|
||||
const status = contextGetDelegationStatus();
|
||||
const [delegationStatus, setDelegationStatus] = useState<{
|
||||
hasDelegation: boolean;
|
||||
isValid: boolean;
|
||||
timeRemaining?: number;
|
||||
expiresAt?: Date;
|
||||
publicKey?: string;
|
||||
address?: string;
|
||||
walletType?: 'bitcoin' | 'ethereum';
|
||||
}>({
|
||||
hasDelegation: false,
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
return {
|
||||
hasDelegation: status.hasDelegation,
|
||||
isValid: status.isValid,
|
||||
timeRemaining: status.timeRemaining,
|
||||
expiresAt: status.timeRemaining
|
||||
? new Date(Date.now() + status.timeRemaining)
|
||||
: undefined,
|
||||
publicKey: status.publicKey,
|
||||
address: status.address,
|
||||
walletType: status.walletType,
|
||||
};
|
||||
// Load delegation status
|
||||
useEffect(() => {
|
||||
contextGetDelegationStatus()
|
||||
.then(status => {
|
||||
setDelegationStatus({
|
||||
hasDelegation: status.hasDelegation,
|
||||
isValid: status.isValid,
|
||||
timeRemaining: status.timeRemaining,
|
||||
expiresAt: status.timeRemaining
|
||||
? new Date(Date.now() + status.timeRemaining)
|
||||
: undefined,
|
||||
publicKey: status.publicKey,
|
||||
address: status.address,
|
||||
walletType: status.walletType,
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [contextGetDelegationStatus]);
|
||||
|
||||
const formatTimeRemaining = useCallback((timeMs: number): string => {
|
||||
|
||||
@ -18,7 +18,8 @@ export const useMessageSigning = () => {
|
||||
const signMessage = useCallback(
|
||||
async (message: OpchanMessage): Promise<OpchanMessage | null> => {
|
||||
// Check if we have a valid delegation before attempting to sign
|
||||
if (!getDelegationStatus().isValid) {
|
||||
const delegationStatus = await getDelegationStatus();
|
||||
if (!delegationStatus.isValid) {
|
||||
console.warn('No valid delegation found. Cannot sign message.');
|
||||
return null;
|
||||
}
|
||||
@ -35,8 +36,9 @@ export const useMessageSigning = () => {
|
||||
[contextVerifyMessage]
|
||||
);
|
||||
|
||||
const canSignMessages = useCallback((): boolean => {
|
||||
return getDelegationStatus().isValid;
|
||||
const canSignMessages = useCallback(async (): Promise<boolean> => {
|
||||
const delegationStatus = await getDelegationStatus();
|
||||
return delegationStatus.isValid;
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
return {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { useForum } from '@/contexts/useForum';
|
||||
import { useAuth } from '@/hooks/core/useAuth';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { DelegationFullStatus } from '@/lib/delegation';
|
||||
|
||||
export interface NetworkHealth {
|
||||
isConnected: boolean;
|
||||
@ -64,7 +65,13 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
|
||||
const { isAuthenticated, currentUser } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
useState<DelegationFullStatus | null>(null);
|
||||
|
||||
// Load delegation status
|
||||
useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
// Network health assessment
|
||||
const health = useMemo((): NetworkHealth => {
|
||||
@ -78,7 +85,7 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
issues.push(`Forum error: ${error}`);
|
||||
}
|
||||
|
||||
if (isAuthenticated && !delegationInfo.isValid) {
|
||||
if (isAuthenticated && !delegationInfo?.isValid) {
|
||||
issues.push('Key delegation expired');
|
||||
}
|
||||
|
||||
@ -93,7 +100,7 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
syncAge,
|
||||
issues,
|
||||
};
|
||||
}, [isNetworkConnected, error, isAuthenticated, delegationInfo.isValid]);
|
||||
}, [isNetworkConnected, error, isAuthenticated, delegationInfo?.isValid]);
|
||||
|
||||
// Sync status
|
||||
const sync = useMemo((): SyncStatus => {
|
||||
@ -124,9 +131,9 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
status: isAuthenticated ? 'connected' : 'disconnected',
|
||||
},
|
||||
delegation: {
|
||||
active: delegationInfo.isValid,
|
||||
expires: delegationInfo.timeRemaining || null,
|
||||
status: delegationInfo.isValid ? 'active' : 'expired',
|
||||
active: delegationInfo?.isValid || false,
|
||||
expires: delegationInfo?.timeRemaining || null,
|
||||
status: delegationInfo?.isValid ? 'active' : 'expired',
|
||||
},
|
||||
};
|
||||
}, [isNetworkConnected, isAuthenticated, currentUser, delegationInfo]);
|
||||
@ -134,7 +141,7 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
// Status assessment
|
||||
const canRefresh = !isRefreshing && !isInitialLoading;
|
||||
const canSync = isNetworkConnected && !isRefreshing;
|
||||
const needsAttention = !health.isHealthy || !delegationInfo.isValid;
|
||||
const needsAttention = !health.isHealthy || !delegationInfo?.isValid;
|
||||
|
||||
// Helper methods
|
||||
const getStatusMessage = useMemo(() => {
|
||||
@ -157,10 +164,15 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
const getHealthColor = useMemo(() => {
|
||||
return (): 'green' | 'yellow' | 'red' => {
|
||||
if (!isNetworkConnected || error) return 'red';
|
||||
if (health.issues.length > 0 || !delegationInfo.isValid) return 'yellow';
|
||||
if (health.issues.length > 0 || !delegationInfo?.isValid) return 'yellow';
|
||||
return 'green';
|
||||
};
|
||||
}, [isNetworkConnected, error, health.issues.length, delegationInfo.isValid]);
|
||||
}, [
|
||||
isNetworkConnected,
|
||||
error,
|
||||
health.issues.length,
|
||||
delegationInfo?.isValid,
|
||||
]);
|
||||
|
||||
const getRecommendedActions = useMemo(() => {
|
||||
return (): string[] => {
|
||||
@ -175,13 +187,13 @@ export function useNetworkStatus(): NetworkStatusData {
|
||||
actions.push('Connect your wallet');
|
||||
}
|
||||
|
||||
if (!delegationInfo.isValid) {
|
||||
if (!delegationInfo?.isValid) {
|
||||
actions.push('Renew key delegation');
|
||||
}
|
||||
|
||||
if (
|
||||
delegationInfo.isValid &&
|
||||
delegationInfo.timeRemaining &&
|
||||
delegationInfo?.isValid &&
|
||||
delegationInfo?.timeRemaining &&
|
||||
delegationInfo.timeRemaining < 3600
|
||||
) {
|
||||
actions.push('Consider renewing key delegation soon');
|
||||
|
||||
@ -14,7 +14,8 @@ import {
|
||||
} from '@/types/waku';
|
||||
import { OpchanMessage } from '@/types/forum';
|
||||
import { MessageValidator } from '@/lib/utils/MessageValidator';
|
||||
import { EVerificationStatus } from '@/types/identity';
|
||||
import { EVerificationStatus, User } from '@/types/identity';
|
||||
import { DelegationInfo } from '@/lib/delegation/types';
|
||||
import { openLocalDB, STORE, StoreName } from '@/lib/database/schema';
|
||||
|
||||
export interface LocalDatabaseCache {
|
||||
@ -277,6 +278,9 @@ export class LocalDatabase {
|
||||
| ModerateMessage
|
||||
| ({ address: string } & UserIdentityCache[string])
|
||||
| { key: string; value: unknown }
|
||||
| { key: string; value: User; timestamp: number }
|
||||
| { key: string; value: DelegationInfo; timestamp: number }
|
||||
| { key: string; value: unknown; timestamp: number }
|
||||
): void {
|
||||
if (!this.db) return;
|
||||
const tx = this.db.transaction(storeName, 'readwrite');
|
||||
@ -327,6 +331,159 @@ export class LocalDatabase {
|
||||
this.pendingListeners.add(listener);
|
||||
return () => this.pendingListeners.delete(listener);
|
||||
}
|
||||
|
||||
// ===== User Authentication Storage =====
|
||||
|
||||
/**
|
||||
* Store user authentication data
|
||||
*/
|
||||
public async storeUser(user: User): Promise<void> {
|
||||
const userData = {
|
||||
key: 'current',
|
||||
value: user,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.put(STORE.USER_AUTH, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user authentication data
|
||||
*/
|
||||
public async loadUser(): Promise<User | null> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db!.transaction(STORE.USER_AUTH, 'readonly');
|
||||
const store = tx.objectStore(STORE.USER_AUTH);
|
||||
const request = store.get('current');
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as
|
||||
| { key: string; value: User; timestamp: number }
|
||||
| undefined;
|
||||
if (!result) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = result.value;
|
||||
const lastChecked = user.lastChecked || 0;
|
||||
const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
if (Date.now() - lastChecked < expiryTime) {
|
||||
resolve(user);
|
||||
} else {
|
||||
// User data expired, clear it
|
||||
this.clearUser();
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear user authentication data
|
||||
*/
|
||||
public async clearUser(): Promise<void> {
|
||||
if (!this.db) return;
|
||||
|
||||
const tx = this.db.transaction(STORE.USER_AUTH, 'readwrite');
|
||||
const store = tx.objectStore(STORE.USER_AUTH);
|
||||
store.delete('current');
|
||||
}
|
||||
|
||||
// ===== Delegation Storage =====
|
||||
|
||||
/**
|
||||
* Store delegation information
|
||||
*/
|
||||
public async storeDelegation(delegation: DelegationInfo): Promise<void> {
|
||||
const delegationData = {
|
||||
key: 'current',
|
||||
value: delegation,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.put(STORE.DELEGATION, delegationData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load delegation information
|
||||
*/
|
||||
public async loadDelegation(): Promise<DelegationInfo | null> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db!.transaction(STORE.DELEGATION, 'readonly');
|
||||
const store = tx.objectStore(STORE.DELEGATION);
|
||||
const request = store.get('current');
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as
|
||||
| { key: string; value: DelegationInfo; timestamp: number }
|
||||
| undefined;
|
||||
resolve(result?.value || null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear delegation information
|
||||
*/
|
||||
public async clearDelegation(): Promise<void> {
|
||||
if (!this.db) return;
|
||||
|
||||
const tx = this.db.transaction(STORE.DELEGATION, 'readwrite');
|
||||
const store = tx.objectStore(STORE.DELEGATION);
|
||||
store.delete('current');
|
||||
}
|
||||
|
||||
// ===== UI State Storage =====
|
||||
|
||||
/**
|
||||
* Store UI state value
|
||||
*/
|
||||
public async storeUIState(key: string, value: unknown): Promise<void> {
|
||||
const stateData = {
|
||||
key,
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
this.put(STORE.UI_STATE, stateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load UI state value
|
||||
*/
|
||||
public async loadUIState(key: string): Promise<unknown> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db!.transaction(STORE.UI_STATE, 'readonly');
|
||||
const store = tx.objectStore(STORE.UI_STATE);
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as
|
||||
| { key: string; value: unknown; timestamp: number }
|
||||
| undefined;
|
||||
resolve(result?.value || null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear UI state value
|
||||
*/
|
||||
public async clearUIState(key: string): Promise<void> {
|
||||
if (!this.db) return;
|
||||
|
||||
const tx = this.db.transaction(STORE.UI_STATE, 'readwrite');
|
||||
const store = tx.objectStore(STORE.UI_STATE);
|
||||
store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export const localDatabase = new LocalDatabase();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export const DB_NAME = 'opchan-local';
|
||||
export const DB_VERSION = 1;
|
||||
export const DB_VERSION = 2;
|
||||
|
||||
export const STORE = {
|
||||
CELLS: 'cells',
|
||||
@ -8,6 +8,9 @@ export const STORE = {
|
||||
VOTES: 'votes',
|
||||
MODERATIONS: 'moderations',
|
||||
USER_IDENTITIES: 'userIdentities',
|
||||
USER_AUTH: 'userAuth',
|
||||
DELEGATION: 'delegation',
|
||||
UI_STATE: 'uiState',
|
||||
META: 'meta',
|
||||
} as const;
|
||||
|
||||
@ -53,6 +56,18 @@ export function openLocalDB(): Promise<IDBDatabase> {
|
||||
// User identities keyed by address
|
||||
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.USER_AUTH)) {
|
||||
// User authentication data with single key 'current'
|
||||
db.createObjectStore(STORE.USER_AUTH, { keyPath: 'key' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.DELEGATION)) {
|
||||
// Key delegation information with single key 'current'
|
||||
db.createObjectStore(STORE.DELEGATION, { keyPath: 'key' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.UI_STATE)) {
|
||||
// UI state like wizard flags, preferences
|
||||
db.createObjectStore(STORE.UI_STATE, { keyPath: 'key' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE.META)) {
|
||||
// Misc metadata like lastSync timestamps
|
||||
db.createObjectStore(STORE.META, { keyPath: 'key' });
|
||||
|
||||
@ -72,7 +72,7 @@ export class DelegationManager {
|
||||
nonce,
|
||||
};
|
||||
|
||||
DelegationStorage.store(delegationInfo);
|
||||
await DelegationStorage.store(delegationInfo);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating delegation:', error);
|
||||
@ -83,13 +83,13 @@ export class DelegationManager {
|
||||
/**
|
||||
* Sign a message with delegated key
|
||||
*/
|
||||
signMessage(message: UnsignedMessage): OpchanMessage | null {
|
||||
async signMessage(message: UnsignedMessage): Promise<OpchanMessage | null> {
|
||||
const now = Date.now();
|
||||
if (
|
||||
!this.cachedDelegation ||
|
||||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS
|
||||
) {
|
||||
this.cachedDelegation = DelegationStorage.retrieve();
|
||||
this.cachedDelegation = await DelegationStorage.retrieve();
|
||||
this.cachedAt = now;
|
||||
}
|
||||
const delegation = this.cachedDelegation;
|
||||
@ -162,16 +162,16 @@ export class DelegationManager {
|
||||
/**
|
||||
* Get delegation status
|
||||
*/
|
||||
getStatus(
|
||||
async getStatus(
|
||||
currentAddress?: string,
|
||||
currentWalletType?: 'bitcoin' | 'ethereum'
|
||||
): DelegationFullStatus {
|
||||
): Promise<DelegationFullStatus> {
|
||||
const now = Date.now();
|
||||
if (
|
||||
!this.cachedDelegation ||
|
||||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS
|
||||
) {
|
||||
this.cachedDelegation = DelegationStorage.retrieve();
|
||||
this.cachedDelegation = await DelegationStorage.retrieve();
|
||||
this.cachedAt = now;
|
||||
}
|
||||
const delegation = this.cachedDelegation;
|
||||
@ -202,8 +202,8 @@ export class DelegationManager {
|
||||
/**
|
||||
* Clear stored delegation
|
||||
*/
|
||||
clear(): void {
|
||||
DelegationStorage.clear();
|
||||
async clear(): Promise<void> {
|
||||
await DelegationStorage.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -1,39 +1,35 @@
|
||||
import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants';
|
||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||
import { DelegationInfo } from './types';
|
||||
|
||||
export class DelegationStorage {
|
||||
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
|
||||
|
||||
/**
|
||||
* Store delegation information in localStorage
|
||||
* Store delegation information in IndexedDB
|
||||
*/
|
||||
static store(delegation: DelegationInfo): void {
|
||||
static async store(delegation: DelegationInfo): Promise<void> {
|
||||
// Reduce verbose logging in production; keep minimal signal
|
||||
if (import.meta.env?.MODE !== 'production') {
|
||||
console.log('DelegationStorage.store');
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
DelegationStorage.STORAGE_KEY,
|
||||
JSON.stringify(delegation)
|
||||
);
|
||||
try {
|
||||
await localDatabase.storeDelegation(delegation);
|
||||
} catch (e) {
|
||||
console.error('Failed to store delegation information', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve delegation information from localStorage
|
||||
* Retrieve delegation information from IndexedDB
|
||||
*/
|
||||
static retrieve(): DelegationInfo | null {
|
||||
const delegationJson = localStorage.getItem(DelegationStorage.STORAGE_KEY);
|
||||
if (!delegationJson) return null;
|
||||
|
||||
static async retrieve(): Promise<DelegationInfo | null> {
|
||||
try {
|
||||
const delegation = JSON.parse(delegationJson);
|
||||
const delegation = await localDatabase.loadDelegation();
|
||||
if (import.meta.env?.MODE !== 'production') {
|
||||
console.log('DelegationStorage.retrieve');
|
||||
}
|
||||
return delegation;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse delegation information', e);
|
||||
console.error('Failed to retrieve delegation information', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -41,7 +37,11 @@ export class DelegationStorage {
|
||||
/**
|
||||
* Clear stored delegation information
|
||||
*/
|
||||
static clear(): void {
|
||||
localStorage.removeItem(DelegationStorage.STORAGE_KEY);
|
||||
static async clear(): Promise<void> {
|
||||
try {
|
||||
await localDatabase.clearDelegation();
|
||||
} catch (e) {
|
||||
console.error('Failed to clear delegation information', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,9 +100,9 @@ export class ForumActions {
|
||||
author: currentUser!.address, // Safe after validation
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedPost);
|
||||
const signed = await this.delegationManager.signMessage(unsignedPost);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -169,9 +169,9 @@ export class ForumActions {
|
||||
};
|
||||
|
||||
// Optimistic path: sign locally, write to cache, mark pending, render immediately
|
||||
const signed = this.delegationManager.signMessage(unsignedComment);
|
||||
const signed = await this.delegationManager.signMessage(unsignedComment);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -241,9 +241,9 @@ export class ForumActions {
|
||||
author: currentUser!.address,
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedCell);
|
||||
const signed = await this.delegationManager.signMessage(unsignedCell);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -313,9 +313,9 @@ export class ForumActions {
|
||||
author: currentUser!.address,
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedVote);
|
||||
const signed = await this.delegationManager.signMessage(unsignedVote);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -384,9 +384,9 @@ export class ForumActions {
|
||||
author: currentUser!.address,
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedMod);
|
||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -457,9 +457,9 @@ export class ForumActions {
|
||||
author: currentUser!.address,
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedMod);
|
||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
@ -530,9 +530,9 @@ export class ForumActions {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const signed = this.delegationManager.signMessage(unsignedMod);
|
||||
const signed = await this.delegationManager.signMessage(unsignedMod);
|
||||
if (!signed) {
|
||||
const status = this.delegationManager.getStatus(
|
||||
const status = await this.delegationManager.getStatus(
|
||||
currentUser!.address,
|
||||
currentUser!.walletType
|
||||
);
|
||||
|
||||
@ -29,11 +29,11 @@ export class MessageService implements MessageServiceInterface {
|
||||
*/
|
||||
async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
|
||||
try {
|
||||
const signedMessage = this.delegationManager.signMessage(message);
|
||||
const signedMessage = await this.delegationManager.signMessage(message);
|
||||
|
||||
if (!signedMessage) {
|
||||
// Check if delegation exists but is expired
|
||||
const delegationStatus = this.delegationManager.getStatus();
|
||||
const delegationStatus = await this.delegationManager.getStatus();
|
||||
const isDelegationExpired = !delegationStatus.isValid;
|
||||
|
||||
return {
|
||||
@ -87,7 +87,7 @@ export class MessageService implements MessageServiceInterface {
|
||||
message: UnsignedMessage
|
||||
): Promise<OpchanMessage | null> {
|
||||
try {
|
||||
const signedMessage = this.delegationManager.signMessage(message);
|
||||
const signedMessage = await this.delegationManager.signMessage(message);
|
||||
if (!signedMessage) {
|
||||
console.error('Failed to sign message');
|
||||
return null;
|
||||
|
||||
@ -9,10 +9,17 @@ interface ValidationReport {
|
||||
}
|
||||
|
||||
export class MessageValidator {
|
||||
private delegationManager: DelegationManager;
|
||||
private delegationManager?: DelegationManager;
|
||||
|
||||
constructor(delegationManager?: DelegationManager) {
|
||||
this.delegationManager = delegationManager || new DelegationManager();
|
||||
this.delegationManager = delegationManager;
|
||||
}
|
||||
|
||||
private getDelegationManager(): DelegationManager {
|
||||
if (!this.delegationManager) {
|
||||
this.delegationManager = new DelegationManager();
|
||||
}
|
||||
return this.delegationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,7 +33,7 @@ export class MessageValidator {
|
||||
|
||||
// Verify signature and delegation proof - we know it's safe to cast here since hasRequiredFields passed
|
||||
try {
|
||||
return await this.delegationManager.verify(
|
||||
return await this.getDelegationManager().verify(
|
||||
message as unknown as OpchanMessage
|
||||
);
|
||||
} catch {
|
||||
@ -80,7 +87,7 @@ export class MessageValidator {
|
||||
|
||||
// Verify signature
|
||||
try {
|
||||
const isValid = await this.delegationManager.verify(
|
||||
const isValid = await this.getDelegationManager().verify(
|
||||
message as unknown as OpchanMessage
|
||||
);
|
||||
if (!isValid) {
|
||||
@ -120,7 +127,7 @@ export class MessageValidator {
|
||||
|
||||
// Verify signature and delegation proof
|
||||
try {
|
||||
const isValid = await this.delegationManager.verify(
|
||||
const isValid = await this.getDelegationManager().verify(
|
||||
message as unknown as OpchanMessage
|
||||
);
|
||||
if (!isValid) {
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useAuth, useUserActions, useForumActions } from '@/hooks';
|
||||
import { useAuth as useAuthContext } from '@/contexts/useAuth';
|
||||
import { useUserDisplay } from '@/hooks';
|
||||
import { DelegationFullStatus } from '@/lib/delegation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -37,9 +38,15 @@ export default function ProfilePage() {
|
||||
// Get current user from auth context for the address
|
||||
const { currentUser } = useAuth();
|
||||
const { getDelegationStatus } = useAuthContext();
|
||||
const delegationInfo = getDelegationStatus();
|
||||
const [delegationInfo, setDelegationInfo] =
|
||||
useState<DelegationFullStatus | null>(null);
|
||||
const address = currentUser?.address;
|
||||
|
||||
// Load delegation status
|
||||
useEffect(() => {
|
||||
getDelegationStatus().then(setDelegationInfo).catch(console.error);
|
||||
}, [getDelegationStatus]);
|
||||
|
||||
// Get comprehensive user information from the unified hook
|
||||
const userInfo = useUserDisplay(address || '');
|
||||
|
||||
@ -341,21 +348,21 @@ export default function ProfilePage() {
|
||||
{/* Delegation Status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge
|
||||
variant={delegationInfo.isValid ? 'default' : 'secondary'}
|
||||
variant={delegationInfo?.isValid ? 'default' : 'secondary'}
|
||||
className={
|
||||
delegationInfo.isValid
|
||||
delegationInfo?.isValid
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{delegationInfo.isValid ? 'Active' : 'Inactive'}
|
||||
{delegationInfo?.isValid ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
{delegationInfo.isValid && delegationInfo.timeRemaining && (
|
||||
{delegationInfo?.isValid && delegationInfo?.timeRemaining && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{delegationInfo.timeRemaining} remaining
|
||||
</span>
|
||||
)}
|
||||
{!delegationInfo.isValid && (
|
||||
{!delegationInfo?.isValid && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-yellow-600 border-yellow-600"
|
||||
@ -363,7 +370,7 @@ export default function ProfilePage() {
|
||||
Renewal Recommended
|
||||
</Badge>
|
||||
)}
|
||||
{!delegationInfo.isValid && (
|
||||
{!delegationInfo?.isValid && (
|
||||
<Badge variant="destructive">Expired</Badge>
|
||||
)}
|
||||
</div>
|
||||
@ -426,7 +433,7 @@ export default function ProfilePage() {
|
||||
Can Delegate
|
||||
</Label>
|
||||
<div className="mt-1 text-sm">
|
||||
{delegationInfo.hasDelegation ? (
|
||||
{delegationInfo?.hasDelegation ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-green-600 border-green-600"
|
||||
@ -446,14 +453,14 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{/* Delegation Actions */}
|
||||
{delegationInfo.hasDelegation && (
|
||||
{delegationInfo?.hasDelegation && (
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setWalletWizardOpen(true)}
|
||||
>
|
||||
{delegationInfo.isValid
|
||||
{delegationInfo?.isValid
|
||||
? 'Renew Delegation'
|
||||
: 'Delegate Key'}
|
||||
</Button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user