diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d3d24308..c86ed4f7 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -13,9 +13,13 @@ import { KycStatusBadge } from "@/components/kyc/KycStatusBadge"; import { useKycStore } from "@/store/kycStore"; import Link from "next/link"; import { TransactionSecuritySettings } from "@/components/security/TransactionSecuritySettings"; -import { StakingPanel } from "@/components/dashboard/StakingPanel"; import { Skeleton } from "@/components/ui/skeleton"; +const StakingPanel = dynamic( + () => import("@/components/dashboard/StakingPanel").then((m) => m.StakingPanel), + { loading: () => } +); + const PortfolioOverview = dynamic( () => import("@/components/dashboard/PortfolioOverview").then((m) => m.PortfolioOverview), { loading: () => } diff --git a/src/app/watchlist/page.tsx b/src/app/watchlist/page.tsx index 5c622fea..ecfae4d0 100644 --- a/src/app/watchlist/page.tsx +++ b/src/app/watchlist/page.tsx @@ -6,6 +6,7 @@ import { PropertyCard } from '@/components/PropertyCard'; import { useFavoritesStore } from '@/store/favoritesStore'; import { WalletConnector } from '@/components/WalletConnector'; import { Heart, ArrowLeft } from 'lucide-react'; +import { EmptyState } from '@/components/ui/EmptyState'; function WatchlistContent() { const { favorites, clearFavorites } = useFavoritesStore(); @@ -60,22 +61,15 @@ function WatchlistContent() { {/* Content */} {favorites.length === 0 ? ( - /* Empty State */ -
- -

- Your watchlist is empty -

-

- Start exploring properties and add them to your watchlist to keep track of the ones you're interested in. -

- - Browse Properties - -
+ ) : ( /* Properties Grid */ <> diff --git a/src/components/ClientProviders.tsx b/src/components/ClientProviders.tsx index 811b5928..49a18b0d 100644 --- a/src/components/ClientProviders.tsx +++ b/src/components/ClientProviders.tsx @@ -11,6 +11,10 @@ import { LoadingProgressBar } from "@/components/LoadingProgressBar"; import "@/lib/i18n"; import dynamic from "next/dynamic"; +import { useOnboardingStore } from "@/store/onboardingStore"; +import { DomainWarningBanner } from "@/components/DomainWarningBanner"; +import { useEffect } from "react"; + interface ClientProvidersProps { children: React.ReactNode; } @@ -35,31 +39,41 @@ const MobileBottomNavigation = dynamic( () => import("@/components/MobileBottomNavigation").then((m) => m.MobileBottomNavigation), { ssr: false } ); +const OnboardingTour = dynamic( + () => import("@/components/OnboardingTour").then((m) => m.OnboardingTour), + { ssr: false } +); export function ClientProviders({ children }: ClientProvidersProps) { + const { startOnboarding, hasCompletedOnboarding } = useOnboardingStore(); + + useEffect(() => { + // Automatically start onboarding for new users after a short delay + const timer = setTimeout(() => { + if (!hasCompletedOnboarding) { + startOnboarding(); + } + }, 2000); + + return () => clearTimeout(timer); + }, [hasCompletedOnboarding, startOnboarding]); + return ( - - - - {children} - - - - - - + {children} + + diff --git a/src/components/DomainWarningBanner.tsx b/src/components/DomainWarningBanner.tsx new file mode 100644 index 00000000..c88a669c --- /dev/null +++ b/src/components/DomainWarningBanner.tsx @@ -0,0 +1,129 @@ +"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); + + 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 (!warning.show || isDismissed) return null; + + const isPhishing = warning.type === 'phishing'; + + return ( +
+
+ + {isPhishing ? ( + + ) : ( + + )} +
+
+ + {isPhishing ? "Security Alert: Phishing Detected" : "Security Warning: Unofficial Domain"} + + + {warning.message} + +
+
+ + {!isPhishing && ( + + )} +
+
+ +
+
+
+ ); +}; diff --git a/src/components/MobileBottomNavigation.tsx b/src/components/MobileBottomNavigation.tsx index 6b453acc..9cebdc72 100644 --- a/src/components/MobileBottomNavigation.tsx +++ b/src/components/MobileBottomNavigation.tsx @@ -73,6 +73,7 @@ export const MobileBottomNavigation: React.FC = () => { { const { @@ -45,12 +46,19 @@ export const MultiChainPortfolio: React.FC = () => { if (!isConnected) { return ( -
-
- -

Connect your wallet to view your portfolio

-
-
+ { + // This would typically trigger the wallet connection modal + // but for now we just show the requirement + } + }} + className="bg-white dark:bg-gray-800 rounded-xl shadow-lg" + /> ); } @@ -65,18 +73,33 @@ export const MultiChainPortfolio: React.FC = () => { if (error || !portfolio) { return ( -
- -

- {error || 'Failed to load portfolio'} -

- -
+ + ); + } + + const totalHoldings = portfolio.chains.reduce((sum, chain) => sum + chain.holdings.length, 0); + + if (totalHoldings === 0) { + return ( + ); } diff --git a/src/components/NotificationCenter.tsx b/src/components/NotificationCenter.tsx index ea901dc9..ebba20b8 100644 --- a/src/components/NotificationCenter.tsx +++ b/src/components/NotificationCenter.tsx @@ -1,12 +1,11 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Separator } from '@/components/ui/separator'; import { PropertyAlert } from '@/types/property'; import { formatDistanceToNow } from 'date-fns'; import { @@ -19,6 +18,7 @@ import { AlertCircle, TrendingUp } from 'lucide-react'; +import { EmptyState } from '@/components/ui/EmptyState'; interface NotificationCenterProps { alerts: PropertyAlert[]; @@ -27,14 +27,14 @@ interface NotificationCenterProps { onClearAlert: (alertId: string) => void; } -export const NotificationCenter: React.FC = ({ +export const NotificationCenter = ({ alerts, onMarkAsRead, onMarkAllAsRead, onClearAlert, -}) => { +}: NotificationCenterProps) => { const [isOpen, setIsOpen] = useState(false); - const unreadCount = alerts.filter(alert => !alert.isRead).length; + const unreadCount = alerts.filter((alert: PropertyAlert) => !alert.isRead).length; const handleMarkAsRead = (alertId: string) => { onMarkAsRead(alertId); @@ -103,15 +103,12 @@ export const NotificationCenter: React.FC = ({ {alerts.length === 0 ? ( -
- -

- No alerts yet -

-

- Save property searches to receive notifications about new listings. -

-
+ ) : (
{alerts.map((alert) => ( diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx new file mode 100644 index 00000000..6fe34763 --- /dev/null +++ b/src/components/OnboardingTour.tsx @@ -0,0 +1,208 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useOnboardingStore } from '@/store/onboardingStore'; +import { Button } from '@/components/ui/button'; +import { X, ChevronRight, ChevronLeft, Building2, Wallet, Search, BarChart3, Info } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface Step { + id: string; + title: string; + description: string; + target?: string; + icon: React.ReactNode; +} + +const steps: Step[] = [ + { + id: 'welcome', + title: 'Welcome to PropChain', + description: 'Invest in real estate with the ease of crypto. Let us show you around!', + icon: , + }, + { + id: 'wallet', + title: 'Connect Your Wallet', + description: 'Connect your crypto wallet to start browsing and investing in properties.', + target: '[data-tour="wallet-connector"]', + icon: , + }, + { + id: 'browse', + title: 'Browse Properties', + description: 'Explore high-yield real estate opportunities across multiple chains.', + target: '[data-tour="browse-properties"]', + icon: , + }, + { + id: 'purchase', + title: 'Purchase Tokens', + description: 'Buy fractional tokens of real estate assets and start earning yield immediately.', + target: '[data-tour="purchase-form"]', + icon: , + }, + { + id: 'portfolio', + title: 'Track Your Portfolio', + description: 'Monitor your investments, earnings, and yield in one place.', + target: '[data-tour="portfolio-link"]', + icon: , + }, +]; + +export const OnboardingTour: React.FC = () => { + const { + isActive, + currentStep, + nextStep, + prevStep, + stopOnboarding, + completeOnboarding + } = useOnboardingStore(); + + const [targetRect, setTargetRect] = useState(null); + const portalRef = useRef(null); + + const step = steps[currentStep]; + + useEffect(() => { + if (!isActive || !step.target) { + setTargetRect(null); + return; + } + + const updateRect = () => { + const element = document.querySelector(step.target!); + if (element) { + setTargetRect(element.getBoundingClientRect()); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + setTargetRect(null); + } + }; + + updateRect(); + window.addEventListener('resize', updateRect); + window.addEventListener('scroll', updateRect); + + return () => { + window.removeEventListener('resize', updateRect); + window.removeEventListener('scroll', updateRect); + }; + }, [isActive, step]); + + if (!isActive) return null; + + const isLastStep = currentStep === steps.length - 1; + + return ( +
+ {/* Backdrop with hole */} + + {isActive && ( + + )} + + + {/* Tour Card */} +
+ window.innerHeight - 200 ? 'auto' : targetRect.bottom + 20, + bottom: targetRect.bottom + 20 > window.innerHeight - 200 ? window.innerHeight - targetRect.top + 20 : 'auto', + left: Math.max(20, Math.min(window.innerWidth - 380, targetRect.left + (targetRect.width / 2) - 192)), + } : {}} + > + + +
+
+ {step.icon} +
+
+

+ {step.title} +

+

+ Step {currentStep + 1} of {steps.length} +

+
+
+ +

+ {step.description} +

+ +
+ + +
+ {currentStep > 0 && ( + + )} + + +
+
+ + {/* Progress bar */} +
+ +
+
+
+
+ ); +}; diff --git a/src/components/PropertySearch.tsx b/src/components/PropertySearch.tsx index c2e5e4f3..01483d6c 100644 --- a/src/components/PropertySearch.tsx +++ b/src/components/PropertySearch.tsx @@ -14,11 +14,11 @@ interface PropertySearchProps { placeholder?: string; } -export const PropertySearch: React.FC = ({ +export const PropertySearch = ({ value, onChange, placeholder = 'Search properties, locations...', -}) => { +}: PropertySearchProps) => { const [isFocused, setIsFocused] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const [showHistory, setShowHistory] = useState(false); @@ -83,14 +83,14 @@ export const PropertySearch: React.FC = ({ switch (e.key) { case 'ArrowDown': e.preventDefault(); - setSelectedIndex(prev => + setSelectedIndex((prev: number) => prev < suggestions.length - 1 ? prev + 1 : prev ); break; case 'ArrowUp': e.preventDefault(); - setSelectedIndex(prev => (prev > 0 ? prev - 1 : -1)); + setSelectedIndex((prev: number) => (prev > 0 ? prev - 1 : -1)); break; case 'Enter': @@ -141,6 +141,7 @@ export const PropertySearch: React.FC = ({ onFocus={handleFocus} onKeyDown={handleKeyDown} placeholder={placeholder} + data-tour="browse-properties" className="w-full pl-12 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500" /> diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx index f950e79f..2e16a82f 100644 --- a/src/components/SearchResults.tsx +++ b/src/components/SearchResults.tsx @@ -8,6 +8,8 @@ import type { Property, ViewMode, SortOption, SearchFilters } from '@/types/prop import { SORT_LABELS } from '@/types/property'; import { Skeleton } from '@/components/ui/skeleton'; import { ComparisonBar } from './ComparisonBar'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { Search } from 'lucide-react'; interface SearchResultsProps { properties: Property[]; @@ -170,17 +172,11 @@ export const SearchResults: React.FC = ({ {/* Empty State */} {!isLoading && properties.length === 0 && ( -
- - - -

- No properties found -

-

- Try adjusting your filters or search criteria to find more properties. -

-
+ )} {/* Results Grid/List */} diff --git a/src/components/TransactionHistory.tsx b/src/components/TransactionHistory.tsx index ad5a20de..59beba0a 100644 --- a/src/components/TransactionHistory.tsx +++ b/src/components/TransactionHistory.tsx @@ -17,6 +17,8 @@ import * as XLSX from 'xlsx'; import { saveAs } from 'file-saver'; import { Skeleton } from '@/components/ui/skeleton'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { EmptyState } from '@/components/ui/EmptyState'; +import { History } from 'lucide-react'; const TRANSACTION_TYPES: TransactionType[] = ['purchase', 'transfer', 'management', 'other']; const TRANSACTION_STATUSES: TransactionStatus[] = ['pending', 'processing', 'confirmed', 'failed', 'cancelled']; @@ -312,10 +314,17 @@ export const TransactionHistory: React.FC = () => { )) ) : rowsToRender.length === 0 ? ( - - {searchTerm || typeFilter !== 'all' || statusFilter !== 'all' - ? 'No transactions match your filters' - : 'No transactions found'} + + ) : ( diff --git a/src/components/WalletConnector.tsx b/src/components/WalletConnector.tsx index d615895f..5898f86b 100644 --- a/src/components/WalletConnector.tsx +++ b/src/components/WalletConnector.tsx @@ -126,6 +126,7 @@ export const WalletConnector: React.FC = () => {
-
- {properties.map((property, index) => ( - - {/* Drag Handle */} -
handleDragStart(e, property)} - onDragEnd={handleDragEnd} + {properties.length === 0 ? ( + + ) : ( +
+ {properties.map((property, index) => ( + - -
+ {/* Drag Handle */} +
handleDragStart(e, property)} + onDragEnd={handleDragEnd} + > + +
- {/* Draggable Property Card */} -
handleDragStart(e, property)} - onDragOver={(e) => handleDragOver(e, index)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, index)} - onDragEnd={handleDragEnd} - onKeyDown={(e) => handleKeyDown(e, index)} - tabIndex={0} - role="button" - aria-label={`Property ${property.name}, press arrow keys to reorder`} - className="cursor-move focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-xl" - > - -
- - ))} -
+ {/* Draggable Property Card */} +
handleDragStart(e, property)} + onDragOver={(e) => handleDragOver(e, index)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, index)} + onDragEnd={handleDragEnd} + onKeyDown={(e) => handleKeyDown(e, index)} + tabIndex={0} + role="button" + aria-label={`Property ${property.name}, press arrow keys to reorder`} + className="cursor-move focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-xl" + > + +
+
+ ))} +
+ )} ); -}; +}); diff --git a/src/components/dashboard/PortfolioOverview.tsx b/src/components/dashboard/PortfolioOverview.tsx index 68c3718f..f2b68de4 100644 --- a/src/components/dashboard/PortfolioOverview.tsx +++ b/src/components/dashboard/PortfolioOverview.tsx @@ -8,11 +8,13 @@ import { Building2, DollarSign, Percent, + Briefcase, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useI18nFormatting } from "@/utils/i18nFormatting"; import { useWalletStore } from "@/store/walletStore"; import { usePortfolioOverview } from "@/hooks/usePortfolioQuery"; +import { EmptyState } from "@/components/ui/EmptyState"; interface MetricCardProps { title: string; @@ -176,6 +178,27 @@ export const PortfolioOverview = () => { const metrics = calculateMetrics(); + // If there's no portfolio or no properties, show an empty state for the whole overview + const totalProperties = portfolio?.chains.reduce( + (total, chain) => total + chain.holdings.length, + 0 + ) || 0; + + if (!portfolio || totalProperties === 0) { + return ( + + ); + } + return (
{metrics.map((metric, index) => ( diff --git a/src/components/dashboard/RentalIncomeDistribution.tsx b/src/components/dashboard/RentalIncomeDistribution.tsx index 95852d52..e4b097e8 100644 --- a/src/components/dashboard/RentalIncomeDistribution.tsx +++ b/src/components/dashboard/RentalIncomeDistribution.tsx @@ -6,10 +6,15 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { TrendingUp, Calendar, History } from "lucide-react"; +import dynamic from "next/dynamic"; import DistributionHistory from "./RentalIncomeDistribution/DistributionHistory"; import PendingDistributions from "./RentalIncomeDistribution/PendingDistributions"; import DistributionCalendar from "./RentalIncomeDistribution/DistributionCalendar"; -import CumulativeIncomeChart from "./RentalIncomeDistribution/CumulativeIncomeChart"; + +const CumulativeIncomeChart = dynamic( + () => import("./RentalIncomeDistribution/CumulativeIncomeChart"), + { loading: () =>
} +); export interface Distribution { id: string; diff --git a/src/components/forms/PurchaseTokenForm.tsx b/src/components/forms/PurchaseTokenForm.tsx index d2d67451..8a328365 100644 --- a/src/components/forms/PurchaseTokenForm.tsx +++ b/src/components/forms/PurchaseTokenForm.tsx @@ -35,7 +35,11 @@ export function PurchaseTokenForm({ propertyId, propertyName, onSubmit }: Purcha return (
- +

Purchase Tokens

Purchase tokens for {propertyName} with on-chain validation and an enforced approval step.

diff --git a/src/components/homepage/HeroSection.tsx b/src/components/homepage/HeroSection.tsx index dd32f45a..cdb7ef62 100644 --- a/src/components/homepage/HeroSection.tsx +++ b/src/components/homepage/HeroSection.tsx @@ -16,6 +16,7 @@ export function HeroSection() {

void; + href?: string; + }; + className?: string; +} + +export const EmptyState = ({ + title, + description, + icon: Icon, + action, + className, +}: EmptyStateProps) => { + return ( +
+
+ {/* Background Decorative Circles */} +
+
+ {Icon ? ( + + ) : ( +
+ )} +
+
+ +

+ {title} +

+

+ {description} +

+ + {action && ( + action.href ? ( + + + + ) : ( + + ) + )} +
+ ); +}; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index c1366bd1..e04abd79 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import * as RechartsPrimitive from "recharts" +import { ResponsiveContainer, Tooltip, Legend, type LegendProps } from "recharts" import { cn } from "@/lib/utils" @@ -43,7 +43,7 @@ function ChartContainer({ }: React.ComponentProps<"div"> & { config: ChartConfig children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer + typeof ResponsiveContainer >["children"] }) { const uniqueId = React.useId() @@ -61,9 +61,9 @@ function ChartContainer({ {...props} > - + {children} - +
) @@ -102,7 +102,7 @@ ${colorConfig ) } -const ChartTooltip = RechartsPrimitive.Tooltip +const ChartTooltip = Tooltip function ChartTooltipContent({ active, @@ -118,7 +118,7 @@ function ChartTooltipContent({ color, nameKey, labelKey, -}: React.ComponentProps & +}: React.ComponentProps & React.ComponentProps<"div"> & { hideLabel?: boolean hideIndicator?: boolean @@ -250,7 +250,7 @@ function ChartTooltipContent({ ) } -const ChartLegend = RechartsPrimitive.Legend +const ChartLegend = Legend function ChartLegendContent({ className, @@ -259,7 +259,7 @@ function ChartLegendContent({ verticalAlign = "bottom", nameKey, }: React.ComponentProps<"div"> & - Pick & { + Pick & { hideIcon?: boolean nameKey?: string }) { diff --git a/src/store/onboardingStore.ts b/src/store/onboardingStore.ts new file mode 100644 index 00000000..2913dcfc --- /dev/null +++ b/src/store/onboardingStore.ts @@ -0,0 +1,51 @@ +import { createBaseStore, type BaseState, type BaseActions } from './base'; + +export interface OnboardingState extends BaseState { + isActive: boolean; + currentStep: number; + hasCompletedOnboarding: boolean; +} + +export interface OnboardingActions extends BaseActions { + startOnboarding: () => void; + stopOnboarding: () => void; + nextStep: () => void; + prevStep: () => void; + completeOnboarding: () => void; + resetOnboarding: () => void; +} + +const initialState: OnboardingState = { + isActive: false, + currentStep: 0, + hasCompletedOnboarding: false, + isLoading: false, + error: null, + lastUpdated: null, +}; + +export const useOnboardingStore = createBaseStore( + initialState, + (set, get) => ({ + setLoading: (loading) => set({ isLoading: loading }), + setError: (error) => set({ error }), + clearError: () => set({ error: null }), + setLastUpdated: (timestamp) => set({ lastUpdated: timestamp }), + reset: () => set(initialState), + + startOnboarding: () => { + if (!get().hasCompletedOnboarding) { + set({ isActive: true, currentStep: 0 }); + } + }, + stopOnboarding: () => set({ isActive: false }), + nextStep: () => set((state: OnboardingState) => ({ currentStep: state.currentStep + 1 })), + prevStep: () => set((state: OnboardingState) => ({ currentStep: Math.max(0, state.currentStep - 1) })), + completeOnboarding: () => set({ isActive: false, hasCompletedOnboarding: true, currentStep: 0 }), + resetOnboarding: () => set({ hasCompletedOnboarding: false, currentStep: 0 }), + }), + { + persist: true, + name: 'propchain-onboarding', + } +); diff --git a/src/utils/security/phishingProtection.ts b/src/utils/security/phishingProtection.ts index 48f135cb..f3e90b96 100644 --- a/src/utils/security/phishingProtection.ts +++ b/src/utils/security/phishingProtection.ts @@ -34,6 +34,14 @@ export class PhishingProtection { // Add more suspicious method signatures ]; + private static readonly OFFICIAL_DOMAINS = [ + 'propchain.io', + 'localhost', + '127.0.0.1', + '0.0.0.0', + // Add more official domains here + ]; + /** * Detects phishing attempts based on domain and content analysis */ @@ -58,6 +66,12 @@ export class PhishingProtection { riskScore += 70; } + // Whitelist check: warn if not on an official domain + if (!this.isOfficialDomain(domain)) { + warnings.push('Unofficial domain detected'); + riskScore += 20; + } + // Pattern-based heuristics: URL shorteners, IP addresses, long random strings if (this.hasSuspiciousUrlPatterns(url)) { warnings.push('Suspicious URL patterns detected'); @@ -217,6 +231,36 @@ export class PhishingProtection { }; } + /** + * Reports a suspicious domain to the security team + */ + static async reportSuspiciousDomain(domain: string, reason: string): Promise { + try { + // In a real implementation, this would send data to a security API + console.warn(`[Security] Reporting suspicious domain: ${domain}. Reason: ${reason}`); + + // Placeholder for actual API call + // await fetch('https://api.propchain.io/security/report', { + // method: 'POST', + // body: JSON.stringify({ domain, reason, timestamp: Date.now() }) + // }); + + return true; + } catch (error) { + console.error('Failed to report suspicious domain:', error); + return false; + } + } + + /** + * Checks if a domain is an official PropChain domain + */ + private static isOfficialDomain(domain: string): boolean { + return this.OFFICIAL_DOMAINS.some(officialDomain => + domain === officialDomain || domain.endsWith(`.${officialDomain}`) + ); + } + /** * Checks if a domain is known for phishing */