From 7cfb65c20c3ea0fba88bf240d23b88b21eab717d Mon Sep 17 00:00:00 2001 From: Lakhan Baheti Date: Wed, 11 Oct 2023 18:55:35 +0530 Subject: [PATCH 1/4] feat: re-generate token on expiry --- lib/main.dart | 3 +- lib/services/dio_service.dart | 61 ++++-------------- .../interceptors/token_refresh_handler.dart | 63 +++++++++++++++++++ 3 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 lib/services/interceptors/token_refresh_handler.dart diff --git a/lib/main.dart b/lib/main.dart index d4ea699..75afcf1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; late String selectedTheme; void main() async { - final widgetsBinding=WidgetsFlutterBinding.ensureInitialized(); + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); await Future.delayed(const Duration(milliseconds: 300)); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); await dotenv.load(fileName: '.env'); @@ -53,6 +53,7 @@ class _MyAppState extends ConsumerState with WidgetsBindingObserver { @override void initState() { WidgetsBinding.instance.addObserver(this); + // Const.accessToken = "${Const.accessToken!}|"; DependencyResolver.resolve(ref: ref); super.initState(); } diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index 9a33e26..c4e3d28 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -4,16 +4,15 @@ import 'dart:io'; // Package imports: import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; import 'package:plane/config/const.dart'; -import 'package:plane/screens/on_boarding/on_boarding_screen.dart'; -import 'package:plane/services/shared_preference_service.dart'; +import 'package:plane/services/interceptors/token_refresh_handler.dart'; import 'package:plane/utils/enums.dart'; import 'package:retry/retry.dart'; class DioConfig { // Static Dio created to directly access Dio client static Dio dio = Dio(); + static Dio getDio({ dynamic data, bool hasAuth = true, @@ -41,52 +40,16 @@ class DioConfig { ); dio.options = options; - dio.interceptors.add( - // InterceptorsWrapper(onRequest: - // (RequestOptions options, RequestInterceptorHandler handler) async { - // if (hasBody) options.data = data; - // return handler.next(options); - // }), - InterceptorsWrapper( - onRequest: - (RequestOptions options, RequestInterceptorHandler handler) async { - if (hasBody) options.data = data; - return handler.next(options); - }, - onError: (DioException error, ErrorInterceptorHandler handler) async { - // bool isConnected = await ConnectionService.checkConnectivity(); - // if (!isConnected) { - // Navigator.push( - // Const.globalKey.currentContext!, - // MaterialPageRoute( - // builder: (ctx) => Scaffold( - // body: errorState( - // context: Const.globalKey.currentContext!, - // )))); - // } - if (error.response?.statusCode == 401) { - await SharedPrefrenceServices.instance.clear(); - Const.accessToken = ''; - Const.userId = null; - Navigator.push(Const.globalKey.currentContext!, - MaterialPageRoute(builder: (ctx) => const OnBoardingScreen())); - } - // Retrieve the error response data - final errorResponse = error.response?.data; - - // Create a new DioError instance with the error response data - final DioException newError = DioException( - response: error.response, - requestOptions: error.requestOptions, - error: errorResponse, - ); - - // Return the new DioError instance - handler.reject(newError); - }, - ) - // DioFirebasePerformanceInterceptor(), - ); + dio.interceptors.addAll([ + InterceptorsWrapper( + onRequest: + (RequestOptions options, RequestInterceptorHandler handler) async { + if (hasBody) options.data = data; + return handler.next(options); + }, + ), + ReGenerateToken(dio) + ]); return dio; } diff --git a/lib/services/interceptors/token_refresh_handler.dart b/lib/services/interceptors/token_refresh_handler.dart new file mode 100644 index 0000000..ec678c4 --- /dev/null +++ b/lib/services/interceptors/token_refresh_handler.dart @@ -0,0 +1,63 @@ +import 'dart:developer'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:plane/config/apis.dart'; +import 'package:plane/config/const.dart'; +import 'package:plane/config/http_satus_codes.dart'; +import 'package:plane/screens/on_boarding/on_boarding_screen.dart'; +import 'package:plane/services/dio_service.dart'; +import 'package:plane/services/shared_preference_service.dart'; +import 'package:plane/utils/enums.dart'; + +class ReGenerateToken extends Interceptor { + ReGenerateToken(this.dio); + final Dio dio; + int retryCount = 0; + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + try { + if (err.response?.statusCode == status401Unauthorized && retryCount < 1) { + retryCount++; + await generateToken(dio); + final response = await dio.get(err.requestOptions.uri.toString()); + handler.resolve(response); + } else { + handler.reject(err); + } + } catch (error) { + await SharedPrefrenceServices.instance.clear(); + Const.accessToken = ''; + Const.userId = null; + Navigator.push(Const.globalKey.currentContext!, + MaterialPageRoute(builder: (ctx) => const OnBoardingScreen())); + handler.reject(err); + } + } + + Future generateToken(Dio dio) async { + // await Future.delayed(const Duration(seconds: 1)); + // log('TOKEN GENERATED'); + // Const.accessToken = Const.accessToken!.split('|').first; + // print(Const.accessToken); + // dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + + try { + final response = await DioConfig().dioServe( + hasAuth: false, + url: '${APIs.baseApi}/api/token/refresh/', + hasBody: true, + data: {"refresh": Const.refreshToken}, + httpMethod: HttpMethod.post, + ); + SharedPrefrenceServices.setTokens( + accessToken: response.data['access'], + refreshToken: Const.refreshToken!, + ); + dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + } on DioException catch (error) { + log(error.toString()); + rethrow; + } + } +} From 3c6e41b647557f820647336c95d2e6e81fbb491d Mon Sep 17 00:00:00 2001 From: Lakhan Baheti Date: Wed, 11 Oct 2023 19:37:06 +0530 Subject: [PATCH 2/4] refactor: support to all type of HTTP request --- .../interceptors/token_refresh_handler.dart | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/services/interceptors/token_refresh_handler.dart b/lib/services/interceptors/token_refresh_handler.dart index ec678c4..c3cb1cb 100644 --- a/lib/services/interceptors/token_refresh_handler.dart +++ b/lib/services/interceptors/token_refresh_handler.dart @@ -19,8 +19,31 @@ class ReGenerateToken extends Interceptor { try { if (err.response?.statusCode == status401Unauthorized && retryCount < 1) { retryCount++; + final Response response; await generateToken(dio); - final response = await dio.get(err.requestOptions.uri.toString()); + switch (err.requestOptions.method) { + case 'GET': + response = await dio.get(err.requestOptions.uri.toString()); + break; + case 'POST': + response = await dio.post(err.requestOptions.uri.toString()); + break; + case 'PUT': + response = await dio.put(err.requestOptions.uri.toString()); + break; + case 'DELETE': + response = await dio.delete(err.requestOptions.uri.toString()); + break; + case 'PATCH': + response = await dio.patch(err.requestOptions.uri.toString()); + break; + default: + response = await dio.get(err.requestOptions.uri.toString()); + break; + } + + retryCount = 0; + log(response.toString()); handler.resolve(response); } else { handler.reject(err); @@ -39,7 +62,6 @@ class ReGenerateToken extends Interceptor { // await Future.delayed(const Duration(seconds: 1)); // log('TOKEN GENERATED'); // Const.accessToken = Const.accessToken!.split('|').first; - // print(Const.accessToken); // dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; try { From 590b08d142b26c9ecd68bef61d6e8280466afb0b Mon Sep 17 00:00:00 2001 From: Lakhan Baheti Date: Thu, 12 Oct 2023 18:39:56 +0530 Subject: [PATCH 3/4] fix: refresh token fail on simultaneous api calls --- lib/provider/projects_provider.dart | 2 + .../Home/Dashboard/dash_board_screen.dart | 5 +- .../ProfileSettings/profile_screen.dart | 3 +- .../ProjectDetail/IssuesTab/create_issue.dart | 5 +- .../ProjectDetail/Settings/states_pages.dart | 1 + .../MainScreens/Projects/project_screen.dart | 2 + lib/services/dio_service.dart | 29 +-- .../interceptors/token_refresh_handler.dart | 167 +++++++++++------- lib/services/jwt_decoder.dart | 99 +++++++++++ lib/services/log.dart | 51 ++++++ pubspec.yaml | 3 +- 11 files changed, 291 insertions(+), 76 deletions(-) create mode 100644 lib/services/jwt_decoder.dart create mode 100644 lib/services/log.dart diff --git a/lib/provider/projects_provider.dart b/lib/provider/projects_provider.dart index 85a7130..e438e33 100644 --- a/lib/provider/projects_provider.dart +++ b/lib/provider/projects_provider.dart @@ -102,10 +102,12 @@ class ProjectsProvider extends ChangeNotifier { .workspaceSlug; prov.getStates(slug: workspaceSlug, projID: currentProject['id']); + prov.getProjectMembers( slug: workspaceSlug, projID: currentProject['id'], ); + // return; ref.read(ProviderList.estimatesProvider).getEstimates( slug: workspaceSlug, projID: currentProject['id'], diff --git a/lib/screens/MainScreens/Home/Dashboard/dash_board_screen.dart b/lib/screens/MainScreens/Home/Dashboard/dash_board_screen.dart index 7ed3110..b0153ff 100644 --- a/lib/screens/MainScreens/Home/Dashboard/dash_board_screen.dart +++ b/lib/screens/MainScreens/Home/Dashboard/dash_board_screen.dart @@ -492,7 +492,7 @@ class _DashBoardScreenState extends ConsumerState { Container( margin: const EdgeInsets.all(8), padding: - const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + const EdgeInsets.symmetric(vertical: 10, horizontal: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(3), color: themeProvider.themeManager.toastErrorColor, @@ -503,6 +503,7 @@ class _DashBoardScreenState extends ConsumerState { flex: flexForUpcomingAndOverdueWidgets[0], child: CustomText( 'Overdue', + height: 1, color: themeProvider.themeManager.primaryTextColor, fontWeight: FontWeightt.Semibold, )), @@ -513,6 +514,7 @@ class _DashBoardScreenState extends ConsumerState { flex: flexForUpcomingAndOverdueWidgets[1], child: CustomText( 'Issue', + height: 1, color: themeProvider.themeManager.primaryTextColor, fontWeight: FontWeightt.Semibold, )), @@ -520,6 +522,7 @@ class _DashBoardScreenState extends ConsumerState { flex: flexForUpcomingAndOverdueWidgets[2], child: CustomText( 'Due Date', + height: 1, color: themeProvider.themeManager.primaryTextColor, fontWeight: FontWeightt.Semibold, ), diff --git a/lib/screens/MainScreens/Profile/ProfileSettings/profile_screen.dart b/lib/screens/MainScreens/Profile/ProfileSettings/profile_screen.dart index 51e57bc..e9950ff 100644 --- a/lib/screens/MainScreens/Profile/ProfileSettings/profile_screen.dart +++ b/lib/screens/MainScreens/Profile/ProfileSettings/profile_screen.dart @@ -461,10 +461,11 @@ class _ProfileScreenState extends ConsumerState { Container( width: MediaQuery.of(context).size.width - 105, padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 10), + vertical: 15, horizontal: 10), child: CustomText( menus[index]['menu'], type: FontStyle.Medium, + height: 1, color: themeProvider.themeManager.primaryColour, textAlign: TextAlign.start, ), diff --git a/lib/screens/MainScreens/Projects/ProjectDetail/IssuesTab/create_issue.dart b/lib/screens/MainScreens/Projects/ProjectDetail/IssuesTab/create_issue.dart index c3bd2ad..fbb3116 100644 --- a/lib/screens/MainScreens/Projects/ProjectDetail/IssuesTab/create_issue.dart +++ b/lib/screens/MainScreens/Projects/ProjectDetail/IssuesTab/create_issue.dart @@ -388,8 +388,11 @@ class _CreateIssueState extends ConsumerState .primaryTextColor, fontWeight: FontWeightt.Medium, ), - const Icon( + Icon( Icons.keyboard_arrow_down, + color: themeProvider + .themeManager + .primaryTextColor, ) ], ) diff --git a/lib/screens/MainScreens/Projects/ProjectDetail/Settings/states_pages.dart b/lib/screens/MainScreens/Projects/ProjectDetail/Settings/states_pages.dart index 3b10601..f2eedd0 100644 --- a/lib/screens/MainScreens/Projects/ProjectDetail/Settings/states_pages.dart +++ b/lib/screens/MainScreens/Projects/ProjectDetail/Settings/states_pages.dart @@ -142,6 +142,7 @@ class _StatesPageState extends ConsumerState { issuesProvider.statesData[states[index]][idx] ['name'], type: FontStyle.Medium, + height: 1, color: themeProvider.themeManager.primaryTextColor, ), diff --git a/lib/screens/MainScreens/Projects/project_screen.dart b/lib/screens/MainScreens/Projects/project_screen.dart index d9fc26e..44542d9 100644 --- a/lib/screens/MainScreens/Projects/project_screen.dart +++ b/lib/screens/MainScreens/Projects/project_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import 'package:plane/bottom_sheets/global_search_sheet.dart'; +import 'package:plane/config/const.dart'; import 'package:plane/provider/projects_provider.dart'; import 'package:plane/provider/theme_provider.dart'; import 'package:plane/screens/MainScreens/Projects/ProjectDetail/project_detail.dart'; @@ -440,6 +441,7 @@ class _ProjectScreenState extends ConsumerState ? Container() : ListTile( onTap: () { + Const.accessToken = "${Const.accessToken!}|"; if (projectProvider.currentProject != projectProvider.projects[index]) { ref.read(ProviderList.issuesProvider).clearData(); diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index c4e3d28..7774dca 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -6,14 +6,18 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:plane/config/const.dart'; import 'package:plane/services/interceptors/token_refresh_handler.dart'; +import 'package:plane/services/log.dart'; import 'package:plane/utils/enums.dart'; import 'package:retry/retry.dart'; class DioConfig { // Static Dio created to directly access Dio client static Dio dio = Dio(); + static bool refreshingToken = false; + static RequestInterceptorHandler? nextHandler; + static RequestOptions? nextOptions; - static Dio getDio({ + Dio getDio({ dynamic data, bool hasAuth = true, bool hasBody = true, @@ -40,16 +44,19 @@ class DioConfig { ); dio.options = options; - dio.interceptors.addAll([ - InterceptorsWrapper( - onRequest: - (RequestOptions options, RequestInterceptorHandler handler) async { - if (hasBody) options.data = data; - return handler.next(options); - }, - ), - ReGenerateToken(dio) - ]); + if (dio.interceptors.length == 1) { + Log.info('Interceptor added'); + dio.interceptors.addAll([ + ReGenerateToken(dio), + // InterceptorsWrapper( + // onRequest: (RequestOptions options, + // RequestInterceptorHandler handler) async { + // if (hasBody) options.data = data; + // return handler.reject(DioException(requestOptions: options)); + // }, + // ), + ]); + } return dio; } diff --git a/lib/services/interceptors/token_refresh_handler.dart b/lib/services/interceptors/token_refresh_handler.dart index c3cb1cb..9e9390e 100644 --- a/lib/services/interceptors/token_refresh_handler.dart +++ b/lib/services/interceptors/token_refresh_handler.dart @@ -1,84 +1,129 @@ -import 'dart:developer'; - +import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:plane/config/apis.dart'; import 'package:plane/config/const.dart'; -import 'package:plane/config/http_satus_codes.dart'; import 'package:plane/screens/on_boarding/on_boarding_screen.dart'; -import 'package:plane/services/dio_service.dart'; +import 'package:plane/services/jwt_decoder.dart'; +import 'package:plane/services/log.dart'; import 'package:plane/services/shared_preference_service.dart'; -import 'package:plane/utils/enums.dart'; + class ReGenerateToken extends Interceptor { ReGenerateToken(this.dio); final Dio dio; int retryCount = 0; + bool isRefreshing = false; + List failedApis = []; + @override - void onError(DioException err, ErrorInterceptorHandler handler) async { - try { - if (err.response?.statusCode == status401Unauthorized && retryCount < 1) { - retryCount++; - final Response response; + void onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { + if ((JwtDecoder.tryDecode(Const.accessToken!) == null || + !JwtDecoder.isExpired(Const.accessToken!)) && + !isRefreshing) { + try { + isRefreshing = true; + Log.warn('TOKEN EXPIRED'); await generateToken(dio); - switch (err.requestOptions.method) { - case 'GET': - response = await dio.get(err.requestOptions.uri.toString()); - break; - case 'POST': - response = await dio.post(err.requestOptions.uri.toString()); - break; - case 'PUT': - response = await dio.put(err.requestOptions.uri.toString()); - break; - case 'DELETE': - response = await dio.delete(err.requestOptions.uri.toString()); - break; - case 'PATCH': - response = await dio.patch(err.requestOptions.uri.toString()); - break; - default: - response = await dio.get(err.requestOptions.uri.toString()); - break; - } - - retryCount = 0; - log(response.toString()); - handler.resolve(response); + options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + return handler.next(options); + } catch (error) { + Log.error('GENERATE TOKEN FAILED'); + isRefreshing = false; + await SharedPrefrenceServices.instance.clear(); + Const.accessToken = ''; + Const.userId = null; + Navigator.push(Const.globalKey.currentContext!, + MaterialPageRoute(builder: (ctx) => const OnBoardingScreen())); + return handler.reject(DioException(requestOptions: options)); + } + } else { + if (isRefreshing) { + Timer.periodic(const Duration(seconds: 6), (timer) async { + if (timer.tick == 1) { + timer.cancel(); + return handler.reject(DioException(requestOptions: options)); + } + if (!isRefreshing) { + timer.cancel(); + options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + return handler.next(options); + } + }); } else { - handler.reject(err); + return handler.next(options); } - } catch (error) { - await SharedPrefrenceServices.instance.clear(); - Const.accessToken = ''; - Const.userId = null; - Navigator.push(Const.globalKey.currentContext!, - MaterialPageRoute(builder: (ctx) => const OnBoardingScreen())); - handler.reject(err); } } + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + Log.debug( + 'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}', + ); + super.onResponse(response, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + Log.error( + 'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}', + ); + super.onError(err, handler); + } + Future generateToken(Dio dio) async { - // await Future.delayed(const Duration(seconds: 1)); - // log('TOKEN GENERATED'); - // Const.accessToken = Const.accessToken!.split('|').first; - // dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + await Future.delayed(const Duration(seconds: 1)); + Log.info('TOKEN GENERATED'); + Const.accessToken = Const.accessToken!.split('|').first; + dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + isRefreshing = false; + // try { + // final response = await DioConfig().dioServe( + // hasAuth: false, + // url: '${APIs.baseApi}/api/token/refresh/', + // hasBody: true, + // data: {"refresh": Const.refreshToken}, + // httpMethod: HttpMethod.post, + // ); + // SharedPrefrenceServices.setTokens( + // accessToken: response.data['access'], + // refreshToken: Const.refreshToken!, + // ); + // dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; + // } on DioException catch (error) { + // log(error.toString()); + // rethrow; + // } + } + Future _retry( + {required Dio dio, required RequestOptions requestOptions}) async { + String endpoint = requestOptions.uri.toString(); + final Response response; try { - final response = await DioConfig().dioServe( - hasAuth: false, - url: '${APIs.baseApi}/api/token/refresh/', - hasBody: true, - data: {"refresh": Const.refreshToken}, - httpMethod: HttpMethod.post, - ); - SharedPrefrenceServices.setTokens( - accessToken: response.data['access'], - refreshToken: Const.refreshToken!, - ); - dio.options.headers['Authorization'] = 'Bearer ${Const.accessToken!}'; - } on DioException catch (error) { - log(error.toString()); + switch (requestOptions.method) { + case 'GET': + response = await dio.get(endpoint); + break; + case 'POST': + response = await dio.post(endpoint); + break; + case 'PUT': + response = await dio.put(endpoint); + break; + case 'DELETE': + response = await dio.delete(endpoint); + break; + case 'PATCH': + response = await dio.patch(endpoint); + break; + default: + response = await dio.get(endpoint); + break; + } + return response; + } on DioException catch (_) { rethrow; } } diff --git a/lib/services/jwt_decoder.dart b/lib/services/jwt_decoder.dart new file mode 100644 index 0000000..77e90e0 --- /dev/null +++ b/lib/services/jwt_decoder.dart @@ -0,0 +1,99 @@ +library jwt_decoder; + +import 'dart:convert'; + +class JwtDecoder { + /// Decode a string JWT token into a `Map` + /// containing the decoded JSON payload. + /// + /// Note: header and signature are not returned by this method. + /// + /// Throws [FormatException] if parameter is not a valid JWT token. + static Map decode(String token) { + final splitToken = token.split("."); // Split the token by '.' + if (splitToken.length != 3) { + throw const FormatException('Invalid token'); + } + try { + final payloadBase64 = splitToken[1]; // Payload is always the index 1 + // Base64 should be multiple of 4. Normalize the payload before decode it + final normalizedPayload = base64.normalize(payloadBase64); + // Decode payload, the result is a String + final payloadString = utf8.decode(base64.decode(normalizedPayload)); + // Parse the String to a Map + final decodedPayload = jsonDecode(payloadString); + + // Return the decoded payload + return decodedPayload; + } catch (error) { + throw const FormatException('Invalid payload'); + } + } + + /// Decode a string JWT token into a `Map` + /// containing the decoded JSON payload. + /// + /// Note: header and signature are not returned by this method. + /// + /// Returns null if the token is not valid + static Map? tryDecode(String token) { + try { + return decode(token); + } catch (error) { + return null; + } + } + + /// Tells whether a token is expired. + /// + /// Returns false if the token is valid, true if it is expired. + /// + /// Throws [FormatException] if parameter is not a valid JWT token. + static bool isExpired(String token) { + final expirationDate = getExpirationDate(token); + if (expirationDate == null) { + return false; + } + // If the current date is after the expiration date, the token is already expired + return DateTime.now().isAfter(expirationDate); + } + + static DateTime? _getDate({required String token, required String claim}) { + final decodedToken = decode(token); + final expiration = decodedToken[claim] as int?; + if (expiration == null) { + return null; + } + return DateTime.fromMillisecondsSinceEpoch(expiration * 1000); + } + + /// Returns token expiration date + /// + /// Throws [FormatException] if parameter is not a valid JWT token. + static DateTime? getExpirationDate(String token) { + return _getDate(token: token, claim: 'exp'); + } + + /// Returns token issuing date (iat) + /// + /// Throws [FormatException] if parameter is not a valid JWT token. + static Duration? getTokenTime(String token) { + final issuedAtDate = _getDate(token: token, claim: 'iat'); + if (issuedAtDate == null) { + return null; + } + return DateTime.now().difference(issuedAtDate); + } + + /// Returns remaining time until expiry date. + /// + /// Throws [FormatException] if parameter is not a valid JWT token. + static Duration? getRemainingTime(String token) { + final expirationDate = getExpirationDate(token); + if (expirationDate == null) { + return null; + } + + return expirationDate.difference(DateTime.now()); + } +} \ No newline at end of file diff --git a/lib/services/log.dart b/lib/services/log.dart new file mode 100644 index 0000000..f36304a --- /dev/null +++ b/lib/services/log.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class Log { + Log() { + _logger = Logger( + printer: PrettyPrinter( + methodCount: 2, // number of method calls to be displayed + errorMethodCount: + 8, // number of method calls if stacktrace is provided + lineLength: 120, // width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + printTime: false // Should each log print contain a timestamp + ), + level: kDebugMode ? Level.debug : Level.info, + ); + } + static final shared = Log(); + late Logger _logger; + + static void info(dynamic msg, [dynamic error, StackTrace? stackTrace]) { + if (kDebugMode) { + Log.shared._logger.i(msg, error: error, stackTrace: stackTrace); + } + } + + static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) { + if (kDebugMode) { + Log.shared._logger.d(msg, error: error, stackTrace: stackTrace); + } + } + + static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) { + if (kDebugMode) { + Log.shared._logger.w(msg, error: error, stackTrace: stackTrace); + } + } + + static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) { + if (kDebugMode) { + Log.shared._logger.t(msg, error: error, stackTrace: stackTrace); + } + } + + static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) { + if (kDebugMode) { + Log.shared._logger.e(msg, error: error, stackTrace: stackTrace); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index dddbf11..28349d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: package_config: ^2.1.0 path_provider: ^2.0.15 percent_indicator: ^4.2.3 - rename: ^2.1.1 + rename: ^3.0.0 retry: ^3.1.1 shared_preferences: ^2.1.1 syncfusion_flutter_charts: ^21.2.9 @@ -74,6 +74,7 @@ dependencies: dartz: ^0.10.1 upgrader: ^8.1.0 flutter_native_splash: ^2.3.2 + logger: ^2.0.2+1 dev_dependencies: build_runner: ^2.1.7 From 6d2800867b294b0379a707405ad75f8b4cb62046 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:10:06 +0530 Subject: [PATCH 4/4] fix: is token expired condition --- lib/services/interceptors/token_refresh_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/interceptors/token_refresh_handler.dart b/lib/services/interceptors/token_refresh_handler.dart index 9e9390e..ec5981f 100644 --- a/lib/services/interceptors/token_refresh_handler.dart +++ b/lib/services/interceptors/token_refresh_handler.dart @@ -19,7 +19,7 @@ class ReGenerateToken extends Interceptor { void onRequest( RequestOptions options, RequestInterceptorHandler handler) async { if ((JwtDecoder.tryDecode(Const.accessToken!) == null || - !JwtDecoder.isExpired(Const.accessToken!)) && + JwtDecoder.isExpired(Const.accessToken!)) && !isRefreshing) { try { isRefreshing = true;