diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 405e9d2..624ef3f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import Header from './components/Header'; import SkipNav from './components/SkipNav'; import RouteSkeleton from './components/RouteSkeleton'; import RequireAdmin from './components/RequireAdmin'; +import RequireAuth from './components/RequireAuth'; import LazyErrorBoundary from './components/LazyErrorBoundary'; import OfflineBanner from './components/OfflineBanner'; import { ToastContainer, useToast } from './components/ui/toast'; @@ -17,7 +18,7 @@ import { ROUTE_SEND, ROUTE_BATCH, ROUTE_TOKEN_TIP, ROUTE_FEED, ROUTE_LEADERBOARD, ROUTE_ACTIVITY, ROUTE_PROFILE, ROUTE_BLOCK, ROUTE_STATS, ROUTE_ADMIN, ROUTE_TELEMETRY, - DEFAULT_AUTHENTICATED_ROUTE, + DEFAULT_AUTHENTICATED_ROUTE, ROUTE_META, } from './config/routes'; import { Zap, Radio, Trophy, User, BarChart3, Users, ShieldBan, Coins, UserCircle, Shield, Gauge } from 'lucide-react'; @@ -99,7 +100,7 @@ function App() { }; const navItems = useMemo(() => { - const items = [ + const allItems = [ { path: ROUTE_SEND, label: 'Send Tip', icon: Zap }, { path: ROUTE_BATCH, label: 'Batch', icon: Users }, { path: ROUTE_TOKEN_TIP, label: 'Token Tip', icon: Coins }, @@ -110,12 +111,23 @@ function App() { { path: ROUTE_BLOCK, label: 'Block', icon: ShieldBan }, { path: ROUTE_STATS, label: 'Stats', icon: BarChart3 }, ]; + + // Filter items based on auth and admin status + const items = allItems.filter((item) => { + const meta = ROUTE_META[item.path]; + // Show authenticated routes only if user is authenticated + if (meta.requiresAuth && !userData) return false; + // Show admin routes only if user is owner + if (meta.adminOnly && !isOwner) return false; + return true; + }); + if (isOwner) { items.push({ path: ROUTE_ADMIN, label: 'Admin', icon: Shield }); items.push({ path: ROUTE_TELEMETRY, label: 'Telemetry', icon: Gauge }); } return items; - }, [isOwner]); + }, [userData, isOwner]); if (healthy === false) { return ( @@ -148,63 +160,141 @@ function App() { />
- {userData ? ( + {/* Show landing hero only if user has not connected AND is on home route */} + {!userData && location.pathname === '/' ? ( + }> + + + ) : (
- {/* Navigation */} - + + )} {/* Page content */} }> - } /> - } /> - } /> + {/* Authenticated-only routes */} + + ) : ( + + + + ) + } + /> + + ) : ( + + + + ) + } + /> + + ) : ( + + + + ) + } + /> + + {/* Public routes - accessible to all */} } /> } /> - } /> - } /> - } /> } /> + + {/* User-specific routes */} + + ) : ( + + + + ) + } + /> + + ) : ( + + + + ) + } + /> + + ) : ( + + + + ) + } + /> + + {/* Admin-only routes */} } /> } /> - } /> + + {/* Root and fallback */} + } /> } />
- ) : ( - }> - - )}
diff --git a/frontend/src/components/RequireAuth.jsx b/frontend/src/components/RequireAuth.jsx new file mode 100644 index 0000000..fc21e6c --- /dev/null +++ b/frontend/src/components/RequireAuth.jsx @@ -0,0 +1,44 @@ +/** + * RequireAuth -- Guards a route to require authentication. + * + * If the user is not authenticated, displays an inline prompt + * with a button to connect their wallet. + * + * @module components/RequireAuth + */ +import { Link } from 'react-router-dom'; + +export default function RequireAuth({ children, onAuth, authLoading, route }) { + return ( +
+ {children} + +
+
+

+ Wallet connection required +

+

+ This feature requires you to connect your Stacks wallet to send tips, + manage your profile, or access personalized features. +

+ +
+
+ +
+

+ or + explore the platform + without connecting first. +

+
+
+ ); +} diff --git a/frontend/src/config/routes.js b/frontend/src/config/routes.js index de179e0..2f888f5 100644 --- a/frontend/src/config/routes.js +++ b/frontend/src/config/routes.js @@ -167,12 +167,12 @@ export const ROUTE_META = { }, [ROUTE_FEED]: { description: 'Real-time feed of tips across the platform.', - requiresAuth: true, + requiresAuth: false, adminOnly: false, }, [ROUTE_LEADERBOARD]: { description: 'Top tippers and recipients ranked by volume.', - requiresAuth: true, + requiresAuth: false, adminOnly: false, }, [ROUTE_ACTIVITY]: { @@ -192,7 +192,7 @@ export const ROUTE_META = { }, [ROUTE_STATS]: { description: 'Platform-wide aggregate statistics.', - requiresAuth: true, + requiresAuth: false, adminOnly: false, }, [ROUTE_ADMIN]: { diff --git a/frontend/src/test/routes.test.js b/frontend/src/test/routes.test.js index d453f78..a143bc8 100644 --- a/frontend/src/test/routes.test.js +++ b/frontend/src/test/routes.test.js @@ -178,8 +178,16 @@ describe('ROUTE_META', () => { } }); - it('all navigable routes require authentication', () => { - for (const route of NAVIGABLE_ROUTES) { + it('public routes do not require authentication', () => { + const publicRoutes = [ROUTE_FEED, ROUTE_LEADERBOARD, ROUTE_STATS]; + for (const route of publicRoutes) { + expect(ROUTE_META[route].requiresAuth).toBe(false); + } + }); + + it('authenticated routes require authentication', () => { + const authRequiredRoutes = [ROUTE_SEND, ROUTE_BATCH, ROUTE_TOKEN_TIP, ROUTE_ACTIVITY, ROUTE_PROFILE, ROUTE_BLOCK]; + for (const route of authRequiredRoutes) { expect(ROUTE_META[route].requiresAuth).toBe(true); } });