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';