diff --git a/src/components/TransactionMonitor.tsx b/src/components/TransactionMonitor.tsx
index 5b33d35f..bf62d3cd 100644
--- a/src/components/TransactionMonitor.tsx
+++ b/src/components/TransactionMonitor.tsx
@@ -3,16 +3,18 @@
import React from 'react';
import { useTransactionStore } from '@/store/transactionStore';
import type { Transaction } from '@/store/transactionStore';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
const TransactionWatcher = ({ transaction }: { transaction: Transaction }) => {
const { updateTransaction } = useTransactionStore();
+ const { setTimeoutSafe, clearTimeoutSafe } = useSafeTimeout();
// For demo purposes, we'll simulate transaction monitoring
// In a real app, you'd use wagmi's useWaitForTransactionReceipt here
React.useEffect(() => {
if (transaction.status === 'pending') {
// Simulate confirmation after 5 seconds for demo
- const timer = setTimeout(() => {
+ const timer = setTimeoutSafe(() => {
updateTransaction(transaction.id, {
status: 'confirmed',
gasUsed: '21000',
@@ -20,7 +22,7 @@ const TransactionWatcher = ({ transaction }: { transaction: Transaction }) => {
});
}, 5000);
- return () => clearTimeout(timer);
+ return () => clearTimeoutSafe(timer);
}
return undefined;
diff --git a/src/components/ViewToggle.tsx b/src/components/ViewToggle.tsx
index eca5eeff..18430596 100644
--- a/src/components/ViewToggle.tsx
+++ b/src/components/ViewToggle.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
+import { logger } from '@/utils/logger';
/**
* UI-only view mode for listing screens.
@@ -30,9 +31,7 @@ export function useViewMode() {
const stored = localStorage.getItem(STORAGE_KEY);
if (isValidViewMode(stored)) return stored;
} catch (err) {
- // Swallow storage errors — fallback to default
- // eslint-disable-next-line no-console
- console.warn("useViewMode: localStorage unavailable, falling back to default view mode", err);
+ logger.warn("useViewMode: localStorage unavailable, falling back to default view mode", err);
}
return "grid";
@@ -41,8 +40,7 @@ export function useViewMode() {
// Wrap setter to validate input before persisting
const setMode = (v: ViewMode) => {
if (!isValidViewMode(v)) {
- // eslint-disable-next-line no-console
- console.warn("useViewMode.setMode called with invalid mode:", v);
+ logger.warn("useViewMode.setMode called with invalid mode:", v);
return;
}
setModeRaw(v);
@@ -52,9 +50,7 @@ export function useViewMode() {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch (err) {
- // Storage might be disabled; log and continue without throwing
- // eslint-disable-next-line no-console
- console.warn("useViewMode: failed to persist mode to localStorage", err);
+ logger.warn("useViewMode: failed to persist mode to localStorage", err);
}
}, [mode]);
@@ -80,11 +76,9 @@ export function ViewToggle({ mode, onChange }: ViewToggleProps) {
if (!isValidViewMode(v)) return;
try {
if (typeof onChange === "function") onChange(v);
- else console.warn("ViewToggle: onChange is not a function", onChange);
+ else logger.warn("ViewToggle: onChange is not a function", onChange);
} catch (err) {
- // Avoid bubbling UI errors — log instead
- // eslint-disable-next-line no-console
- console.error("ViewToggle: onChange handler threw an error", err);
+ logger.error("ViewToggle: onChange handler threw an error", err);
}
};
diff --git a/src/components/dashboard/DataRefreshWrapper.tsx b/src/components/dashboard/DataRefreshWrapper.tsx
index 2e8e9d43..6728530e 100644
--- a/src/components/dashboard/DataRefreshWrapper.tsx
+++ b/src/components/dashboard/DataRefreshWrapper.tsx
@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { RefreshCw, AlertCircle, CheckCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
interface DataRefreshWrapperProps {
children: ReactNode;
@@ -21,17 +22,7 @@ export const DataRefreshWrapper = ({
}: DataRefreshWrapperProps) => {
const [refreshState, setRefreshState] = useState
("idle");
const [error, setError] = useState(null);
- const successTimerRef = useRef | null>(null);
-
- // Cleanup all timers on unmount
- useEffect(() => {
- return () => {
- if (successTimerRef.current) {
- clearTimeout(successTimerRef.current);
- successTimerRef.current = null;
- }
- };
- }, []);
+ const { setTimeoutSafe } = useSafeTimeout();
const handleRefresh = useCallback(async () => {
// Cancel any pending success timeout from a previous refresh
@@ -61,10 +52,7 @@ export const DataRefreshWrapper = ({
}
setRefreshState("success");
- successTimerRef.current = setTimeout(() => {
- setRefreshState("idle");
- successTimerRef.current = null;
- }, 2000);
+ setTimeoutSafe(() => setRefreshState("idle"), 2000);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
setRefreshState("error");
diff --git a/src/components/dashboard/PortfolioReport.tsx b/src/components/dashboard/PortfolioReport.tsx
index 68dbe200..43ee3c6a 100644
--- a/src/components/dashboard/PortfolioReport.tsx
+++ b/src/components/dashboard/PortfolioReport.tsx
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
type ReportType = "full" | "tax" | "performance" | "transactions";
@@ -81,6 +82,7 @@ export const PortfolioReport = () => {
const [year, setYear] = useState("2024");
const [isGenerating, setIsGenerating] = useState(false);
const [isGenerated, setIsGenerated] = useState(false);
+ const { setTimeoutSafe } = useSafeTimeout();
const generatePDF = async () => {
setIsGenerating(true);
@@ -185,7 +187,7 @@ export const PortfolioReport = () => {
setIsGenerating(false);
setIsGenerated(true);
- setTimeout(() => setIsGenerated(false), 3000);
+ setTimeoutSafe(() => setIsGenerated(false), 3000);
};
return (
diff --git a/src/components/dashboard/StakingModal.tsx b/src/components/dashboard/StakingModal.tsx
index aa6a4b0c..d7c794e6 100644
--- a/src/components/dashboard/StakingModal.tsx
+++ b/src/components/dashboard/StakingModal.tsx
@@ -1,6 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import {
X,
ShieldCheck,
@@ -57,6 +58,7 @@ export const StakingModal: React.FC = ({
const [lockPeriod, setLockPeriod] = useState(3); // months
const [isProcessing, setIsProcessing] = useState(false);
const [selectedToken, setSelectedToken] = useState(token || mockAvailableTokens[0]);
+ const { setTimeoutSafe } = useSafeTimeout();
useEffect(() => {
if (isOpen) {
@@ -89,7 +91,7 @@ export const StakingModal: React.FC = ({
const handleClose = () => {
onClose();
// Small delay to reset state after animation
- setTimeout(() => setStep('input'), 300);
+ setTimeoutSafe(() => setStep('input'), 300);
};
const apyMultiplier = 1 + (lockPeriod / 12) * 0.5; // Simple mock formula
diff --git a/src/components/kyc/KycVerificationCenter.tsx b/src/components/kyc/KycVerificationCenter.tsx
index c62c37a1..eb367d43 100644
--- a/src/components/kyc/KycVerificationCenter.tsx
+++ b/src/components/kyc/KycVerificationCenter.tsx
@@ -2,6 +2,7 @@
import { useMemo, useState, type ChangeEvent } from 'react';
import { useRouter } from 'next/navigation';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import { BadgeCheck, FileUp, ScanFace, ShieldAlert, ShieldCheck, UploadCloud } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -20,6 +21,7 @@ export function KycVerificationCenter() {
useKycStore();
const [thresholdDraft, setThresholdDraft] = useState(String(profile.thresholdEth));
const [notes, setNotes] = useState('');
+ const { setTimeoutSafe } = useSafeTimeout();
const completion = useMemo(() => {
const steps = [
@@ -48,7 +50,7 @@ export function KycVerificationCenter() {
const handleLiveness = async () => {
startLivenessCheck();
- window.setTimeout(() => {
+ setTimeoutSafe(() => {
completeLivenessCheck(true);
}, 1200);
};
diff --git a/src/components/referral/ClaimRewardsModal.tsx b/src/components/referral/ClaimRewardsModal.tsx
index f4d928e8..0e12ca78 100644
--- a/src/components/referral/ClaimRewardsModal.tsx
+++ b/src/components/referral/ClaimRewardsModal.tsx
@@ -6,6 +6,7 @@ import { useReferralStore, useReferralStats } from '@/store/referralStore';
import { referralService } from '@/lib/referralService';
import { createWalletAddress } from '@/types/referral';
import { formatUnits } from 'viem';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
const SUPPORTED_CHAIN_NAMES: Record = {
1: 'Ethereum',
@@ -37,6 +38,7 @@ export default function ClaimRewardsModal({
const [step, setStep] = useState<'confirm' | 'claiming' | 'success' | 'error'>(
'confirm'
);
+ const { setTimeoutSafe } = useSafeTimeout();
const { setIsClaimingRewards, setNotification } = useReferralStore();
const stats = useReferralStats();
@@ -81,7 +83,7 @@ export default function ClaimRewardsModal({
setNotification('Rewards claimed successfully!', 'success');
// Close modal after 3 seconds
- setTimeout(onClose, 3000);
+ setTimeoutSafe(onClose, 3000);
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to claim rewards';
diff --git a/src/components/referral/ReferralLinksCard.tsx b/src/components/referral/ReferralLinksCard.tsx
index 06e2e045..be884ec3 100644
--- a/src/components/referral/ReferralLinksCard.tsx
+++ b/src/components/referral/ReferralLinksCard.tsx
@@ -12,6 +12,7 @@ import { createWalletAddress } from '@/types/referral';
import CopyButton from './CopyButton';
import ShareButton from './ShareButton';
import CreateReferralLinkModal from './CreateReferralLinkModal';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
export interface ReferralLinksCardProps {
maxLinksToShow?: number;
@@ -25,12 +26,13 @@ export default function ReferralLinksCard({
const [showCreateModal, setShowCreateModal] = useState(false);
const [copiedCode, setCopiedCode] = useState(null);
const [loadingShortUrl, setLoadingShortUrl] = useState(null);
+ const { setTimeoutSafe } = useSafeTimeout();
const handleCopy = useCallback(
(code: string, url: string) => {
navigator.clipboard.writeText(url);
setCopiedCode(code);
- setTimeout(() => setCopiedCode(null), 2000);
+ setTimeoutSafe(() => setCopiedCode(null), 2000);
},
[]
);
diff --git a/src/components/responsive/LazyLoadingExample.tsx b/src/components/responsive/LazyLoadingExample.tsx
index c036bb53..535e2f68 100644
--- a/src/components/responsive/LazyLoadingExample.tsx
+++ b/src/components/responsive/LazyLoadingExample.tsx
@@ -3,6 +3,7 @@
import React from 'react';
import { ImagePlaceholder, SkeletonImage, Skeleton } from './ImagePlaceholder';
import { setupLazyLoading, preloadCriticalResources } from '@/lib/mobile-optimizer';
+import { useSafeTimeout } from '@/hooks/useSafeTimeout';
/**
* Lazy Loading System Examples
@@ -169,10 +170,11 @@ export const CustomPlaceholderExample: React.FC = () => {
*/
export const ContentSkeletonExample: React.FC = () => {
const [isLoading, setIsLoading] = React.useState(true);
+ const { setTimeoutSafe } = useSafeTimeout();
React.useEffect(() => {
// Simulate data loading
- setTimeout(() => setIsLoading(false), 2000);
+ setTimeoutSafe(() => setIsLoading(false), 2000);
}, []);
if (isLoading) {
@@ -210,13 +212,14 @@ export const ContentSkeletonExample: React.FC = () => {
export const PropertyListingExample: React.FC = () => {
const [properties, setProperties] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(true);
+ const { setTimeoutSafe } = useSafeTimeout();
React.useEffect(() => {
// Preload hero image
preloadCriticalResources(['/images/hero-property.jpg']);
// Simulate API call
- setTimeout(() => {
+ setTimeoutSafe(() => {
setProperties([
{ id: 1, image: '/images/property-1.jpg', title: 'Modern Villa', price: '$850,000' },
{ id: 2, image: '/images/property-2.jpg', title: 'Cozy Cottage', price: '$450,000' },
diff --git a/src/hooks/useSafeTimeout.ts b/src/hooks/useSafeTimeout.ts
new file mode 100644
index 00000000..654cc3bc
--- /dev/null
+++ b/src/hooks/useSafeTimeout.ts
@@ -0,0 +1,26 @@
+import { useEffect, useRef, useCallback } from 'react';
+
+export function useSafeTimeout() {
+ const timeoutsRef = useRef>>(new Set());
+
+ useEffect(() => {
+ return () => {
+ timeoutsRef.current.forEach(clearTimeout);
+ timeoutsRef.current.clear();
+ };
+ }, []);
+
+ const setTimeoutSafe = useCallback((fn: () => void, delay: number) => {
+ const id = setTimeout(fn, delay);
+ timeoutsRef.current.add(id);
+ return id;
+ }, []);
+
+ const clearTimeoutSafe = useCallback((id: ReturnType) => {
+ clearTimeout(id);
+ timeoutsRef.current.delete(id);
+ return id;
+ }, []);
+
+ return { setTimeoutSafe, clearTimeoutSafe };
+}
diff --git a/src/utils/security/phishingProtection.ts b/src/utils/security/phishingProtection.ts
index 2720e823..913466c2 100644
--- a/src/utils/security/phishingProtection.ts
+++ b/src/utils/security/phishingProtection.ts
@@ -88,7 +88,7 @@ export class PhishingProtection {
}
} catch (error) {
- // URL constructor throws on malformed URLs — treat as suspicious
+ logger.warn('Invalid URL format detected in phishing check:', error);
warnings.push('Invalid URL format');
riskScore += 20;
}
@@ -133,6 +133,7 @@ export class PhishingProtection {
decodedData = messageAnalysis.decodedData;
} catch (error) {
+ logger.warn('Invalid signature format:', error);
return {
isValid: false,
isMalicious: true,
@@ -191,6 +192,7 @@ export class PhishingProtection {
}
} catch (error) {
+ logger.warn('Unable to decode transaction data:', error);
warnings.push('Unable to decode transaction data');
}
}
@@ -381,6 +383,7 @@ export class PhishingProtection {
}
} catch {
+ logger.warn('Failed to parse message as JSON, analyzing as text');
// Not JSON, analyze as text
if (this.containsSensitiveOperations(message)) {
warnings.push('Message contains sensitive operations');
@@ -420,6 +423,7 @@ export class PhishingProtection {
decoded: false // Would need ABI for full decoding
};
} catch {
+ logger.warn('Failed to decode transaction data');
return null;
}
}
diff --git a/src/utils/walletHelpers.ts b/src/utils/walletHelpers.ts
index f1f0141d..a80e6f53 100644
--- a/src/utils/walletHelpers.ts
+++ b/src/utils/walletHelpers.ts
@@ -27,10 +27,8 @@ export function assertValidAddress(addr: string): string {
const checksummed = getAddress(trimmed);
return checksummed;
} catch {
- logger.warn('EIP-55 checksum validation failed for address:', { address: trimmed.slice(0, 6) + '...' });
- throw new Error(
- 'Invalid wallet address checksum. Please verify the address is correct and try again.'
- );
+ logger.warn('Failed to parse balance');
+ return '0.0000';
}
}