Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,12 @@ RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_MAX_REQUESTS_PER_WALLET=50

# -----------------------------------------------------------------------------
# Webhook Configuration
# -----------------------------------------------------------------------------
# Chainalysis Security API Configuration
# -----------------------------------------------------------------------------
# API key for Chainalysis address/transaction risk checks.
# IMPORTANT: This is read server-side only. Never expose it to the browser.
# CHAINALYSIS_API_KEY=your_chainalysis_api_key

# Chainalysis API base URL (optional, uses default if not set)
# CHAINALYSIS_API_URL=https://api.chainalysis.com/api/v2
# Secret used to sign and verify revalidation webhook requests
# This must be set in production to secure the /api/revalidate endpoint
REVALIDATE_WEBHOOK_SECRET=your-webhook-secret

# -----------------------------------------------------------------------------
# Secret Management (Production)
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { revalidateProperty, revalidateAllProperties } from '@/lib/propertyServi
import { requireEnvStrict } from '@/lib/requireEnv';
import crypto from 'crypto';

const WEBHOOK_SECRET = requireEnvStrict('REVALIDATE_WEBHOOK_SECRET');
const WEBHOOK_SECRET: string | undefined = process.env.REVALIDATE_WEBHOOK_SECRET;

export async function POST(request: NextRequest) {
try {
if (!WEBHOOK_SECRET) {
return NextResponse.json(
{ error: 'REVALIDATE_WEBHOOK_SECRET is not configured' },
{ status: 500 }
);
}

// Verify webhook signature for security
const signature = request.headers.get('x-webhook-signature');
const body = await request.text();
Expand Down
4 changes: 3 additions & 1 deletion src/app/widget/embed-code/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, { useState, useCallback } from 'react';
import Link from 'next/link';
import { ArrowLeft, Copy, Check, Code, ExternalLink } from 'lucide-react';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
Expand All @@ -22,6 +23,7 @@ export default function WidgetEmbedCodePage() {
const [ctaText, setCtaText] = useState('Invest on PropChain');
const [compact, setCompact] = useState(false);
const [copied, setCopied] = useState(false);
const { setTimeoutSafe } = useSafeTimeout();

const widgetWidth = compact ? '400px' : '600px';
const widgetHeight = compact ? '450px' : '800px';
Expand Down Expand Up @@ -54,7 +56,7 @@ export default function WidgetEmbedCodePage() {
const code = generateEmbedCode();
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
setTimeoutSafe(() => setCopied(false), 2000);
};

return (
Expand Down
4 changes: 3 additions & 1 deletion src/components/ComparisonBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link';
import { Copy, ArrowRight, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useCompareStore } from '@/store/compareStore';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';

const MAX_COMPARE = 3;

Expand All @@ -15,6 +16,7 @@ export const ComparisonBar = () => {
const clearCompare = useCompareStore((state) => state.clearCompare);
const [shareUrl, setShareUrl] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const { setTimeoutSafe } = useSafeTimeout();

useEffect(() => {
if (typeof window === 'undefined') return;
Expand All @@ -28,7 +30,7 @@ export const ComparisonBar = () => {
try {
await navigator.clipboard.writeText(shareUrl);
setCopySuccess(true);
window.setTimeout(() => setCopySuccess(false), 2000);
setTimeoutSafe(() => setCopySuccess(false), 2000);
} catch {
setCopySuccess(false);
}
Expand Down
57 changes: 0 additions & 57 deletions src/components/TransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,63 +492,6 @@ export const TransactionHistory: React.FC = () => {
</Button>
</div>
)}
{/* Transaction Table */}
{isLoading ? (
<TableSkeleton rows={8} columns={6} showHeader={true} />
) : (
<div className="max-h-96 overflow-y-auto rounded-lg border border-border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Hash</TableHead>
<TableHead>Type</TableHead>
<TableHead>Status</TableHead>
<TableHead className="hidden md:table-cell">From</TableHead>
<TableHead className="hidden md:table-cell">To</TableHead>
<TableHead className="text-right">Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rowsToRender.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="p-0">
<EmptyState
title={searchTerm || typeFilter !== 'all' || statusFilter !== 'all'
? 'No transactions match your filters'
: 'No transactions found'}
description={searchTerm || typeFilter !== 'all' || statusFilter !== 'all'
? 'Try adjusting your search or filters to see more results.'
: 'Your transaction history will appear here once you start using the platform.'}
icon={History}
className="py-12"
/>
</TableCell>
</TableRow>
) : (
rowsToRender.map((tx) => (
<TableRow key={tx.id}>
<TableCell className="font-mono text-xs">
{tx.hash.slice(0, 10)}…{tx.hash.slice(-8)}
</TableCell>
<TableCell className="capitalize">{tx.type}</TableCell>
<TableCell className="capitalize">{tx.status}</TableCell>
<TableCell className="hidden md:table-cell font-mono text-xs">
{tx.from.slice(0, 10)}…{tx.from.slice(-8)}
</TableCell>
<TableCell className="hidden md:table-cell font-mono text-xs">
{tx.to ? `${tx.to.slice(0, 10)}…${tx.to.slice(-8)}` : '-'}
</TableCell>
<TableCell className="text-right text-xs text-muted-foreground">
{new Date(tx.timestamp).toLocaleString()}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)}

{/* Transaction Table */}
<div className="rounded-lg border border-border overflow-hidden">
<div className="overflow-x-auto">
Expand Down
6 changes: 4 additions & 2 deletions src/components/TransactionMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@
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',
confirmations: 1,
});
}, 5000);

return () => clearTimeout(timer);
return () => clearTimeoutSafe(timer);
}

return undefined;
Expand Down
18 changes: 6 additions & 12 deletions src/components/ViewToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import { logger } from '@/utils/logger';

/**
* UI-only view mode for listing screens.
Expand Down Expand Up @@ -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";
Expand All @@ -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);
Expand All @@ -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]);

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

Expand Down
18 changes: 3 additions & 15 deletions src/components/dashboard/DataRefreshWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,17 +22,7 @@ export const DataRefreshWrapper = ({
}: DataRefreshWrapperProps) => {
const [refreshState, setRefreshState] = useState<RefreshState>("idle");
const [error, setError] = useState<string | null>(null);
const successTimerRef = useRef<ReturnType<typeof setTimeout> | 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
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 3 additions & 1 deletion src/components/dashboard/PortfolioReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -185,7 +187,7 @@ export const PortfolioReport = () => {
setIsGenerating(false);
setIsGenerated(true);

setTimeout(() => setIsGenerated(false), 3000);
setTimeoutSafe(() => setIsGenerated(false), 3000);
};

return (
Expand Down
4 changes: 3 additions & 1 deletion src/components/dashboard/StakingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import React, { useState, useEffect } from 'react';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import {
X,
ShieldCheck,
Expand Down Expand Up @@ -57,6 +58,7 @@ export const StakingModal: React.FC<StakingModalProps> = ({
const [lockPeriod, setLockPeriod] = useState<number>(3); // months
const [isProcessing, setIsProcessing] = useState(false);
const [selectedToken, setSelectedToken] = useState(token || mockAvailableTokens[0]);
const { setTimeoutSafe } = useSafeTimeout();

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -89,7 +91,7 @@ export const StakingModal: React.FC<StakingModalProps> = ({
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
Expand Down
4 changes: 3 additions & 1 deletion src/components/kyc/KycVerificationCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = [
Expand Down Expand Up @@ -48,7 +50,7 @@ export function KycVerificationCenter() {

const handleLiveness = async () => {
startLivenessCheck();
window.setTimeout(() => {
setTimeoutSafe(() => {
completeLivenessCheck(true);
}, 1200);
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/referral/ClaimRewardsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
1: 'Ethereum',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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';
Expand Down
4 changes: 3 additions & 1 deletion src/components/referral/ReferralLinksCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,12 +26,13 @@ export default function ReferralLinksCard({
const [showCreateModal, setShowCreateModal] = useState(false);
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const [loadingShortUrl, setLoadingShortUrl] = useState<string | null>(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);
},
[]
);
Expand Down
Loading