Skip to content
Merged
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
3 changes: 0 additions & 3 deletions src/components/ViewToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function useViewMode() {
const stored = localStorage.getItem(STORAGE_KEY);
if (isValidViewMode(stored)) return stored;
} catch (err) {
// Swallow storage errors — fallback to default
logger.warn("useViewMode: localStorage unavailable, falling back to default view mode", err);
}

Expand All @@ -51,7 +50,6 @@ export function useViewMode() {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch (err) {
// Storage might be disabled; log and continue without throwing
logger.warn("useViewMode: failed to persist mode to localStorage", err);
}
}, [mode]);
Expand Down Expand Up @@ -80,7 +78,6 @@ export function ViewToggle({ mode, onChange }: ViewToggleProps) {
if (typeof onChange === "function") onChange(v);
else logger.warn("ViewToggle: onChange is not a function", onChange);
} catch (err) {
// Avoid bubbling UI errors — log instead
logger.error("ViewToggle: onChange handler threw an error", err);
}
};
Expand Down
8 changes: 3 additions & 5 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,7 +22,7 @@ export const DataRefreshWrapper = ({
}: DataRefreshWrapperProps) => {
const [refreshState, setRefreshState] = useState<RefreshState>("idle");
const [error, setError] = useState<string | null>(null);
const id = useId();
const { setTimeoutSafe } = useSafeTimeout();

const handleRefresh = useCallback(async () => {
// Cancel any pending success timeout from a previous refresh
Expand Down Expand Up @@ -52,10 +53,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
8 changes: 5 additions & 3 deletions src/components/responsive/LazyLoadingExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -210,14 +212,14 @@ export const ContentSkeletonExample: React.FC = () => {
export const PropertyListingExample: React.FC = () => {
const [properties, setProperties] = React.useState<any[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const loadingId = React.useId();
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' },
Expand Down
Loading