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:
@@ -81,11 +81,11 @@ class ApiClient {
|
|||||||
if (requiresAuth) {
|
if (requiresAuth) {
|
||||||
final token = await TokenStorage.getToken();
|
final token = await TokenStorage.getToken();
|
||||||
final username = await TokenStorage.getUsername();
|
final username = await TokenStorage.getUsername();
|
||||||
if (token == null || username == null) {
|
if (token != null && username != null) {
|
||||||
throw Exception('Authentication required');
|
finalParams['token'] = token;
|
||||||
|
finalParams['username'] = username;
|
||||||
}
|
}
|
||||||
finalParams['token'] = token;
|
// Guest mode: proceed without token — let backend decide
|
||||||
finalParams['username'] = username;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params != null) finalParams.addAll(params);
|
if (params != null) finalParams.addAll(params);
|
||||||
@@ -103,7 +103,7 @@ class ApiClient {
|
|||||||
return _handleResponse(url, response, finalParams);
|
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 {
|
Future<Map<String, dynamic>> _buildAuthBody(Map<String, dynamic>? body, bool requiresAuth) async {
|
||||||
final Map<String, dynamic> finalBody = {};
|
final Map<String, dynamic> finalBody = {};
|
||||||
|
|
||||||
@@ -111,12 +111,11 @@ class ApiClient {
|
|||||||
final token = await TokenStorage.getToken();
|
final token = await TokenStorage.getToken();
|
||||||
final username = await TokenStorage.getUsername();
|
final username = await TokenStorage.getUsername();
|
||||||
|
|
||||||
if (token == null || username == null) {
|
if (token != null && username != null) {
|
||||||
throw Exception('Authentication required');
|
finalBody['token'] = token;
|
||||||
|
finalBody['username'] = username;
|
||||||
}
|
}
|
||||||
|
// Guest mode: proceed without token — let backend decide
|
||||||
finalBody['token'] = token;
|
|
||||||
finalBody['username'] = username;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body != null) finalBody.addAll(body);
|
if (body != null) finalBody.addAll(body);
|
||||||
|
|||||||
71
lib/core/auth/auth_guard.dart
Normal file
71
lib/core/auth/auth_guard.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../../core/api/api_client.dart';
|
import '../../../core/api/api_client.dart';
|
||||||
import '../../../core/api/api_endpoints.dart';
|
import '../../../core/api/api_endpoints.dart';
|
||||||
|
import '../../../core/auth/auth_guard.dart';
|
||||||
import '../../../core/storage/token_storage.dart';
|
import '../../../core/storage/token_storage.dart';
|
||||||
import '../models/user_model.dart';
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ class AuthService {
|
|||||||
// candidate display name (server username or email fallback)
|
// candidate display name (server username or email fallback)
|
||||||
final displayCandidate = serverUsername ?? savedEmail;
|
final displayCandidate = serverUsername ?? savedEmail;
|
||||||
|
|
||||||
|
// clear guest mode on successful login
|
||||||
|
AuthGuard.setGuest(false);
|
||||||
|
|
||||||
// save token (TokenStorage stays responsible for token)
|
// save token (TokenStorage stays responsible for token)
|
||||||
await TokenStorage.saveToken(token.toString(), savedEmail);
|
await TokenStorage.saveToken(token.toString(), savedEmail);
|
||||||
|
|
||||||
@@ -90,6 +94,9 @@ class AuthService {
|
|||||||
final savedRole = (res['role'] ?? 'user').toString();
|
final savedRole = (res['role'] ?? 'user').toString();
|
||||||
final savedPhone = (res['phone_number'] ?? phoneNumber)?.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
|
// Save token + canonical user id for token storage
|
||||||
await TokenStorage.saveToken(token.toString(), savedEmail);
|
await TokenStorage.saveToken(token.toString(), savedEmail);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../features/auth/services/auth_service.dart';
|
import '../features/auth/services/auth_service.dart';
|
||||||
|
import '../core/auth/auth_guard.dart';
|
||||||
import 'home_desktop_screen.dart';
|
import 'home_desktop_screen.dart';
|
||||||
import '../core/app_decoration.dart';
|
import '../core/app_decoration.dart';
|
||||||
|
|
||||||
@@ -241,7 +242,17 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
|
|||||||
alignment: WrapAlignment.spaceBetween,
|
alignment: WrapAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")),
|
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'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../core/auth/auth_guard.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
import '../features/events/models/event_models.dart';
|
import '../features/events/models/event_models.dart';
|
||||||
@@ -448,7 +449,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
final active = _selectedIndex == index;
|
final active = _selectedIndex == index;
|
||||||
|
|
||||||
return GestureDetector(
|
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,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import '../features/auth/services/auth_service.dart';
|
import '../features/auth/services/auth_service.dart';
|
||||||
|
import '../core/auth/auth_guard.dart';
|
||||||
import 'home_screen.dart';
|
import 'home_screen.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
@@ -509,6 +510,30 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user