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 {
static BuildContext? get _ctx => key.currentContext;
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 (Get.isDialogOpen ?? false) Get.back();
_authDialogShown = true;
......
......@@ -10,7 +10,7 @@ class Constants {
class ErrorCodes {
static const List<String> tokenInvalidCodes = [
authTokenInvalid, authTokenEmpty
authTokenInvalid, authTokenEmpty, authTokenExpired
];
static const String deviceLock = "ERR_DEVICE_LOCK";
static const String deviceUndefined = "ERR_DEVICE_UNDEFINED";
......@@ -18,9 +18,10 @@ class ErrorCodes {
static const String invalidAccount = "ERR_INVALID_ACCOUNT";
static const String bioTokenInvalid = "ERR_INVALID_BIO_TOKEN";
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 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 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 '../../base/app_navigator.dart';
import '../../configs/api_paths.dart';
import '../../configs/constants.dart';
import '../../base/app_navigator.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 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../dio_extra_keys.dart';
import '../dio_http_service.dart';
class AuthInterceptor extends Interceptor {
bool _isHandlingAuth = false;
static const _kAuthHandledKey = '__auth_handled__';
static Completer<bool>? _refreshCompleter;
static Completer<void>? _logoutCompleter;
// Danh sách các path không cần refresh token khi gặp lỗi auth
static final List<String> _skipRefreshTokenPaths = [
APIPaths.getUserInfo,
APIPaths.login,
......@@ -21,36 +24,30 @@ class AuthInterceptor extends Interceptor {
];
@override
Future<void> onResponse(Response response, ResponseInterceptorHandler handler) async {
final data = response.data;
if (_isTokenInvalid(data)) {
response.requestOptions.extra[_kAuthHandledKey] = true;
// Kiểm tra xem path này có cần skip refresh token không
if (_shouldSkipRefreshToken(response.requestOptions.path)) {
handler.reject(
DioException(
requestOptions: response.requestOptions
..extra['mapped_error'] = ErrorCodes.tokenInvalidMessage,
Future<void> onResponse(
Response response,
ResponseInterceptorHandler handler,
) async {
if (_isTokenInvalid(response.data)) {
final request = response.requestOptions;
final authException = DioException(
requestOptions: request,
response: response,
type: DioExceptionType.badResponse,
error: 'ERR_AUTH_TOKEN_INVALID',
error: ErrorCodes.authTokenInvalid,
message: ErrorCodes.tokenInvalidMessage,
),
);
if (_shouldSkipRefreshToken(request)) {
handler.reject(authException);
return;
}
await _handleAuthError(data);
handler.reject(
DioException(
requestOptions: response.requestOptions
..extra['mapped_error'] = ErrorCodes.tokenInvalidMessage,
response: response,
type: DioExceptionType.badResponse,
error: 'ERR_AUTH_TOKEN_INVALID',
message: ErrorCodes.tokenInvalidMessage,
),
request.extra[_kAuthHandledKey] = true;
await _handleAuthError(
response.data,
originalRequest: request,
resolve: (retried) async => handler.resolve(retried),
reject: (error) async => handler.reject(error),
originalError: authException,
);
return;
}
......@@ -58,21 +55,33 @@ class AuthInterceptor extends Interceptor {
}
@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 data = err.response?.data;
final statusCode = err.response?.statusCode;
final isAuthException = (statusCode == 401 || statusCode == 403 || _isTokenInvalid(data));
if (alreadyHandled) {
handler.next(err);
return;
}
if (alreadyHandled) return;
if (_shouldSkipRefreshToken(err.requestOptions.path) &&
(statusCode == 401 || statusCode == 403 || _isTokenInvalid(data))) {
if (_shouldSkipRefreshToken(err.requestOptions)) {
handler.next(err);
return;
}
if (statusCode == 401 || _isTokenInvalid(data)) {
await _handleAuthError(data, originalRequest: err.requestOptions, handler: handler, originalError: err);
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;
}
handler.next(err);
......@@ -86,50 +95,107 @@ class AuthInterceptor extends Interceptor {
return false;
}
/// Kiểm tra xem path này có cần skip refresh token không
bool _shouldSkipRefreshToken(String path) {
bool _shouldSkipRefreshToken(RequestOptions options) {
final path = options.path;
if (path.isEmpty) return false;
for (String skipPath in _skipRefreshTokenPaths) {
if (path.contains(skipPath)) {
print('🔍 Path "$path" matches skip pattern "$skipPath", skipping refresh token.');
for (final candidate in _skipRefreshTokenPaths) {
if (path.contains(candidate)) {
return true;
}
}
print('🔍 Path "$path" does not match any skip patterns, will attempt refresh token if needed.');
return false;
}
Future<void> _handleAuthError(dynamic data, {RequestOptions? originalRequest, ErrorInterceptorHandler? handler, DioException? originalError}) async {
if (_isHandlingAuth) return;
_isHandlingAuth = true;
try {
// Thử refresh token trước khi logout
final refreshService = TokenRefreshService();
if (!refreshService.isRefreshing) {
await refreshService.refreshToken((success) async {
if (success && originalRequest != null && handler != null) {
Future<void> _handleAuthError(
dynamic data, {
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 RequestOptions retryOptions = originalRequest.copyWith();
final retryOptions = originalRequest.copyWith();
retryOptions.extra.remove(_kAuthHandledKey);
final dio = DioHttpService().dio;
final Response retried = await dio.fetch(retryOptions);
handler.resolve(retried);
await resolve(retried);
return;
} catch (e) {
handler.reject(errFrom(e, originalRequest));
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;
}
} else if (!success) {
_performLogout(data);
if (handler != null && originalError != null) handler.reject(originalError);
final existing = _refreshCompleter;
if (existing != null) {
return existing.future;
}
final completer = Completer<bool>();
_refreshCompleter = completer;
try {
final refreshService = TokenRefreshService();
await refreshService.refreshToken((success) async {
if (!success) {
await _ensureLogout(data);
if (!completer.isCompleted) completer.complete(false);
return;
}
if (!completer.isCompleted) completer.complete(true);
});
} else {
_performLogout(data);
if (handler != null && originalError != null) handler.reject(originalError);
return await completer.future;
} catch (_) {
await _ensureLogout(data);
if (!completer.isCompleted) completer.complete(false);
return false;
} finally {
if (identical(_refreshCompleter, completer)) {
_refreshCompleter = null;
}
}
}
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 {
_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) {
......@@ -138,14 +204,25 @@ class AuthInterceptor extends Interceptor {
}
Future<void> _performLogout(dynamic data) async {
LogoutService.logout();
await DataPreference.instance.clearData();
String? message;
if (data is Map<String, dynamic>) {
message = data['error_message']?.toString() ??
message =
data['error_message']?.toString() ??
data['errorMessage']?.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