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
5 changes: 3 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { structuredLogger } from "@/utils/structuredLogger";
import { errorMonitoring } from "@/utils/errorMonitoringService";
import { ErrorCategory, ErrorSeverity } from "@/types/errors";
import { logger } from "@/utils/logger";
import { generateErrorId } from "@/utils/secureId";
import { WalletConnector } from "@/components/WalletConnector";
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
import {
Expand Down Expand Up @@ -48,7 +49,7 @@ function HomeContent() {
error.stack = event.error?.stack;

const appError = {
id: `error_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
id: generateErrorId(),
category: ErrorCategory.UI,
severity: ErrorSeverity.HIGH,
message: event.message,
Expand All @@ -71,7 +72,7 @@ function HomeContent() {
const error = new Error(event.reason?.message || 'Unhandled promise rejection');

const appError = {
id: `error_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
id: generateErrorId(),
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
message: error.message,
Expand Down
232 changes: 118 additions & 114 deletions src/components/DomainWarningBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,133 @@
"use client";

import React, { useEffect, useState } from 'react';
import { AlertTriangle, ShieldAlert, X } from 'lucide-react';
import { PhishingProtection } from '@/utils/security/phishingProtection';
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

export const DomainWarningBanner = () => {
const [warning, setWarning] = useState<{
show: boolean;
type: 'phishing' | 'unofficial';
message: string;
riskScore: number;
}>({
show: false,
type: 'unofficial',
message: '',
riskScore: 0,
});

const [isDismissed, setIsDismissed] = useState(false);
'use client';

import React, { useState, useEffect, useCallback } from 'react';
import { AlertTriangle, Shield, ExternalLink } from 'lucide-react';

const TRUSTED_DOMAINS = new Set([
'propchain.io',
'app.propchain.io',
'localhost',
'127.0.0.1',
]);

const OFFICIAL_URL = 'https://propchain.io';

interface DomainWarningBannerProps {
className?: string;
}

/**
* DomainWarningBanner
*
* Renders a non-navigating warning panel when the current host is not a
* trusted domain. The user must explicitly click "Proceed only if you trust
* this site" to dismiss the banner. Includes an aria-live region for screen
* reader announcements.
*
* NEVER auto-redirects. All navigation is gated behind explicit user consent.
*/
export const DomainWarningBanner: React.FC<DomainWarningBannerProps> = ({ className = '' }) => {
const [isSuspicious, setIsSuspicious] = useState(false);
const [dismissed, setDismissed] = useState(false);
const [hostname, setHostname] = useState('');
const [announcement, setAnnouncement] = useState('');

useEffect(() => {
const checkDomain = async () => {
if (typeof window === 'undefined') return;

const url = window.location.href;
const domain = window.location.hostname;
const result = PhishingProtection.detectPhishing(url);

if (result.isPhishing) {
setWarning({
show: true,
type: 'phishing',
message: 'This domain is flagged as a known phishing site. Your funds may be at risk.',
riskScore: result.riskScore,
});
// Auto-report phishing domains
PhishingProtection.reportSuspiciousDomain(domain, 'Known phishing domain');
} else if (result.warnings.includes('Unofficial domain detected')) {
setWarning({
show: true,
type: 'unofficial',
message: 'You are accessing PropChain from an unofficial domain. Please ensure you are on propchain.io.',
riskScore: result.riskScore,
});
// Report unofficial domains for investigation
PhishingProtection.reportSuspiciousDomain(domain, 'Unofficial domain');
}
};

checkDomain();
if (typeof window === 'undefined') return;

const host = window.location.hostname;
const isTrusted =
TRUSTED_DOMAINS.has(host) ||
host.endsWith('.propchain.io') ||
host.endsWith('.vercel.app') ||
host.endsWith('.netlify.app');

if (!isTrusted) {
setHostname(host);
setIsSuspicious(true);
setAnnouncement(
`Warning: You are viewing PropChain on an untrusted domain: ${host}. Please verify the site is legitimate before proceeding.`
);
}
}, []);

const handleDismiss = useCallback(() => {
setDismissed(true);
setAnnouncement('Domain warning dismissed by user. Proceeding on untrusted domain.');
}, []);

if (!warning.show || isDismissed) return null;
const handleGoToOfficial = useCallback(() => {
setAnnouncement('Navigating to official PropChain website.');
window.location.href = OFFICIAL_URL;
}, []);

const isPhishing = warning.type === 'phishing';
if (!isSuspicious || dismissed) return null;

return (
<div className={cn(
"fixed top-0 left-0 right-0 z-[100] p-4 animate-in fade-in slide-in-from-top-4 duration-500",
isPhishing ? "bg-red-600/10 backdrop-blur-md" : "bg-yellow-600/10 backdrop-blur-md"
)}>
<div className="max-w-5xl mx-auto">
<Alert variant={isPhishing ? "destructive" : "default"} className={cn(
"border-2 shadow-2xl",
isPhishing ? "border-red-500 bg-red-50 dark:bg-red-950/50" : "border-yellow-500 bg-yellow-50 dark:bg-yellow-950/50"
)}>
{isPhishing ? (
<ShieldAlert className="h-5 w-5 text-red-600 dark:text-red-400" />
) : (
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
)}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pr-8">
<div>
<AlertTitle className={cn(
"text-lg font-bold",
isPhishing ? "text-red-800 dark:text-red-200" : "text-yellow-800 dark:text-yellow-200"
)}>
{isPhishing ? "Security Alert: Phishing Detected" : "Security Warning: Unofficial Domain"}
</AlertTitle>
<AlertDescription className={cn(
"mt-1 font-medium",
isPhishing ? "text-red-700 dark:text-red-300" : "text-yellow-700 dark:text-yellow-300"
)}>
{warning.message}
</AlertDescription>
<>
{/* Screen-reader live region for announcements */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>

<div
className={`fixed bottom-0 left-0 right-0 z-[9999] ${className}`}
role="alert"
aria-label="Domain security warning"
>
<div className="border-t-2 border-amber-500 bg-amber-50 px-4 py-3 shadow-lg dark:border-amber-600 dark:bg-amber-950/95">
<div className="mx-auto flex max-w-5xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{/* Warning message */}
<div className="flex items-start gap-3">
<AlertTriangle
className="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600 dark:text-amber-400"
aria-hidden="true"
/>
<div>
<h2 className="text-sm font-semibold text-amber-900 dark:text-amber-100">
Untrusted Domain Detected
</h2>
<p className="mt-0.5 text-xs text-amber-800 dark:text-amber-200">
You are viewing PropChain on{' '}
<code className="rounded bg-amber-200 px-1 py-0.5 font-mono text-amber-900 dark:bg-amber-800 dark:text-amber-100">
{hostname}
</code>
. This is not an official PropChain domain. Phishing sites may
impersonate PropChain to steal your wallet credentials.
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={() => window.location.href = 'https://propchain.io'}
className={cn(
"font-bold transition-all hover:scale-105",
isPhishing
? "border-red-600 text-red-600 hover:bg-red-600 hover:text-white"
: "border-yellow-600 text-yellow-600 hover:bg-yellow-600 hover:text-white"
)}

{/* Action buttons */}
<div className="flex flex-shrink-0 gap-2">
<button
type="button"
onClick={handleGoToOfficial}
className="inline-flex items-center gap-1.5 rounded-lg border border-amber-300 bg-white px-3 py-2 text-xs font-medium text-amber-800 shadow-sm transition-colors hover:bg-amber-100 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-1 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-100 dark:hover:bg-amber-800"
>
<Shield className="h-3.5 w-3.5" />
Go to Official Site
</Button>
{!isPhishing && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsDismissed(true)}
className="text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200/50 dark:hover:bg-yellow-800/50"
>
Ignore
</Button>
)}
<ExternalLink className="h-3 w-3" />
</button>

<button
type="button"
onClick={handleDismiss}
className="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-red-600 px-3 py-2 text-xs font-medium text-white shadow-sm transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1 dark:border-red-700"
>
Proceed only if you trust this site
</button>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-2 h-8 w-8 opacity-70 hover:opacity-100"
onClick={() => setIsDismissed(true)}
>
<X className="h-4 w-4" />
</Button>
</Alert>
</div>
</div>
</div>
</>
);
};

export default DomainWarningBanner;
95 changes: 35 additions & 60 deletions src/components/TransactionProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { Progress } from '@/components/ui/progress';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Web3Tooltip } from '@/components/ui/Web3Tooltip';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';

interface TransactionStep {
id: string;
Expand Down Expand Up @@ -202,60 +208,30 @@ export const TransactionProgress: React.FC<TransactionProgressProps> = memo(({
if (!isOpen) return null;

return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
role="presentation"
>
<motion.div
ref={modalRef}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="bg-white dark:bg-gray-900 rounded-xl shadow-xl max-w-md w-full"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="transaction-progress-title"
aria-describedby="transaction-progress-description"
>
<Card className="border-0 shadow-none">
<CardContent className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3
id="transaction-progress-title"
className="text-lg font-semibold text-gray-900 dark:text-white"
>
Transaction in Progress
</h3>
{transactionHash && (
<p
id="transaction-progress-description"
className="text-sm text-gray-500 dark:text-gray-400 mt-1 font-mono"
>
Transaction hash: {transactionHash.slice(0, 10)}...{transactionHash.slice(-8)}
</p>
)}
</div>
<Button
ref={closeButtonRef}
variant="ghost"
size="sm"
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
aria-label="Close transaction progress"
>
<X className="w-5 h-5" aria-hidden="true" />
</Button>
</div>

{/* Overall Progress */}
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogContent className="sm:max-w-md" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>Transaction in Progress</span>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 -mr-2"
>
×
</Button>
</DialogTitle>
{transactionHash && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 font-mono">
{transactionHash.slice(0, 10)}...{transactionHash.slice(-8)}
</p>
)}
</DialogHeader>

<Card className="border-0 shadow-none">
<CardContent className="px-0 py-2">
{/* Overall Progress */}
<div className="mb-6">
<div className="flex justify-between text-sm mb-2">
<span className="text-gray-600 dark:text-gray-400">Overall Progress</span>
Expand Down Expand Up @@ -372,13 +348,12 @@ export const TransactionProgress: React.FC<TransactionProgressProps> = memo(({
{steps[steps.length - 1].status === 'completed' ? 'Close' : 'Processing...'}
</Button>
</div>
</CardContent>
</Card>
</motion.div>
</motion.div>
</AnimatePresence>
</CardContent>
</Card>
</DialogContent>
</Dialog>
);
};
});

// Hook to use transaction progress
export const useTransactionProgress = () => {
Expand Down
Loading