Merge pull request 'refresh token ability added to project' (#65) from feat_refresh_token into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/65
This commit is contained in:
a.ghabeli
2025-08-25 12:15:20 +03:30
4 changed files with 73 additions and 15 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ List<SingleChildWidget> get providersRemote {
List<SingleChildWidget> get cores { List<SingleChildWidget> get cores {
return [ return [
Provider(create: (context) => NetworkManager()), Provider(create: (context) => NetworkManager(context.read())),
ChangeNotifierProvider(create: (context) => ThemeProvider()), ChangeNotifierProvider(create: (context) => ThemeProvider()),
]; ];
} }
+58 -12
View File
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.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 '../config/app_config.dart';
import '../utils/logger.dart'; import '../utils/logger.dart';
@@ -13,11 +14,14 @@ class NetworkManager {
final Dio mainDio; final Dio mainDio;
final NetworkConnectivity _connectivity = NetworkConnectivity(); final NetworkConnectivity _connectivity = NetworkConnectivity();
bool _interceptorsAdded = false; bool _interceptorsAdded = false;
final SharedPreferences _prefs;
NetworkManager() NetworkManager(this._prefs)
: mainDio = Dio( : mainDio = Dio(
BaseOptions( BaseOptions(
baseUrl: AppConfig.apiBaseUrl, baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -33,7 +37,10 @@ class NetworkManager {
final requestInterceptor = InterceptorsWrapper( final requestInterceptor = InterceptorsWrapper(
onRequest: (options, handler) { onRequest: (options, handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}'); 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); return handler.next(options);
}, },
onResponse: (response, handler) { onResponse: (response, handler) {
@@ -43,11 +50,18 @@ class NetworkManager {
return handler.next(response); return handler.next(response);
}, },
onError: (error, handler) { onError: (error, handler) async {
appLogger.error( appLogger.error(
'❌ Error: ${error.message} - ${error.requestOptions.path}', '❌ Error: ${error.message} - ${error.requestOptions.path}',
); );
appLogger.error('❌ Error type: ${error.type}'); 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); return handler.next(error);
}, },
); );
@@ -75,18 +89,12 @@ class NetworkManager {
); );
} }
SharedPreferences prefs = await SharedPreferences.getInstance();
Dio dioInstance = mainDio; Dio dioInstance = mainDio;
Future<Response> makeApiCall(String token) async { Future<Response> makeApiCall(String token) async {
final headers =
token.isEmpty
? <String, String>{}
: {'Authorization': 'Bearer $token'};
final options = Options( final options = Options(
headers: headers, validateStatus:
validateStatus: (status) => status != null && status < 500, (status) => status != null && status >= 200 && status < 300,
); );
switch (method.toUpperCase()) { switch (method.toUpperCase()) {
@@ -133,7 +141,7 @@ class NetworkManager {
} }
} }
String? bearerToken = prefs.getString('bearer'); String? bearerToken = _prefs.getString('bearer');
// String? url = prefs.getString('url'); // String? url = prefs.getString('url');
// appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}'); // appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
@@ -297,4 +305,42 @@ class NetworkManager {
return 'Unknown error'; return 'Unknown error';
} }
/// Refresh the access token using the refresh token
Future<String?> refreshToken() async {
try {
final refreshToken = _prefs.getString('refresh_token');
if (refreshToken == null) {
appLogger.error('❌ No refresh token available');
return null;
}
final result = await makeRequest<LoginResponseModel>(
'/api/v1/profile/refresh-token',
(json) => LoginResponseModel.fromJson(json),
method: 'POST',
data: {'refresh_token': refreshToken},
);
if (result is Ok<LoginResponseModel>) {
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<LoginResponseModel>) {
appLogger.error('❌ Token refresh failed: ${result.error}');
}
} catch (e) {
appLogger.error('❌ Token refresh error: $e');
}
return null;
}
} }
+3
View File
@@ -31,7 +31,10 @@ class AuthService {
if (result is Ok<LoginResponseModel>) { if (result is Ok<LoginResponseModel>) {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final token = result.value.data.accessToken; final token = result.value.data.accessToken;
final refreshToken = result.value.data.refreshToken;
await prefs.setString('bearer', token!); await prefs.setString('bearer', token!);
await prefs.setString('refresh_token', refreshToken!);
} }
return result; return result;
} }
+11 -2
View File
@@ -3,6 +3,8 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/config/dependencies.dart';
import 'package:tm_app/core/theme/theme_provider.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/theme/colors.dart';
import 'core/utils/logger.dart'; import 'core/utils/logger.dart';
void main() { void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize the logger // Initialize the logger
appLogger.init(); appLogger.init();
// Log app start // Log app start
appLogger.info('🚀 Starting TM App...'); appLogger.info('🚀 Starting TM App...');
// appLogger.logNavigation('Home', 'Login'); // appLogger.logNavigation('Home', 'Login');
final prefs = await SharedPreferences.getInstance();
runApp(MultiProvider(providers: [...providersRemote], child: const MyApp())); List<SingleChildWidget> providers = [
Provider(create: (context) => prefs),
...providersRemote,
];
runApp(MultiProvider(providers: providers, child: const MyApp()));
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {