feat: add guest mode — browse events without login

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).
This commit is contained in:
2026-03-20 22:40:50 +05:30
parent cf21e0a58c
commit cac2671fd6
6 changed files with 130 additions and 12 deletions

View File

@@ -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<Map<String, dynamic>> _buildAuthBody(Map<String, dynamic>? body, bool requiresAuth) async {
final Map<String, dynamic> 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);

View File

@@ -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),
],
),
),
);
}
}