diff --git a/firestore.rules b/firestore.rules index 3aca3c7..3ce557d 100644 --- a/firestore.rules +++ b/firestore.rules @@ -108,6 +108,8 @@ service cloud.firestore { // Locks referralPoints to the actual totalEarned value in the referrals index. allow update: if isAuthenticated() && request.auth.uid != uid + // 🛡️ NEW GUARD (Issue #432): Only users actively onboarding can trigger a referral point grant + && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.onboardingStatus == "incomplete" // Prevent modifying any root-level fields other than points && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points']) // FIX: Sync referralPoints directly with the referrals index document's totalEarned counter @@ -120,7 +122,6 @@ service cloud.firestore { && request.resource.data.points.auditorPoints == resource.data.points.auditorPoints && exists(/databases/$(database)/documents/referrals/$(uid)) && request.auth.uid in get(/databases/$(database)/documents/referrals/$(uid)).data.usedBy; - } match /referrals/{uid} { allow read: if isAuthenticated(); @@ -131,6 +132,8 @@ service cloud.firestore { // Allow either the owner to write, OR a referred user to atomically append their UID allow update: if isOwner(uid) || ( isAuthenticated() + // 🛡️ NEW GUARD (Issue #432): Prevent existing users from arbitrarily claiming codes + && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.onboardingStatus == "incomplete" && !(request.auth.uid in resource.data.usedBy) && request.resource.data.usedBy.size() == resource.data.usedBy.size() + 1 && request.resource.data.usedBy[resource.data.usedBy.size()] == request.auth.uid diff --git a/src/components/ui/ErrorBoundary.jsx b/src/components/ui/ErrorBoundary.jsx index e286456..0f9a2f9 100644 --- a/src/components/ui/ErrorBoundary.jsx +++ b/src/components/ui/ErrorBoundary.jsx @@ -1,9 +1,10 @@ import React, { Component } from "react"; +import { motion } from "framer-motion"; export class ErrorBoundary extends Component { constructor(props) { super(props); - this.state = { hasError: false, error: null }; + this.state = { hasError: false, error: null, errorInfo: null }; } static getDerivedStateFromError(error) { @@ -12,13 +13,30 @@ export class ErrorBoundary extends Component { componentDidCatch(error, errorInfo) { console.error("ErrorBoundary caught an error:", error, errorInfo); + this.setState({ errorInfo }); } + // ADDED: Graceful recovery method (No full page reload) + handleRetry = () => { + this.setState({ hasError: false, error: null, errorInfo: null }); + }; + render() { if (this.state.hasError) { + // ADDED: Dynamic context-specific message + const customMessage = this.props.fallbackMessage || "The application encountered an unexpected error. Don't worry, your data is safe."; + return ( -
-
+ // Changed min-h-screen to min-h-[60vh] so it fits perfectly inside dashboard layouts too +
+ + {/* ADDED: Framer motion wrapper for entry animation */} + {/* Premium CSS-Animated SVG warning graphic */}
{/* Glowing backdrop rings */} @@ -33,19 +51,31 @@ export class ErrorBoundary extends Component {
+

Something went wrong.

+

- The application encountered an unexpected error. Don't worry, your data is safe. + {customMessage}

+ + {/* ADDED: Hidden technical details for developers (only shows in localhost/dev mode) */} + {process.env.NODE_ENV === 'development' && this.state.error && ( +
+

+ {this.state.error.toString()} +

+
+ )} + -
+
); } @@ -54,4 +84,4 @@ export class ErrorBoundary extends Component { } } -export default ErrorBoundary; +export default ErrorBoundary; \ No newline at end of file diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx index a6fb109..fd573c9 100644 --- a/src/routes/AppRoutes.jsx +++ b/src/routes/AppRoutes.jsx @@ -1,35 +1,39 @@ -import React from "react"; +import React, { Suspense } from "react"; import { Routes, Route, Navigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import PublicLayout from "../layouts/PublicLayout"; import DashboardLayout from "../layouts/DashboardLayout"; -import Home from "../pages/Home"; -import Dashboard from "../pages/Dashboard"; -import GitRank from "../pages/GitRank"; -import RankHer from "../pages/RankHer"; -import CodingVerse from "../pages/CodingVerse"; -import CodingOwl from "../pages/CodingOwl"; -import Matchmaker from "../pages/Matchmaker"; -import Profile from "../pages/Profile"; -import Friends from "../pages/Friends"; -import Login from "../pages/Login"; -import Onboarding from "../pages/Onboarding"; -import NotFound from "../pages/NotFound"; -import Achievements from "../pages/Achievements"; -import About from "../pages/About"; -import Terms from "../pages/Terms"; -import Privacy from "../pages/Privacy"; import ComingSoonCard from "../components/ui/ComingSoonCard"; import GlobalModals from "../components/ui/GlobalModals"; import { Settings as SettingsIcon } from "lucide-react"; -import Auditor from "../pages/Auditor"; +import ErrorBoundary from "../components/ui/ErrorBoundary"; + +// Lazy Loaded Pages to reduce initial JS bundle +const Home = React.lazy(() => import("../pages/Home")); +const Dashboard = React.lazy(() => import("../pages/Dashboard")); +const GitRank = React.lazy(() => import("../pages/GitRank")); +const RankHer = React.lazy(() => import("../pages/RankHer")); +const CodingVerse = React.lazy(() => import("../pages/CodingVerse")); +const CodingOwl = React.lazy(() => import("../pages/CodingOwl")); +const Matchmaker = React.lazy(() => import("../pages/Matchmaker")); +const Profile = React.lazy(() => import("../pages/Profile")); +const Friends = React.lazy(() => import("../pages/Friends")); +const Login = React.lazy(() => import("../pages/Login")); +const Onboarding = React.lazy(() => import("../pages/Onboarding")); +const NotFound = React.lazy(() => import("../pages/NotFound")); +const Achievements = React.lazy(() => import("../pages/Achievements")); +const About = React.lazy(() => import("../pages/About")); +const Terms = React.lazy(() => import("../pages/Terms")); +const Privacy = React.lazy(() => import("../pages/Privacy")); +const Auditor = React.lazy(() => import("../pages/Auditor")); +const CardBuilder = React.lazy(() => import("../pages/CardBuilder")); // Inline loading indicator const LoadingScreen = ({ message }) => (
- {message || "Syncing Session..."} + {message || "Loading..."}
); @@ -65,8 +69,6 @@ const OnboardingRoute = ({ children }) => { return ; } - // Strict guard: if the user's data explicitly says they are complete, OR if isOnboarding is false, redirect. - // We only allow access if they are explicitly incomplete. if (userData?.onboardingStatus === "complete" || !isOnboarding || (userData && userData.onboardingStatus !== "incomplete")) { return ; } @@ -79,7 +81,7 @@ const GuestRoute = ({ children }) => { const { user, loading, isOnboarding } = useAuth(); if (loading) { - return null; // Don't redirect prematurely while state is resolving + return null; } if (user) { @@ -92,9 +94,6 @@ const GuestRoute = ({ children }) => { return children; }; -import CardBuilder from "../pages/CardBuilder"; - -// An inline settings page to keep route integrated const SettingsPage = () => (
(
); +// Helper wrapper to easily wrap lazy components with ErrorBoundary +const withErrorBoundary = (Component, componentName, fallbackMessage) => ( + + + +); + export const AppRoutes = () => { return ( <> - - {/* Public Site Layout & Pages */} - }> - } /> - } /> - } /> - } /> - } /> - - - {/* Standalone About Us page */} - } /> - - {/* Standalone Legal pages */} - } /> - } /> - - {/* Public Login page (standalone) - guarded from logged in users */} - } /> - - {/* Onboarding page (standalone) - guarded so only incomplete profiles see it */} - } /> - - {/* Layout dashboard sub-pages - locked to authenticated & fully onboarded users */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* 404 Catch All */} - } /> - + }> + + {/* Public Site Layout & Pages */} + }> + + + + + + + + {/* Standalone About Us page */} + + + {/* Standalone Legal pages */} + + + + {/* Public Login page */} + {withErrorBoundary(Login, "Login", "Failed to initialize the login portal.")}} /> + + {/* Onboarding page */} + {withErrorBoundary(Onboarding, "Onboarding", "Failed to load the onboarding flow.")}} /> + + {/* Layout dashboard sub-pages */} + }> + + + + + + + + + + + + + + + } /> + + + + {/* 404 Catch All */} + } /> + + ); }; -export default AppRoutes; +export default AppRoutes; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 1c09993..e2c14a7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -36,6 +36,11 @@ export default defineConfig({ if (id.includes('framer-motion')) { return 'vendor-motion'; } + + // Swiper (Carousel) + if (id.includes('swiper')) { + return 'vendor-swiper'; + } // All other node_modules return 'vendor';