While doing our final application testing, we noticed that the token expiration was not handled properly. When the token expires, the application crashes. We need to handle this situation gracefully by logging the user out and redirecting them to the login page.
So, let's handle all of the 401
responses from the server and log the user out if the token has expired.
Adding AuthProvider to Api Service
First, we have to modify our API Service to accept AuthProvider. This will allow us to log the user out when the token expires:
import 'package:laravel_api_flutter_app/providers/auth_provider.dart'; // ... class ApiService { late String token; late AuthProvider? authProvider; ApiService(String token) { ApiService(String token, AuthProvider? auth) { token = token; authProvider = auth; } // ...
Now, we need to pass that AuthProvider
to the ApiService
for all Providers:
lib/providers/auth_provider.dart
// ... class AuthProvider extends ChangeNotifier { bool isAuthenticated = false; late String token; ApiService apiService = ApiService(''); ApiService apiService = ApiService('', null);// // ...
lib/providers/category_provider.dart
// ... class CategoryProvider extends ChangeNotifier { List<Category> categories = []; late ApiService apiService; late AuthProvider authProvider; CategoryProvider(AuthProvider authProvider) { authProvider = authProvider; apiService = ApiService(authProvider.token); apiService = ApiService(authProvider.token, authProvider); init(); }
lib/providers/transaction_provider.dart
// ... class TransactionProvider extends ChangeNotifier { List<Transaction> transactions = []; late ApiService apiService; late AuthProvider authProvider; TransactionProvider(AuthProvider authProvider) { authProvider = authProvider; init(); } Future init() async { apiService = ApiService(await authProvider.getToken()); apiService = ApiService(await authProvider.getToken(), authProvider); transactions = await apiService.fetchTransactions(); notifyListeners(); } // ...
Handling Token Expiration
Now, let's handle the token expiration in our API Service. We will do that by checking the response status code and logging the user out if the token has expired:
lib/services/api.dart
import 'dart:convert';import 'package:http/http.dart' as http;import 'package:laravel_api_flutter_app/models/category.dart';import 'package:laravel_api_flutter_app/models/transaction.dart';import 'package:laravel_api_flutter_app/providers/auth_provider.dart'; class ApiService { late String token; late AuthProvider? authProvider; ApiService(String token, AuthProvider? auth) { token = token; authProvider = auth; } final String baseUrl = 'https://0875-78-58-236-130.ngrok-free.app'; void logout() { authProvider?.logout(); } Future<List<Category>> fetchCategories() async { final http.Response response = await http .get(Uri.parse('$baseUrl/api/categories'), headers: <String, String>{ 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } final Map<String, dynamic> data = json.decode(response.body); if (!data.containsKey('data') || data['data'] is! List) { throw Exception('Failed to load categories'); } List categories = data['data']; return categories.map((category) => Category.fromJson(category)).toList(); } Future addCategory(String name) async { String url = '$baseUrl/api/categories'; final http.Response response = await http.post( Uri.parse(url), headers: <String, String>{ 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }, body: jsonEncode(<String, String>{ 'name': name, }), ); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } if (response.statusCode != 201) { throw Exception('Failed to create category'); } final Map<String, dynamic> data = json.decode(response.body); return Category.fromJson(data['data']); } Future saveCategory(Category category) async { String url = '$baseUrl/api/categories/${category.id}'; final http.Response response = await http.put( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode(<String, String>{ 'name': category.name, 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }), ); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } if (response.statusCode != 200) { throw Exception('Failed to update category'); } final Map<String, dynamic> data = json.decode(response.body); return Category.fromJson(data['data']); } Future<void> deleteCategory(id) async { String url = '$baseUrl/api/categories/$id'; final http.Response response = await http.delete(Uri.parse(url), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } if (response.statusCode != 204) { throw Exception('Failed to delete category'); } } Future<String> register(String name, String email, String password, String passwordConfirm, String deviceName) async { String url = '$baseUrl/api/auth/register'; final http.Response response = await http.post( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', 'Accept': 'application/json', }, body: jsonEncode(<String, String>{ 'name': name, 'email': email, 'password': password, 'password_confirmation': passwordConfirm, 'device_name': deviceName, }), ); if (response.statusCode == 422) { final Map<String, dynamic> data = json.decode(response.body); final Map<String, dynamic> errors = data['errors']; String message = ''; errors.forEach((key, value) { value.forEach((error) { message += '$error\n'; }); }); throw Exception(message); } return response.body; } Future<String> login( String email, String password, String deviceName) async { String url = '$baseUrl/api/auth/login'; final http.Response response = await http.post( Uri.parse(url), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', 'Accept': 'application/json', }, body: jsonEncode(<String, String>{ 'email': email, 'password': password, 'device_name': deviceName, }), ); if (response.statusCode == 422) { final Map<String, dynamic> data = json.decode(response.body); final Map<String, dynamic> errors = data['errors']; String message = ''; errors.forEach((key, value) { value.forEach((error) { message += '$error\n'; }); }); throw Exception(message); } return response.body; } Future<List<Transaction>> fetchTransactions() async { http.Response response = await http.get( Uri.parse('$baseUrl/api/transactions'), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }, ); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } final Map<String, dynamic> data = json.decode(response.body); if (!data.containsKey('data') || data['data'] is! List) { throw Exception('Failed to load categories'); } List transactions = data['data']; return transactions .map((transaction) => Transaction.fromJson(transaction)) .toList(); } Future<Transaction> addTransaction( String amount, String category, String description, String date) async { String uri = '$baseUrl/api/transactions'; http.Response response = await http.post(Uri.parse(uri), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }, body: jsonEncode({ 'amount': amount, 'category_id': category, 'description': description, 'transaction_date': date })); if (response.statusCode != 201) { throw Exception('Error happened on create'); } return Transaction.fromJson(jsonDecode(response.body)['data']); } if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } Future<Transaction> updateTransaction(Transaction transaction) async { String uri = '$baseUrl/api/transactions/${transaction.id}'; http.Response response = await http.put(Uri.parse(uri), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }, body: jsonEncode({ 'amount': transaction.amount, 'category_id': transaction.categoryId, 'description': transaction.description, 'transaction_date': transaction.transactionDate })); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } if (response.statusCode != 200) { print(response.body); throw Exception('Error happened on update'); } return Transaction.fromJson(jsonDecode(response.body)['data']); } Future<void> deleteTransaction(id) async { String uri = '$baseUrl/api/transactions/$id'; http.Response response = await http.delete( Uri.parse(uri), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token' }, ); if (response.statusCode == 401) { logout(); throw Exception('Unauthorized'); } if (response.statusCode != 204) { throw Exception('Error happened on delete'); } }}
When the token expires, the user will be logged out and redirected to the login page.
Check out the GitHub Commit for this lesson.
No comments yet…