Description:
I’m working on a Flutter application where I have an AuthService
class for handling user authentication and a LogInScreen
for the login UI. When the login fails, I’m trying to display an error message on the LogInScreen
. However, I keep encountering a generic “Unknown error” message instead of the actual error message from the server.
Here’s my AuthService
and LogInScreen
code:
AuthService:
import 'dart:async';
import 'dart:convert';
import 'package:demo/core/constants/constants.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class AuthService extends ChangeNotifier {
Timer? _authTimer;
DateTime? _expiryDate;
String? _token;
String? get token => _token ?? '';
Future<void> login(
String username,
String password, {
required bool rememberMe,
}) async {
final url = Uri.parse('${AppConstants.baseURL}/auth/login');
final response = await http.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'username': username,
'password': password,
}),
);
if (response.statusCode != 200) {
final responseData = jsonDecode(response.body);
final errorMessage = responseData['message'] ?? 'Unknown error';
throw Exception(errorMessage);
}
final responseData = jsonDecode(response.body);
_token = responseData['token'];
final expiresIn = responseData['expires_in'];
_expiryDate = expiresIn != null && expiresIn is int
? DateTime.now().add(Duration(seconds: expiresIn))
: DateTime.now().add(const Duration(hours: 4));
await _persistToken(_token!, _expiryDate!);
}
Future<void> refreshToken() async {
final url = Uri.parse('${AppConstants.baseURL}/auth/refresh');
final response = await http.post(
url,
headers: {
'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to refresh token: ${response.reasonPhrase}');
}
final responseData = jsonDecode(response.body);
_token = responseData['token'];
final expiresIn = responseData['expires_in'];
_expiryDate = DateTime.now().add(Duration(seconds: expiresIn));
await _persistToken(_token!, _expiryDate!);
}
Future<bool> isLoggedIn() async {
await _loadPersistedToken();
return _token != null;
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
await prefs.remove('expiryDate');
_token = null;
_expiryDate = null;
_authTimer?.cancel();
_authTimer = null;
notifyListeners();
}
Future<void> _persistToken(String token, DateTime expiryDate) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token);
await prefs.setString('expiryDate', expiryDate.toIso8601String());
_token = token;
_expiryDate = expiryDate;
_autoLogout();
notifyListeners();
}
Future<void> _loadPersistedToken() async {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
final expiryDateString = prefs.getString('expiryDate');
if (expiryDateString == null) {
notifyListeners();
return;
}
_expiryDate = DateTime.parse(expiryDateString);
if (_expiryDate!.isBefore(DateTime.now())) {
await refreshToken();
}
notifyListeners();
}
void _autoLogout() {
_authTimer?.cancel();
if (_expiryDate == null) return;
final timeToExpiry = _expiryDate!.difference(DateTime.now()).inSeconds;
_authTimer = Timer(Duration(seconds: timeToExpiry), logout);
}
}
LogInScreen:
import 'package:demo/core/constants/app_colors.dart';
import 'package:demo/core/constants/constants.dart';
import 'package:demo/ui/screens/applicants/views/applicant_form_screen.dart';
import 'package:demo/ui/widgets/re_usable_widgets/re_usable_widgets.dart';
import 'package:demo/ui/widgets/ui_widgets/custom_app_bar.dart';
import 'package:demo/ui/widgets/ui_widgets/custom_clipper.dart';
import 'package:demo/ui/widgets/ui_widgets/error_message.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/password_visibility_provider.dart';
class LogInScreen extends ConsumerStatefulWidget {
const LogInScreen({super.key});
@override
_LogInScreenState createState() => _LogInScreenState();
}
class _LogInScreenState extends ConsumerState<LogInScreen> {
String? _errorMessage;
final _formKey = GlobalKey<FormState>();
bool _isLoading = true;
late TextEditingController _passwordController;
bool _rememberMe = false;
late TextEditingController _userNameController;
@override
void dispose() {
_userNameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_userNameController = TextEditingController();
_passwordController = TextEditingController();
_checkLoginStatus();
}
Future<void> _checkLoginStatus() async {
final isLoggedIn = await ref.read(authProvider).isLoggedIn();
if (!isLoggedIn) {
setState(() {
_isLoading = false;
});
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ApplicantFormScreen()),
);
}
}
Future<void> _onSubmit() async {
setState(() {
_errorMessage = null;
});
if (!_formKey.currentState!.validate()) return;
final name = _userNameController.text;
final password = _passwordController.text;
try {
await ref
.read(authProvider)
.login(name, password, rememberMe: _rememberMe);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ApplicantFormScreen()),
);
} catch (e) {
setState(() {
_errorMessage = e.toString();
});
}
}
SizedBox _buildLoginButton() {
return SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 150.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20),
backgroundColor: AppColors.kBlueColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
),
onPressed: _onSubmit,
child: const Text(
'LOGIN',
style: TextStyle(
fontSize: 16.0,
color: AppColors.kwhite,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
void _handleErrorMessageClose() {
setState(() {
_errorMessage = null;
});
}
@override
Widget build(BuildContext context) {
final obscureText = ref.watch(obscureTextProvider);
final Size size = MediaQuery.of(context).size;
return SafeArea(
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Row(
children: [
Expanded(
child: Container(
color: AppColors.kwhite,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: size.height * 0.250,
child: Image.asset(
AppConstants.solidCheckLogo,
fit: BoxFit.contain,
),
),
],
),
),
),
),
Expanded(
child: ClipPath(
clipper: CustomClipPath(),
child: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF014F8C),
Color(0xFF0EDCF8),
],
),
),
child: SizedBox(
child: Center(
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.only(left: 50.0),
child: Container(
height: size.height * 0.70,
width: size.width * 0.34,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(10.0),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 60.0,
),
child: ReusableWidgets.buildTextField(
controller: _userNameController,
fieldTitle: 'Username',
hintText: 'Enter your username',
context: context,
),
),
const SizedBox(height: 20.0),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 60.0,
),
child: ReusableWidgets.buildTextField(
controller: _passwordController,
fieldTitle: 'Password',
hintText: 'Enter your password',
isPassword: true,
obscureText: obscureText,
context: context,
toggleVisibility: () => ref
.read(obscureTextProvider.notifier)
.toggle(),
),
),
const SizedBox(height: 20.0),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 60.0,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Checkbox(
activeColor:
AppColors.kBlueColor,
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
),
const Text('Remember Me'),
],
),
),
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: ErrorMessageWidget(
errorMessage: _errorMessage,
onClose: _handleErrorMessageClose,
),
),
const SizedBox(height: 10.0),
const SizedBox(height: 40),
_buildLoginButton(),
],
),
),
),
),
),
),
),
),
),
),
],
),
),
);
}
}
Any help on fixing this issue would be greatly appreciated. Thank you!