diff --git a/lib/core/config/dependencies.dart b/lib/core/config/dependencies.dart index 207d007..da42d67 100644 --- a/lib/core/config/dependencies.dart +++ b/lib/core/config/dependencies.dart @@ -28,7 +28,7 @@ List get providersRemote { List get cores { return [ - Provider(create: (context) => NetworkManager()), + Provider(create: (context) => NetworkManager(context.read())), ChangeNotifierProvider(create: (context) => ThemeProvider()), ]; } diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index b557e6b..9832ccb 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; import '../config/app_config.dart'; import '../utils/logger.dart'; @@ -13,11 +14,14 @@ class NetworkManager { final Dio mainDio; final NetworkConnectivity _connectivity = NetworkConnectivity(); bool _interceptorsAdded = false; + final SharedPreferences _prefs; - NetworkManager() + NetworkManager(this._prefs) : mainDio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -33,7 +37,10 @@ class NetworkManager { final requestInterceptor = InterceptorsWrapper( onRequest: (options, handler) { appLogger.info('🌐 Request: ${options.method} ${options.path}'); - // appLogger.info('🌐 Headers: ${options.headers}'); + String? token = _prefs.getString('bearer'); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } return handler.next(options); }, onResponse: (response, handler) { @@ -43,11 +50,18 @@ class NetworkManager { return handler.next(response); }, - onError: (error, handler) { + onError: (error, handler) async { appLogger.error( '❌ Error: ${error.message} - ${error.requestOptions.path}', ); appLogger.error('❌ Error type: ${error.type}'); + if (error.response?.statusCode == 401) { + final newAccessToken = await refreshToken(); + if (newAccessToken != null) { + mainDio.options.headers['Authorization'] = 'Bearer $newAccessToken'; + return handler.resolve(await mainDio.fetch(error.requestOptions)); + } + } return handler.next(error); }, ); @@ -75,18 +89,12 @@ class NetworkManager { ); } - SharedPreferences prefs = await SharedPreferences.getInstance(); Dio dioInstance = mainDio; Future makeApiCall(String token) async { - final headers = - token.isEmpty - ? {} - : {'Authorization': 'Bearer $token'}; - final options = Options( - headers: headers, - validateStatus: (status) => status != null && status < 500, + validateStatus: + (status) => status != null && status >= 200 && status < 300, ); switch (method.toUpperCase()) { @@ -133,7 +141,7 @@ class NetworkManager { } } - String? bearerToken = prefs.getString('bearer'); + String? bearerToken = _prefs.getString('bearer'); // String? url = prefs.getString('url'); // appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}'); @@ -297,4 +305,42 @@ class NetworkManager { return 'Unknown error'; } + + /// Refresh the access token using the refresh token + Future refreshToken() async { + try { + final refreshToken = _prefs.getString('refresh_token'); + if (refreshToken == null) { + appLogger.error('❌ No refresh token available'); + return null; + } + + final result = await makeRequest( + '/api/v1/profile/refresh-token', + (json) => LoginResponseModel.fromJson(json), + method: 'POST', + data: {'refresh_token': refreshToken}, + ); + + if (result is Ok) { + final loginResponse = result.value; + final newAccessToken = loginResponse.data.accessToken; + final newRefreshToken = loginResponse.data.refreshToken; + + if (newAccessToken != null) { + await _prefs.setString('bearer', newAccessToken); + if (newRefreshToken != null) { + await _prefs.setString('refresh_token', newRefreshToken); + } + appLogger.info('✅ Token refreshed successfully'); + return newAccessToken; + } + } else if (result is Error) { + appLogger.error('❌ Token refresh failed: ${result.error}'); + } + } catch (e) { + appLogger.error('❌ Token refresh error: $e'); + } + return null; + } } diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 39c4564..3e19a17 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -31,7 +31,10 @@ class AuthService { if (result is Ok) { final prefs = await SharedPreferences.getInstance(); final token = result.value.data.accessToken; + final refreshToken = result.value.data.refreshToken; + await prefs.setString('bearer', token!); + await prefs.setString('refresh_token', refreshToken!); } return result; } diff --git a/lib/main.dart b/lib/main.dart index 8ed1ab3..80c3405 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/core/config/dependencies.dart'; import 'package:tm_app/core/theme/theme_provider.dart'; @@ -10,15 +12,22 @@ import 'core/routes/app_routes.dart'; import 'core/theme/colors.dart'; import 'core/utils/logger.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); // Initialize the logger appLogger.init(); // Log app start appLogger.info('🚀 Starting TM App...'); // appLogger.logNavigation('Home', 'Login'); + final prefs = await SharedPreferences.getInstance(); - runApp(MultiProvider(providers: [...providersRemote], child: const MyApp())); + List providers = [ + Provider(create: (context) => prefs), + ...providersRemote, + ]; + + runApp(MultiProvider(providers: providers, child: const MyApp())); } class MyApp extends StatefulWidget {