chore: move all storages to indexedDB

This commit is contained in:
Danish Arora 2025-09-05 14:03:29 +05:30
parent 9e6be6156f
commit aa17bda249
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
17 changed files with 463 additions and 221 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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';
};

View File

@ -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);

View File

@ -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',

View File

@ -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 => {

View File

@ -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 {

View File

@ -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');

View File

@ -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();

View File

@ -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' });

View File

@ -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();
}
// ============================================================================

View File

@ -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);
}
}
}

View File

@ -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
);

View File

@ -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;

View File

@ -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) {

View File

@ -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>