Skip to content

Commit 4c933de

Browse files
committed
TF-4081 Try to categorize the errors and display them to the user.
1 parent 0896778 commit 4c933de

File tree

5 files changed

+212
-89
lines changed

5 files changed

+212
-89
lines changed

lib/features/base/base_controller.dart

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import 'dart:async';
2-
import 'package:core/core.dart';
3-
import 'package:flutter/services.dart' as services;
2+
43
import 'package:contact/contact/model/capability_contact.dart';
4+
import 'package:core/core.dart';
55
import 'package:dartz/dartz.dart';
66
import 'package:fcm/model/firebase_capability.dart';
77
import 'package:fcm/model/firebase_registration_id.dart';
88
import 'package:flutter/material.dart';
9+
import 'package:flutter/services.dart' as services;
910
import 'package:flutter_svg/flutter_svg.dart';
1011
import 'package:forward/forward/capability_forward.dart';
1112
import 'package:get/get.dart';
@@ -56,8 +57,8 @@ import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
5657
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
5758
import 'package:tmail_ui_user/main/routes/app_routes.dart';
5859
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
59-
import 'package:tmail_ui_user/main/utils/app_config.dart';
6060
import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html;
61+
import 'package:tmail_ui_user/main/utils/app_config.dart';
6162
import 'package:tmail_ui_user/main/utils/toast_manager.dart';
6263
import 'package:tmail_ui_user/main/utils/twake_app_manager.dart';
6364
import 'package:uuid/uuid.dart';
@@ -152,7 +153,8 @@ abstract class BaseController extends GetxController
152153
bool validateUrgentException(dynamic exception) {
153154
return exception is NoNetworkError
154155
|| exception is BadCredentialsException
155-
|| exception is ConnectionError;
156+
|| exception is ConnectionError
157+
|| exception is ClientAuthenticationException;
156158
}
157159

158160
void handleErrorViewState(Object error, StackTrace stackTrace) {}
@@ -169,10 +171,14 @@ abstract class BaseController extends GetxController
169171

170172
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
171173
logError('$runtimeType::handleUrgentExceptionOnMobile():Failure: $failure | Exception: $exception');
172-
if (exception is ConnectionError) {
174+
if (exception is NoNetworkError) {
175+
_handleNotNetworkErrorException();
176+
} else if (exception is ConnectionError) {
173177
_handleConnectionErrorException();
174178
} else if (exception is BadCredentialsException) {
175179
handleBadCredentialsException();
180+
} else if (exception is ClientAuthenticationException) {
181+
handleClientAuthenticationException(exception);
176182
}
177183
}
178184

@@ -184,6 +190,8 @@ abstract class BaseController extends GetxController
184190
_handleConnectionErrorException();
185191
} else if (exception is BadCredentialsException) {
186192
handleBadCredentialsException();
193+
} else if (exception is ClientAuthenticationException) {
194+
handleClientAuthenticationException(exception);
187195
}
188196
}
189197

@@ -213,7 +221,8 @@ abstract class BaseController extends GetxController
213221
leadingSVGIcon: imagePaths.icNotConnection,
214222
backgroundColor: AppColor.textFieldErrorBorderColor,
215223
textColor: Colors.white,
216-
infinityToast: true);
224+
infinityToast: PlatformInfo.isWeb,
225+
);
217226
}
218227
}
219228

@@ -226,6 +235,21 @@ abstract class BaseController extends GetxController
226235
}
227236
}
228237

238+
void handleClientAuthenticationException(
239+
ClientAuthenticationException exception,
240+
) {
241+
final message = exception.message;
242+
final firstErrorCode = exception.code;
243+
final secondErrorCode = exception.secondErrorCode;
244+
log('$runtimeType::handleClientAuthenticationException: Message is $message, [$firstErrorCode - $secondErrorCode]');
245+
if (currentOverlayContext != null && currentContext != null) {
246+
appToast.showToastErrorMessage(
247+
currentOverlayContext!,
248+
'$message [$firstErrorCode - $secondErrorCode]',
249+
);
250+
}
251+
}
252+
229253
void _performSaveAndReconnection() {
230254
if (PlatformInfo.isWeb) {
231255
_executeBeforeReconnectAndLogOut();

lib/features/login/data/network/interceptors/authorization_interceptors.dart

Lines changed: 110 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import 'package:model/oidc/token_oidc.dart';
1414
import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart';
1515
import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart';
1616
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
17-
import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart';
1817
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
1918
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
19+
import 'package:tmail_ui_user/main/exceptions/chained_request_error.dart';
2020
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
2121

2222
class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
@@ -86,81 +86,46 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
8686
logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err');
8787
try {
8888
final requestOptions = err.requestOptions;
89-
final extraInRequest = requestOptions.extra;
90-
bool isRetryRequest = false;
89+
final extra = requestOptions.extra;
9190

92-
if (validateToRefreshToken(
91+
// Decide whether we need to refresh or retry
92+
final shouldRefresh = validateToRefreshToken(
9393
responseStatusCode: err.response?.statusCode,
94-
tokenOIDC: _token
95-
)) {
96-
log('AuthorizationInterceptors::onError: Perform get New Token');
97-
final newTokenOidc = PlatformInfo.isIOS
98-
? await _getNewTokenForIOSPlatform()
99-
: await _getNewTokenForOtherPlatform();
100-
101-
if (newTokenOidc.token == _token?.token) {
102-
log('AuthorizationInterceptors::onError: Token duplicated');
103-
return super.onError(err, handler);
104-
}
105-
_updateNewToken(newTokenOidc);
106-
107-
final personalAccount = await _updateCurrentAccount(tokenOIDC: newTokenOidc);
108-
109-
if (PlatformInfo.isIOS) {
110-
await _iosSharingManager.saveKeyChainSharingSession(personalAccount);
111-
}
112-
113-
isRetryRequest = true;
114-
} else if (validateToRetryTheRequestWithNewToken(
94+
tokenOIDC: _token,
95+
);
96+
final shouldRetryWithNewToken = validateToRetryTheRequestWithNewToken(
11597
authHeader: requestOptions.headers[HttpHeaders.authorizationHeader],
116-
tokenOIDC: _token
117-
)) {
118-
log('AuthorizationInterceptors::onError: Request using old token');
119-
isRetryRequest = true;
120-
} else {
98+
tokenOIDC: _token,
99+
);
100+
101+
if (!shouldRefresh && !shouldRetryWithNewToken) {
121102
return super.onError(err, handler);
122103
}
123104

124-
if (isRetryRequest) {
125-
if (extraInRequest.containsKey(FileUploader.uploadAttachmentExtraKey)) {
126-
log('AuthorizationInterceptors::onError: Retry upload request with TokenId = ${_token?.tokenIdHash}');
127-
final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey];
128-
129-
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token);
105+
// Refresh token if required
106+
if (shouldRefresh) {
107+
final newToken = await _refreshToken();
108+
if (newToken == null) {
109+
// Failed or duplicate token
110+
return super.onError(err, handler);
111+
}
112+
}
130113

131-
final newOptions = Options(
132-
method: requestOptions.method,
133-
headers: requestOptions.headers,
134-
);
114+
// Retry request with updated token
115+
final updatedAuthHeader = _getTokenAsBearerHeader(_token!.token);
116+
requestOptions.headers[HttpHeaders.authorizationHeader] =
117+
updatedAuthHeader;
135118

136-
final response = await _dio.request(
137-
requestOptions.path,
138-
data: _getDataUploadRequest(uploadExtra),
139-
queryParameters: requestOptions.queryParameters,
140-
options: newOptions,
141-
);
119+
final bool isUploadRequest =
120+
extra.containsKey(FileUploader.uploadAttachmentExtraKey);
142121

143-
return handler.resolve(response);
144-
} else {
145-
log('AuthorizationInterceptors::onError: Retry request with TokenId = ${_token?.tokenIdHash}');
146-
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token);
122+
final response = isUploadRequest
123+
? await _retryUploadRequest(requestOptions, extra)
124+
: await _retryNormalRequest(requestOptions);
147125

148-
final response = await _dio.fetch(requestOptions);
149-
return handler.resolve(response);
150-
}
151-
} else {
152-
return super.onError(err, handler);
153-
}
126+
return handler.resolve(response);
154127
} catch (e) {
155-
logError('AuthorizationInterceptors::onError:Exception: $e');
156-
if (e is ServerError || e is TemporarilyUnavailable) {
157-
return super.onError(
158-
DioError(requestOptions: err.requestOptions, error: e),
159-
handler,
160-
);
161-
} else {
162-
return super.onError(err.copyWith(error: e), handler);
163-
}
128+
return _handleRetryException(err, handler, e);
164129
}
165130
}
166131

@@ -211,24 +176,29 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
211176

212177
String _getTokenAsBearerHeader(String token) => 'Bearer $token';
213178

214-
Future<PersonalAccount> _updateCurrentAccount({required TokenOIDC tokenOIDC}) async {
215-
final currentAccount = await _accountCacheManager.getCurrentAccount();
179+
Future<PersonalAccount?> _updateCurrentAccount({required TokenOIDC tokenOIDC}) async {
180+
try {
181+
final currentAccount = await _accountCacheManager.getCurrentAccount();
216182

217-
await _accountCacheManager.deleteCurrentAccount(currentAccount.id);
183+
await _accountCacheManager.deleteCurrentAccount(currentAccount.id);
218184

219-
await _tokenOidcCacheManager.persistOneTokenOidc(tokenOIDC);
185+
await _tokenOidcCacheManager.persistOneTokenOidc(tokenOIDC);
220186

221-
final personalAccount = PersonalAccount(
222-
tokenOIDC.tokenIdHash,
223-
AuthenticationType.oidc,
224-
isSelected: true,
225-
accountId: currentAccount.accountId,
226-
apiUrl: currentAccount.apiUrl,
227-
userName: currentAccount.userName
228-
);
229-
await _accountCacheManager.setCurrentAccount(personalAccount);
187+
final personalAccount = PersonalAccount(
188+
tokenOIDC.tokenIdHash,
189+
AuthenticationType.oidc,
190+
isSelected: true,
191+
accountId: currentAccount.accountId,
192+
apiUrl: currentAccount.apiUrl,
193+
userName: currentAccount.userName
194+
);
195+
await _accountCacheManager.setCurrentAccount(personalAccount);
230196

231-
return personalAccount;
197+
return personalAccount;
198+
} catch (e) {
199+
logError('AuthorizationInterceptors::_updateCurrentAccount: Exception = $e');
200+
return null;
201+
}
232202
}
233203

234204
Future<TokenOIDC?> _getTokenInKeychain(TokenOIDC currentTokenOidc) async {
@@ -275,6 +245,67 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
275245
return _invokeRefreshTokenFromServer();
276246
}
277247

248+
Future<TokenOIDC?> _refreshToken() async {
249+
log('AuthorizationInterceptors::onError: Perform get New Token');
250+
251+
final newToken = PlatformInfo.isIOS
252+
? await _getNewTokenForIOSPlatform()
253+
: await _getNewTokenForOtherPlatform();
254+
255+
// Skip if duplicated
256+
if (newToken.token == _token?.token) {
257+
log('AuthorizationInterceptors::onError: Token duplicated');
258+
return null;
259+
}
260+
261+
_updateNewToken(newToken);
262+
263+
final account = await _updateCurrentAccount(tokenOIDC: newToken);
264+
if (PlatformInfo.isIOS && account != null) {
265+
await _iosSharingManager.saveKeyChainSharingSession(account);
266+
}
267+
268+
return newToken;
269+
}
270+
271+
Future<Response<dynamic>> _retryNormalRequest(RequestOptions options) async {
272+
log('AuthorizationInterceptors::_retryNormalRequest: Retry request with TokenId = ${_token?.tokenIdHash}');
273+
return _dio.fetch(options);
274+
}
275+
276+
Future<Response<dynamic>> _retryUploadRequest(
277+
RequestOptions options,
278+
Map<String, dynamic> extra,
279+
) async {
280+
log('AuthorizationInterceptors::_retryUploadRequest: Retry upload request with TokenId = ${_token?.tokenIdHash}');
281+
final uploadExtra = extra[FileUploader.uploadAttachmentExtraKey];
282+
final newOptions = Options(
283+
method: options.method,
284+
headers: options.headers,
285+
);
286+
287+
return _dio.request(
288+
options.path,
289+
data: _getDataUploadRequest(uploadExtra),
290+
queryParameters: options.queryParameters,
291+
options: newOptions,
292+
);
293+
}
294+
295+
Future<void> _handleRetryException(
296+
DioError err,
297+
ErrorInterceptorHandler handler,
298+
Object e,
299+
) async {
300+
logError('AuthorizationInterceptors::_handleRetryException:Exception: $e');
301+
final chainedError = ChainedRequestError(
302+
requestOptions: err.requestOptions,
303+
primaryError: err,
304+
secondaryError: e,
305+
);
306+
return super.onError(chainedError, handler);
307+
}
308+
278309
void clear() {
279310
_authorization = null;
280311
_token = null;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'package:dio/dio.dart';
2+
3+
class ChainedRequestError extends DioError {
4+
/// Error from the first request
5+
final DioError primaryError;
6+
7+
/// Error from the second request (retry/refresh)
8+
final Object? secondaryError;
9+
10+
ChainedRequestError({
11+
required super.requestOptions,
12+
required this.primaryError,
13+
this.secondaryError,
14+
});
15+
}

lib/main/exceptions/remote_exception.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ abstract class RemoteException with EquatableMixin implements Exception {
99
static const internalServerError = 'Internal Server Error';
1010
static const noNetworkError = 'No network error';
1111
static const badCredentials = 'Bad credentials';
12+
static const badResponse = 'Bad response';
1213
static const socketException = 'Socket exception';
1314

1415
final Object? message;
@@ -28,6 +29,10 @@ class UnknownError extends RemoteException {
2829
const UnknownError({int? code, Object? message}) : super(code: code, message: message);
2930
}
3031

32+
class BadResponseException extends RemoteException {
33+
const BadResponseException({String? message}) : super(message: message ?? RemoteException.badResponse);
34+
}
35+
3136
class ConnectionError extends RemoteException {
3237
const ConnectionError({String? message}) : super(message: message ?? RemoteException.connectionError);
3338
}
@@ -62,4 +67,14 @@ class CannotCalculateChangesMethodResponseException extends MethodLevelErrors {
6267

6368
class NoNetworkError extends RemoteException {
6469
const NoNetworkError() : super(message: RemoteException.noNetworkError);
70+
}
71+
72+
class ClientAuthenticationException extends RemoteException {
73+
final String? secondErrorCode;
74+
75+
const ClientAuthenticationException({
76+
super.code,
77+
super.message,
78+
this.secondErrorCode,
79+
});
6580
}

0 commit comments

Comments
 (0)