From cac2671fd639ea4770d7da0a781c00548c32d39b Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Fri, 20 Mar 2026 22:40:50 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20add=20guest=20mode=20=E2=80=94=20browse?= =?UTF-8?q?=20events=20without=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New file: lib/core/auth/auth_guard.dart Static AuthGuard class with isGuest flag and requireLogin() helper that shows a login prompt bottom sheet when guests try protected actions. login_screen.dart / desktop_login_screen.dart: Added "Continue as Guest" button below sign-up link. Sets AuthGuard.isGuest = true, then navigates to HomeScreen. api_client.dart: _buildAuthBody() and GET auth check no longer throw when token is missing. If no token (guest), request proceeds without auth — backend decides. home_screen.dart: Bottom nav guards: tapping Contribute (index 2) or Profile (index 3) as guest shows login prompt instead of navigating. auth_service.dart: AuthGuard.setGuest(false) called on successful login AND register so guest flag is always cleared when user authenticates. Guest CAN: browse home, calendar, search, filter, view event details. Guest CANNOT: contribute, view profile, book events (prompts login). --- lib/core/api/api_client.dart | 19 +++--- lib/core/auth/auth_guard.dart | 71 ++++++++++++++++++++ lib/features/auth/services/auth_service.dart | 7 ++ lib/screens/desktop_login_screen.dart | 13 +++- lib/screens/home_screen.dart | 7 +- lib/screens/login_screen.dart | 25 +++++++ 6 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 lib/core/auth/auth_guard.dart diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index aa5ed9f..7b70f97 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -81,11 +81,11 @@ class ApiClient { if (requiresAuth) { final token = await TokenStorage.getToken(); final username = await TokenStorage.getUsername(); - if (token == null || username == null) { - throw Exception('Authentication required'); + if (token != null && username != null) { + finalParams['token'] = token; + finalParams['username'] = username; } - finalParams['token'] = token; - finalParams['username'] = username; + // Guest mode: proceed without token — let backend decide } if (params != null) finalParams.addAll(params); @@ -103,7 +103,7 @@ class ApiClient { return _handleResponse(url, response, finalParams); } - /// Build request body and attach token + username if required + /// Build request body and attach token + username if available Future> _buildAuthBody(Map? body, bool requiresAuth) async { final Map finalBody = {}; @@ -111,12 +111,11 @@ class ApiClient { final token = await TokenStorage.getToken(); final username = await TokenStorage.getUsername(); - if (token == null || username == null) { - throw Exception('Authentication required'); + if (token != null && username != null) { + finalBody['token'] = token; + finalBody['username'] = username; } - - finalBody['token'] = token; - finalBody['username'] = username; + // Guest mode: proceed without token — let backend decide } if (body != null) finalBody.addAll(body); diff --git a/lib/core/auth/auth_guard.dart b/lib/core/auth/auth_guard.dart new file mode 100644 index 0000000..cb3bd0a --- /dev/null +++ b/lib/core/auth/auth_guard.dart @@ -0,0 +1,71 @@ +// lib/core/auth/auth_guard.dart +import 'package:flutter/material.dart'; +import '../../screens/login_screen.dart'; + +class AuthGuard { + static bool _isGuest = false; + + static bool get isGuest => _isGuest; + static bool get isLoggedIn => !_isGuest; + + static void setGuest(bool value) => _isGuest = value; + + /// Call before any action that requires login. + /// Returns true if logged in (proceed). Returns false if guest (shows prompt). + static bool requireLogin(BuildContext context, + {String reason = 'This feature requires an account.'}) { + if (!_isGuest) return true; + _showLoginPrompt(context, reason); + return false; + } + + static void _showLoginPrompt(BuildContext context, String reason) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) => Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.lock_outline, size: 48, color: Color(0xFF0B63D6)), + const SizedBox(height: 16), + Text(reason, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + const Text('Sign in or create an account to continue.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0B63D6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + onPressed: () { + Navigator.of(ctx).pop(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const LoginScreen()), + (route) => false, + ); + }, + child: const Text('Sign In', + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.w700)), + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ); + } +} diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index b06c464..30c8727 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/api/api_client.dart'; import '../../../core/api/api_endpoints.dart'; +import '../../../core/auth/auth_guard.dart'; import '../../../core/storage/token_storage.dart'; import '../models/user_model.dart'; @@ -33,6 +34,9 @@ class AuthService { // candidate display name (server username or email fallback) final displayCandidate = serverUsername ?? savedEmail; + // clear guest mode on successful login + AuthGuard.setGuest(false); + // save token (TokenStorage stays responsible for token) await TokenStorage.saveToken(token.toString(), savedEmail); @@ -90,6 +94,9 @@ class AuthService { final savedRole = (res['role'] ?? 'user').toString(); final savedPhone = (res['phone_number'] ?? phoneNumber)?.toString(); + // clear guest mode on successful registration + AuthGuard.setGuest(false); + // Save token + canonical user id for token storage await TokenStorage.saveToken(token.toString(), savedEmail); diff --git a/lib/screens/desktop_login_screen.dart b/lib/screens/desktop_login_screen.dart index d050d0a..1fac1f0 100644 --- a/lib/screens/desktop_login_screen.dart +++ b/lib/screens/desktop_login_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../features/auth/services/auth_service.dart'; +import '../core/auth/auth_guard.dart'; import 'home_desktop_screen.dart'; import '../core/app_decoration.dart'; @@ -241,7 +242,17 @@ class _DesktopLoginScreenState extends State with SingleTick alignment: WrapAlignment.spaceBetween, children: [ TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")), - TextButton(onPressed: () {}, child: const Text('Contact support')) + TextButton(onPressed: () {}, child: const Text('Contact support')), + TextButton( + onPressed: () { + AuthGuard.setGuest(true); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true)), + (route) => false, + ); + }, + child: const Text('Continue as Guest'), + ), ], ) ], diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 13264c0..dd16dca 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../core/auth/auth_guard.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../features/events/models/event_models.dart'; @@ -448,7 +449,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM final active = _selectedIndex == index; return GestureDetector( - onTap: () => setState(() => _selectedIndex = index), + onTap: () { + if (index == 2 && !AuthGuard.requireLogin(context, reason: 'Sign in to contribute events and earn rewards.')) return; + if (index == 3 && !AuthGuard.requireLogin(context, reason: 'Sign in to view your profile.')) return; + setState(() => _selectedIndex = index); + }, behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index d225dc4..9eb8941 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../features/auth/services/auth_service.dart'; +import '../core/auth/auth_guard.dart'; import 'home_screen.dart'; class LoginScreen extends StatefulWidget { @@ -509,6 +510,30 @@ class _LoginScreenState extends State { ], ), ), + + const SizedBox(height: 16), + + // Continue as Guest + Center( + child: GestureDetector( + onTap: () { + AuthGuard.setGuest(true); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeScreen()), + (route) => false, + ); + }, + child: const Text( + 'Continue as Guest', + style: TextStyle( + color: _textMuted, + fontSize: 13, + decoration: TextDecoration.underline, + decorationColor: _textMuted, + ), + ), + ), + ), ], ), ),