Commit e9cf8244 authored by DatHV's avatar DatHV
Browse files

update fix bug, handle error.

parent a0bcdab2
assets/images/ic_point.png

5.13 KB | W: | H:

assets/images/ic_point.png

1.82 KB | W: | H:

assets/images/ic_point.png
assets/images/ic_point.png
assets/images/ic_point.png
assets/images/ic_point.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_rank_gray.png

2.88 KB | W: | H:

assets/images/ic_rank_gray.png

1.04 KB | W: | H:

assets/images/ic_rank_gray.png
assets/images/ic_rank_gray.png
assets/images/ic_rank_gray.png
assets/images/ic_rank_gray.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_scan_tutorial.png

39.2 KB | W: | H:

assets/images/ic_scan_tutorial.png

14.5 KB | W: | H:

assets/images/ic_scan_tutorial.png
assets/images/ic_scan_tutorial.png
assets/images/ic_scan_tutorial.png
assets/images/ic_scan_tutorial.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_search_black.png

1.57 KB | W: | H:

assets/images/ic_search_black.png

604 Bytes | W: | H:

assets/images/ic_search_black.png
assets/images/ic_search_black.png
assets/images/ic_search_black.png
assets/images/ic_search_black.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_sim_service.png

135 KB | W: | H:

assets/images/ic_sim_service.png

42.1 KB | W: | H:

assets/images/ic_sim_service.png
assets/images/ic_sim_service.png
assets/images/ic_sim_service.png
assets/images/ic_sim_service.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_speaker_toast.png

10.2 KB | W: | H:

assets/images/ic_speaker_toast.png

3.55 KB | W: | H:

assets/images/ic_speaker_toast.png
assets/images/ic_speaker_toast.png
assets/images/ic_speaker_toast.png
assets/images/ic_speaker_toast.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_time_record.png

3.28 KB | W: | H:

assets/images/ic_time_record.png

1.4 KB | W: | H:

assets/images/ic_time_record.png
assets/images/ic_time_record.png
assets/images/ic_time_record.png
assets/images/ic_time_record.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_time_refund.png

4.09 KB | W: | H:

assets/images/ic_time_refund.png

1.73 KB | W: | H:

assets/images/ic_time_refund.png
assets/images/ic_time_refund.png
assets/images/ic_time_refund.png
assets/images/ic_time_refund.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_topup.png

20.5 KB | W: | H:

assets/images/ic_topup.png

5.91 KB | W: | H:

assets/images/ic_topup.png
assets/images/ic_topup.png
assets/images/ic_topup.png
assets/images/ic_topup.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_topup_data.png

215 KB | W: | H:

assets/images/ic_topup_data.png

42.9 KB | W: | H:

assets/images/ic_topup_data.png
assets/images/ic_topup_data.png
assets/images/ic_topup_data.png
assets/images/ic_topup_data.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/ic_voucher_gray.png

1.61 KB | W: | H:

assets/images/ic_voucher_gray.png

678 Bytes | W: | H:

assets/images/ic_voucher_gray.png
assets/images/ic_voucher_gray.png
assets/images/ic_voucher_gray.png
assets/images/ic_voucher_gray.png
  • 2-up
  • Swipe
  • Onion skin
assets/images/splash_screen.png

40.2 KB | W: | H:

assets/images/splash_screen.png

34.4 KB | W: | H:

assets/images/splash_screen.png
assets/images/splash_screen.png
assets/images/splash_screen.png
assets/images/splash_screen.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -24,7 +24,7 @@ class AppNavigator { ...@@ -24,7 +24,7 @@ class AppNavigator {
static BuildContext? get _ctx => key.currentContext; static BuildContext? get _ctx => key.currentContext;
static Future<void> showAuthAlertAndGoLogin(String message) async { static Future<void> showAuthAlertAndGoLogin(String message) async {
final description = 'Phiên đăng nhập của bạn đã hết hạn. Vui lòng đăng nhập lại để tiếp tục sử dụng ứng dụng.'; final description = ErrorCodes.tokenInvalidMessage;
if (_authDialogShown || _ctx == null) return; if (_authDialogShown || _ctx == null) return;
if (Get.isDialogOpen ?? false) Get.back(); if (Get.isDialogOpen ?? false) Get.back();
_authDialogShown = true; _authDialogShown = true;
......
...@@ -10,7 +10,7 @@ class Constants { ...@@ -10,7 +10,7 @@ class Constants {
class ErrorCodes { class ErrorCodes {
static const List<String> tokenInvalidCodes = [ static const List<String> tokenInvalidCodes = [
authTokenInvalid, authTokenEmpty authTokenInvalid, authTokenEmpty, authTokenExpired
]; ];
static const String deviceLock = "ERR_DEVICE_LOCK"; static const String deviceLock = "ERR_DEVICE_LOCK";
static const String deviceUndefined = "ERR_DEVICE_UNDEFINED"; static const String deviceUndefined = "ERR_DEVICE_UNDEFINED";
...@@ -18,9 +18,10 @@ class ErrorCodes { ...@@ -18,9 +18,10 @@ class ErrorCodes {
static const String invalidAccount = "ERR_INVALID_ACCOUNT"; static const String invalidAccount = "ERR_INVALID_ACCOUNT";
static const String bioTokenInvalid = "ERR_INVALID_BIO_TOKEN"; static const String bioTokenInvalid = "ERR_INVALID_BIO_TOKEN";
static const String authTokenInvalid = "ERR_AUTH_TOKEN_INVALID"; static const String authTokenInvalid = "ERR_AUTH_TOKEN_INVALID";
static const String authTokenExpired = "ERR_AUTH_TOKEN_EXPIRED";
static const String authTokenEmpty = "ERR_AUTH_TOKEN_EMPTY"; static const String authTokenEmpty = "ERR_AUTH_TOKEN_EMPTY";
static const String commonError = "Hệ thống không thể xử lý yêu cầu hiện tại. Vui lòng thử lại sau hoặc liên hệ hotline 1900599863 để được trợ giúp."; static const String commonError = "Hệ thống không thể xử lý yêu cầu hiện tại. Vui lòng thử lại sau hoặc liên hệ hotline 1900599863 để được trợ giúp.";
static const String networkError = 'Kết nối mạng không ổn định. Vui lòng thử lại!'; static const String networkError = 'Kết nối mạng không ổn định. Vui lòng thử lại!';
static const String serverErrorMessage = 'Hệ thống không thể xử lý yêu cầu hiện tại. Vui lòng thử lại sau!'; static const String serverErrorMessage = 'Hệ thống không thể xử lý yêu cầu hiện tại. Vui lòng thử lại sau!';
static const String tokenInvalidMessage = 'Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.'; static const String tokenInvalidMessage = 'Phiên đăng nhập của bạn đã hết hạn. Vui lòng đăng nhập lại để tiếp tục sử dụng.';
} }
\ No newline at end of file
const String kExtraSkipApiErrorHandling = '__skip_api_error_handling__';
import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../base/app_navigator.dart';
import '../../configs/api_paths.dart'; import '../../configs/api_paths.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
import '../../base/app_navigator.dart';
import '../../services/logout_service.dart'; import '../../services/logout_service.dart';
import '../dio_http_service.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../../services/token_refresh_service.dart'; import '../../services/token_refresh_service.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../dio_extra_keys.dart';
import '../dio_http_service.dart';
class AuthInterceptor extends Interceptor { class AuthInterceptor extends Interceptor {
bool _isHandlingAuth = false;
static const _kAuthHandledKey = '__auth_handled__'; static const _kAuthHandledKey = '__auth_handled__';
static Completer<bool>? _refreshCompleter;
// Danh sách các path không cần refresh token khi gặp lỗi auth static Completer<void>? _logoutCompleter;
static final List<String> _skipRefreshTokenPaths = [ static final List<String> _skipRefreshTokenPaths = [
APIPaths.getUserInfo, APIPaths.getUserInfo,
APIPaths.login, APIPaths.login,
...@@ -21,36 +24,30 @@ class AuthInterceptor extends Interceptor { ...@@ -21,36 +24,30 @@ class AuthInterceptor extends Interceptor {
]; ];
@override @override
Future<void> onResponse(Response response, ResponseInterceptorHandler handler) async { Future<void> onResponse(
final data = response.data; Response response,
ResponseInterceptorHandler handler,
if (_isTokenInvalid(data)) { ) async {
response.requestOptions.extra[_kAuthHandledKey] = true; if (_isTokenInvalid(response.data)) {
// Kiểm tra xem path này có cần skip refresh token không final request = response.requestOptions;
if (_shouldSkipRefreshToken(response.requestOptions.path)) { final authException = DioException(
handler.reject( requestOptions: request,
DioException( response: response,
requestOptions: response.requestOptions type: DioExceptionType.badResponse,
..extra['mapped_error'] = ErrorCodes.tokenInvalidMessage, error: ErrorCodes.authTokenInvalid,
response: response, message: ErrorCodes.tokenInvalidMessage,
type: DioExceptionType.badResponse, );
error: 'ERR_AUTH_TOKEN_INVALID', if (_shouldSkipRefreshToken(request)) {
message: ErrorCodes.tokenInvalidMessage, handler.reject(authException);
),
);
return; return;
} }
request.extra[_kAuthHandledKey] = true;
await _handleAuthError(data); await _handleAuthError(
handler.reject( response.data,
DioException( originalRequest: request,
requestOptions: response.requestOptions resolve: (retried) async => handler.resolve(retried),
..extra['mapped_error'] = ErrorCodes.tokenInvalidMessage, reject: (error) async => handler.reject(error),
response: response, originalError: authException,
type: DioExceptionType.badResponse,
error: 'ERR_AUTH_TOKEN_INVALID',
message: ErrorCodes.tokenInvalidMessage,
),
); );
return; return;
} }
...@@ -58,21 +55,33 @@ class AuthInterceptor extends Interceptor { ...@@ -58,21 +55,33 @@ class AuthInterceptor extends Interceptor {
} }
@override @override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async { Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
final alreadyHandled = err.requestOptions.extra[_kAuthHandledKey] == true; final alreadyHandled = err.requestOptions.extra[_kAuthHandledKey] == true;
final data = err.response?.data; final data = err.response?.data;
final statusCode = err.response?.statusCode; final statusCode = err.response?.statusCode;
final isAuthException = (statusCode == 401 || statusCode == 403 || _isTokenInvalid(data));
if (alreadyHandled) return; if (alreadyHandled) {
if (_shouldSkipRefreshToken(err.requestOptions.path) &&
(statusCode == 401 || statusCode == 403 || _isTokenInvalid(data))) {
handler.next(err); handler.next(err);
return; return;
} }
if (statusCode == 401 || _isTokenInvalid(data)) { if (_shouldSkipRefreshToken(err.requestOptions)) {
await _handleAuthError(data, originalRequest: err.requestOptions, handler: handler, originalError: err); handler.next(err);
return;
}
if (isAuthException) {
err.requestOptions.extra[_kAuthHandledKey] = true;
await _handleAuthError(
data,
originalRequest: err.requestOptions,
resolve: (retried) async => handler.resolve(retried),
reject: (error) async => handler.reject(error),
originalError: err,
);
return; return;
} }
handler.next(err); handler.next(err);
...@@ -86,66 +95,134 @@ class AuthInterceptor extends Interceptor { ...@@ -86,66 +95,134 @@ class AuthInterceptor extends Interceptor {
return false; return false;
} }
/// Kiểm tra xem path này có cần skip refresh token không bool _shouldSkipRefreshToken(RequestOptions options) {
bool _shouldSkipRefreshToken(String path) { final path = options.path;
if (path.isEmpty) return false; if (path.isEmpty) return false;
for (String skipPath in _skipRefreshTokenPaths) { for (final candidate in _skipRefreshTokenPaths) {
if (path.contains(skipPath)) { if (path.contains(candidate)) {
print('🔍 Path "$path" matches skip pattern "$skipPath", skipping refresh token.');
return true; return true;
} }
} }
print('🔍 Path "$path" does not match any skip patterns, will attempt refresh token if needed.');
return false; return false;
} }
Future<void> _handleAuthError(dynamic data, {RequestOptions? originalRequest, ErrorInterceptorHandler? handler, DioException? originalError}) async { Future<void> _handleAuthError(
if (_isHandlingAuth) return; dynamic data, {
_isHandlingAuth = true; required RequestOptions originalRequest,
required Future<void> Function(Response response) resolve,
required Future<void> Function(DioException error) reject,
required DioException originalError,
}) async {
final refreshSucceeded = await _ensureTokenRefreshed(data);
if (refreshSucceeded) {
try {
final retryOptions = originalRequest.copyWith();
retryOptions.extra.remove(_kAuthHandledKey);
final dio = DioHttpService().dio;
final Response retried = await dio.fetch(retryOptions);
await resolve(retried);
return;
} catch (e) {
if (e is DioException) {
if (_isAuthException(e)) {
await _ensureLogout(e.response?.data ?? data);
_markAuthFailure(e.requestOptions);
}
e.requestOptions.extra[kExtraSkipApiErrorHandling] = true;
await reject(e);
return;
}
await reject(errFrom(e, originalRequest));
return;
}
}
_markAuthFailure(originalRequest);
originalError.requestOptions.extra[kExtraSkipApiErrorHandling] = true;
await reject(originalError);
}
Future<bool> _ensureTokenRefreshed(dynamic data) async {
if (_logoutCompleter != null) {
await _logoutCompleter!.future;
return false;
}
final existing = _refreshCompleter;
if (existing != null) {
return existing.future;
}
final completer = Completer<bool>();
_refreshCompleter = completer;
try { try {
// Thử refresh token trước khi logout
final refreshService = TokenRefreshService(); final refreshService = TokenRefreshService();
if (!refreshService.isRefreshing) { await refreshService.refreshToken((success) async {
await refreshService.refreshToken((success) async { if (!success) {
if (success && originalRequest != null && handler != null) { await _ensureLogout(data);
try { if (!completer.isCompleted) completer.complete(false);
final RequestOptions retryOptions = originalRequest.copyWith(); return;
retryOptions.extra.remove(_kAuthHandledKey); }
final dio = DioHttpService().dio; if (!completer.isCompleted) completer.complete(true);
final Response retried = await dio.fetch(retryOptions); });
handler.resolve(retried); return await completer.future;
return; } catch (_) {
} catch (e) { await _ensureLogout(data);
handler.reject(errFrom(e, originalRequest)); if (!completer.isCompleted) completer.complete(false);
} return false;
} else if (!success) { } finally {
_performLogout(data); if (identical(_refreshCompleter, completer)) {
if (handler != null && originalError != null) handler.reject(originalError); _refreshCompleter = null;
}
});
} else {
_performLogout(data);
if (handler != null && originalError != null) handler.reject(originalError);
} }
}
}
Future<void> _ensureLogout(dynamic data) async {
final existing = _logoutCompleter;
if (existing != null) {
await existing.future;
return;
}
final completer = Completer<void>();
_logoutCompleter = completer;
try {
await _performLogout(data);
} finally { } finally {
_isHandlingAuth = false; if (!completer.isCompleted) completer.complete();
_logoutCompleter = null;
} }
} }
bool _isAuthException(DioException exception) {
final status = exception.response?.statusCode;
return status == 401 ||
status == 403 ||
_isTokenInvalid(exception.response?.data);
}
DioException errFrom(Object e, RequestOptions req) { DioException errFrom(Object e, RequestOptions req) {
if (e is DioException) return e; if (e is DioException) return e;
return DioException(requestOptions: req, error: e); return DioException(requestOptions: req, error: e);
} }
Future<void> _performLogout(dynamic data) async { Future<void> _performLogout(dynamic data) async {
LogoutService.logout();
await DataPreference.instance.clearData();
String? message; String? message;
if (data is Map<String, dynamic>) { if (data is Map<String, dynamic>) {
message = data['error_message']?.toString() ?? message =
data['error_message']?.toString() ??
data['errorMessage']?.toString() ?? data['errorMessage']?.toString() ??
data['message']?.toString(); data['message']?.toString();
} }
await AppNavigator.showAuthAlertAndGoLogin(message ?? ErrorCodes.tokenInvalidMessage); final logged = DataPreference.instance.logged;
if (logged) {
await AppNavigator.showAuthAlertAndGoLogin(
message ?? ErrorCodes.tokenInvalidMessage,
);
}
await LogoutService.logout();
await DataPreference.instance.clearData();
}
void _markAuthFailure(RequestOptions options) {
options.extra[_kAuthHandledKey] = true;
options.extra[kExtraSkipApiErrorHandling] = true;
} }
} }
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment