TF-3278 Handle open app via deep link at MailboxDashboard screen

This commit is contained in:
dab246
2024-11-22 23:30:37 +07:00
committed by Dat H. Pham
parent 8022559bc2
commit 223fdbf1fb
59 changed files with 1058 additions and 429 deletions
+31 -30
View File
@@ -1,19 +1,7 @@
import 'dart:async';
import 'package:core/core.dart';
import 'package:flutter/services.dart' as services;
import 'package:contact/contact/model/capability_contact.dart';
import 'package:core/data/network/config/dynamic_url_interceptors.dart';
import 'package:core/domain/exceptions/platform_exception.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/app_toast.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/toast/tmail_toast.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/application_manager.dart';
import 'package:core/utils/fps_manager.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:fcm/model/firebase_capability.dart';
import 'package:fcm/model/firebase_registration_id.dart';
@@ -34,7 +22,9 @@ import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixi
import 'package:tmail_ui_user/features/caching/caching_manager.dart';
import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart';
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
@@ -203,6 +193,8 @@ abstract class BaseController extends GetxController
clearDataAndGoToLoginPage();
}
void onCancelReconnectWhenSessionExpired() {}
void _handleConnectionErrorException() {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
@@ -241,7 +233,9 @@ abstract class BaseController extends GetxController
outsideDismissible: false,
titleActionButtonMaxLines: 1,
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
onConfirmAction: _executeBeforeReconnectAndLogOut);
onConfirmAction: _executeBeforeReconnectAndLogOut,
onCancelAction: onCancelReconnectWhenSessionExpired
);
} else if (PlatformInfo.isMobile) {
if (currentContext == null) {
clearDataAndGoToLoginPage();
@@ -257,7 +251,9 @@ abstract class BaseController extends GetxController
outsideDismissible: false,
titleActionButtonMaxLines: 1,
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
onConfirmAction: clearDataAndGoToLoginPage);
onConfirmAction: clearDataAndGoToLoginPage,
onCancelAction: onCancelReconnectWhenSessionExpired
);
}
}
@@ -477,20 +473,22 @@ abstract class BaseController extends GetxController
required Session session,
required AccountId accountId,
required Function onSuccessCallback,
required Function onFailureCallback,
required Function({Object? exception}) onFailureCallback,
}) async {
try {
_isFcmEnabled = _isFcmActivated(session, accountId);
if (isAuthenticatedWithOidc) {
final logoutViewState = await logoutOidcInteractor.execute().last;
logoutViewState.fold(
(failure) => onFailureCallback(),
(success) async {
if (success is LogoutOidcSuccess) {
logoutViewState.foldSuccess<LogoutOidcSuccess>(
onSuccess: (success) async {
await _handleDeleteFCMAndClearData();
onSuccessCallback();
},
onFailure: (failure) async {
if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) {
await _handleDeleteFCMAndClearData();
onSuccessCallback();
onFailureCallback(exception: UserCancelledLogoutOIDCFlowException());
} else {
onFailureCallback();
}
@@ -506,6 +504,11 @@ abstract class BaseController extends GetxController
}
}
bool _validateUserCancelledLogoutOidcFlow(dynamic exception) {
return exception is services.PlatformException &&
exception.code == OIDCConstant.endSessionFailedCode;
}
Future<void> _handleDeleteFCMAndClearData() async {
await Future.wait([
if (_isFcmEnabled)
@@ -519,14 +522,12 @@ abstract class BaseController extends GetxController
_getStoredFirebaseRegistrationInteractor = getBinding<GetStoredFirebaseRegistrationInteractor>();
final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last;
fcmRegistration?.fold(
(failure) => null,
(success) async {
if (success is GetStoredFirebaseRegistrationSuccess) {
_destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
}
fcmRegistration?.foldSuccess<GetStoredFirebaseRegistrationSuccess>(
onSuccess: (success) async {
_destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
},
onFailure: (failure) {},
);
} catch (e) {
logError('BaseController::_handleDeleteFCMRegistration:Exception = $e');
@@ -34,7 +34,7 @@ abstract class ReloadableController extends BaseController {
goToLogin();
} else if (failure is GetSessionFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure');
handleGetSessionFailure(failure.exception);
handleGetSessionFailure(failure);
} else if (failure is UpdateAccountCacheFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure');
_handleUpdateAccountCacheCompleted(
@@ -0,0 +1,12 @@
import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
class OidcConfigurationCacheClient extends HiveCacheClient<OidcConfigurationCache> {
@override
String get tableName => CachingConstants.oidcConfigurationCacheBoxName;
@override
bool get encryption => true;
}
@@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/login/data/local/encryption_key_cache_man
import 'package:tmail_ui_user/features/login/data/model/account_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/encryption_key_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/recent_login_username_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/token_oidc_cache.dart';
@@ -185,6 +186,10 @@ class HiveCacheConfig {
SessionHiveObjAdapter(),
CachingConstants.SESSION_HIVE_CACHE_ID
);
registerCacheAdapter<OidcConfigurationCache>(
OidcConfigurationCacheAdapter(),
CachingConstants.OIDC_CONFIGURATION_CACHE_ID,
);
}
void registerCacheAdapter<T>(TypeAdapter<T> typeAdapter, int typeId) {
@@ -19,6 +19,7 @@ class CachingConstants {
static const int DETAILED_EMAIL_HIVE_CACHE_ID = 17;
static const int SENDING_EMAIL_HIVE_CACHE_ID = 18;
static const int SESSION_HIVE_CACHE_ID = 19;
static const int OIDC_CONFIGURATION_CACHE_ID = 20;
static const String fcmCacheBoxName = 'fcm_cache_box';
static const String newEmailCacheBoxName = 'new_email_cache_box';
@@ -26,6 +27,9 @@ class CachingConstants {
static const String sendingEmailCacheBoxName = 'sending_email_cache_box';
static const String sessionCacheBoxName = 'session_cache_box';
static const String firebaseRegistrationCacheBoxName = 'firebase_registration_cache_box';
static const String oidcConfigurationCacheBoxName = 'oidc_configuration_cache_box';
static const String oidcConfigurationCacheKeyName = 'oidc_configuration_cache_key';
static const String newEmailsContentFolderName = 'new_emails';
static const String openedEmailContentFolderName = 'opened_email';
@@ -32,7 +32,7 @@ class AutoSignInViaDeepLinkInteractor {
await Future.wait([
_credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]);
await _accountRepository.setCurrentAccount(
@@ -6,6 +6,7 @@ import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/account/personal_account.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
@@ -21,9 +22,12 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_lo
import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart';
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart';
import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -65,7 +69,7 @@ class HomeController extends ReloadableController {
@override
void onReady() {
_cleanupCache();
_handleNavigateToScreen();
super.onReady();
}
@@ -76,6 +80,11 @@ class HomeController extends ReloadableController {
arguments: session);
}
@override
void onCancelReconnectWhenSessionExpired() {
clearDataAndGoToLoginPage();
}
void _initFlutterDownloader() {
FlutterDownloader
.initialize(debug: kDebugMode)
@@ -84,6 +93,15 @@ class HomeController extends ReloadableController {
static void downloadCallback(String id, DownloadTaskStatus status, int progress) {}
void _handleNavigateToScreen() {
final arguments = Get.arguments;
if (arguments is LoginNavigateArguments) {
_handleLoginNavigateArguments(arguments);
} else {
_cleanupCache();
}
}
Future<void> _cleanupCache() async {
await HiveCacheConfig.instance.onUpgradeDatabase(cachingManager);
@@ -95,6 +113,17 @@ class HomeController extends ReloadableController {
], eagerError: true).then((_) => getAuthenticatedAccountAction());
}
void _handleLoginNavigateArguments(LoginNavigateArguments arguments) {
switch (arguments.navigateType) {
case LoginNavigateType.autoSignIn:
_handleAutoSignInViaDeepLinkSuccess(arguments.autoSignInViaDeepLinkSuccess!);
break;
default:
_cleanupCache();
break;
}
}
void _registerReceivingFileSharing() {
_emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed();
}
@@ -124,7 +153,7 @@ class HomeController extends ReloadableController {
void _handleLogOutAndSignInNewAccount({
required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount,
required DeepLinkData deepLinkData,
required OpenAppDeepLinkData openAppDeepLinkData,
}) {
if (authenticationViewStateSuccess is GetCredentialViewState) {
setDataToInterceptors(
@@ -143,14 +172,14 @@ class HomeController extends ReloadableController {
_getSessionActionToLogOut(
authenticationViewStateSuccess: authenticationViewStateSuccess,
personalAccount: personalAccount,
deepLinkData: deepLinkData,
openAppDeepLinkData: openAppDeepLinkData,
);
}
Future<void> _getSessionActionToLogOut({
required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount,
required DeepLinkData deepLinkData,
required OpenAppDeepLinkData openAppDeepLinkData,
}) async {
try {
final sessionViewState = await getSessionInteractor.execute().last;
@@ -161,7 +190,7 @@ class HomeController extends ReloadableController {
sessionViewStateSuccess: success,
authenticationViewStateSuccess: authenticationViewStateSuccess,
personalAccount: personalAccount,
deepLinkData: deepLinkData,
openAppDeepLinkData: openAppDeepLinkData,
),
);
} catch (e) {
@@ -174,18 +203,27 @@ class HomeController extends ReloadableController {
required Success sessionViewStateSuccess,
required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount,
required DeepLinkData deepLinkData,
required OpenAppDeepLinkData openAppDeepLinkData,
}) {
if (sessionViewStateSuccess is GetSessionSuccess) {
logoutToSignInNewAccount(
session: sessionViewStateSuccess.session,
accountId: personalAccount.accountId!,
onFailureCallback: () =>
_continueUsingTheApp(authenticationViewStateSuccess),
onFailureCallback: ({exception}) {
if (exception is UserCancelledLogoutOIDCFlowException) {
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
);
} else {
_continueUsingTheApp(authenticationViewStateSuccess);
}
},
onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink(
deepLinkData: deepLinkData,
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
),
);
} else {
@@ -234,8 +272,19 @@ class HomeController extends ReloadableController {
@override
void handleFailureViewState(Failure failure) {
if (_validateToHandleDeepLinksNotSignedIn(failure)) {
_deepLinksManager!.handleDeepLinksWhenAppTerminatedNotSignedIn(
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
_deepLinksManager!.handleDeepLinksWhenAppTerminated(
onSuccessCallback: (deepLinkData) {
if (deepLinkData is OpenAppDeepLinkData) {
_deepLinksManager!.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: false,
onFailureCallback: goToLogin,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess
);
} else {
goToLogin();
}
},
onFailureCallback: goToLogin,
);
} else {
@@ -248,14 +297,25 @@ class HomeController extends ReloadableController {
if (_validateToHandleDeepLinksSignedIn(success)) {
final personalAccount = _getPersonalAccountFromViewStateSuccess(success);
_deepLinksManager!.handleDeepLinksWhenAppTerminatedSignedIn(
username: personalAccount?.userName?.value,
_deepLinksManager!.handleDeepLinksWhenAppTerminated(
onSuccessCallback: (deepLinkData) {
if (deepLinkData is OpenAppDeepLinkData) {
_deepLinksManager!.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: true,
username: personalAccount?.userName,
onConfirmLogoutCallback: (openAppDeepLinkData) => _handleLogOutAndSignInNewAccount(
authenticationViewStateSuccess: success,
personalAccount: personalAccount!,
openAppDeepLinkData: openAppDeepLinkData,
),
onFailureCallback: () => _continueUsingTheApp(success),
);
} else {
_continueUsingTheApp(success);
}
},
onFailureCallback: () => _continueUsingTheApp(success),
onConfirmCallback: (deepLinkData) => _handleLogOutAndSignInNewAccount(
authenticationViewStateSuccess: success,
personalAccount: personalAccount!,
deepLinkData: deepLinkData,
),
);
} else {
super.handleSuccessViewState(success);
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCDataSource {
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
Future<void> persistAuthorityOidc(String authority);
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
Future<void> deleteAuthorityOidc();
Future<void> deleteOidcConfiguration();
Future<OIDCConfiguration> getStoredOidcConfiguration();
@@ -74,9 +74,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
}
@override
Future<void> persistAuthorityOidc(String authority) {
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
return Future.sync(() async {
return await _oidcConfigurationCacheManager.persistAuthorityOidc(authority);
return await _oidcConfigurationCacheManager.persistOidcConfiguration(oidcConfiguration);
}).catchError(_cacheExceptionThrower.throwException);
}
@@ -106,9 +106,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
}
@override
Future<void> deleteAuthorityOidc() {
Future<void> deleteOidcConfiguration() {
return Future.sync(() async {
return await _oidcConfigurationCacheManager.deleteAuthorityOidc();
return await _oidcConfigurationCacheManager.deleteOidcConfiguration();
}).catchError(_exceptionThrower.throwException);
}
@@ -0,0 +1,16 @@
import 'package:model/oidc/oidc_configuration.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
extension OidcConfigutationCacheExtension on OidcConfigurationCache {
OIDCConfiguration toOIDCConfiguration() {
return OIDCConfiguration(
authority: authority,
isTWP: isTWP,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes,
);
}
}
@@ -1,34 +1,54 @@
import 'package:core/utils/app_logger.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tmail_ui_user/features/caching/clients/oidc_configuration_cache_client.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
import 'package:tmail_ui_user/features/login/data/extensions/oidc_configutation_cache_extension.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
class OidcConfigurationCacheManager {
final SharedPreferences _sharedPreferences;
final OidcConfigurationCacheClient _oidcConfigurationCacheClient;
OidcConfigurationCacheManager(this._sharedPreferences);
OidcConfigurationCacheManager(this._sharedPreferences, this._oidcConfigurationCacheClient);
Future<OIDCConfiguration> getOidcConfiguration() async {
final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc);
if (authority == null || authority.isEmpty) {
throw CanNotFoundOIDCAuthority();
} else {
final oidcConfigurationCache = await _oidcConfigurationCacheClient.getItem(CachingConstants.oidcConfigurationCacheKeyName);
if (oidcConfigurationCache == null) {
final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc);
if (authority == null || authority.isEmpty) {
throw CanNotFoundOIDCAuthority();
}
return OIDCConfiguration(
authority: authority,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes);
authority: authority,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes,
);
} else {
return oidcConfigurationCache.toOIDCConfiguration();
}
}
Future<void> persistAuthorityOidc(String authority) async {
log('OidcConfigurationCacheManager::persistAuthorityOidc(): $authority');
await _sharedPreferences.setString(OIDCConstant.keyAuthorityOidc, authority);
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) async {
log('OidcConfigurationCacheManager::persistOidcConfiguration(): $oidcConfiguration');
await _oidcConfigurationCacheClient.insertItem(
CachingConstants.oidcConfigurationCacheKeyName,
oidcConfiguration.toOidcConfigurationCache(),
);
}
Future<void> deleteAuthorityOidc() async {
log('OidcConfigurationCacheManager::deleteAuthorityOidc()');
await _sharedPreferences.remove(OIDCConstant.keyAuthorityOidc);
Future<void> deleteOidcConfiguration() async {
log('OidcConfigurationCacheManager::deleteOidcConfiguration()');
await Future.wait([
_oidcConfigurationCacheClient.deleteItem(
CachingConstants.oidcConfigurationCacheKeyName,
),
_sharedPreferences.remove(OIDCConstant.keyAuthorityOidc),
]);
}
}
@@ -0,0 +1,20 @@
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
part 'oidc_configuration_cache.g.dart';
@HiveType(typeId: CachingConstants.OIDC_CONFIGURATION_CACHE_ID)
class OidcConfigurationCache extends HiveObject with EquatableMixin {
@HiveField(0)
final String authority;
@HiveField(1)
final bool isTWP;
OidcConfigurationCache(this.authority, this.isTWP);
@override
List<Object?> get props => [authority, isTWP];
}
@@ -4,13 +4,17 @@ import 'package:tmail_ui_user/main/utils/app_config.dart';
class OIDCConstant {
static String get mobileOidcClientId => 'teammail-mobile';
static List<String> get oidcScope => ['openid', 'profile', 'email', 'offline_access'];
static const keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
static const authResponseKey = "auth_info";
static const String keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
static const String authResponseKey = "auth_info";
static const String twakeWorkplaceUrlScheme = 'twakemail.mobile';
static const String twakeWorkplaceRedirectUrl = '$twakeWorkplaceUrlScheme://redirect';
static const String appParameter = 'tmail';
static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url';
static const String postLoginRedirectUrlPathParams = 'post_login_redirect_url';
static const String endSessionFailedCode = 'end_session_failed';
static const String redirectOidcMobile = 'teammail.mobile://oauthredirect';
static const String loginRedirectOidcWeb = 'login-callback.html';
static const String logoutRedirectOidcWeb = 'logout-callback.html';
static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId;
}
@@ -52,11 +52,14 @@ class OIDCHttpClient {
if (oidcResponse.links.isEmpty) {
throw CanNotFoundOIDCAuthority();
}
log('OIDCHttpClient::getOIDCConfiguration(): href: ${oidcResponse.links[0].href}');
final authority = oidcResponse.links[0].href.toString();
final isTWP = authority == AppConfig.saasRegistrationUrl;
log('OIDCHttpClient::getOIDCConfiguration():authority: $authority');
return OIDCConfiguration(
authority: oidcResponse.links[0].href.toString(),
authority: authority,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes
scopes: AppConfig.oidcScopes,
isTWP: isTWP,
);
}
@@ -22,7 +22,6 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
return _oidcDataSource.getOIDCConfiguration(oidcResponse);
}
@override
Future<OIDCDiscoveryResponse> discoverOIDC(OIDCConfiguration oidcConfiguration) {
return _oidcDataSource.discoverOIDC(oidcConfiguration);
@@ -49,8 +48,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
}
@override
Future<void> persistAuthorityOidc(String authority) {
return _oidcDataSource.persistAuthorityOidc(authority);
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
return _oidcDataSource.persistOidcConfiguration(oidcConfiguration);
}
@override
@@ -75,8 +74,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
}
@override
Future<void> deleteAuthorityOidc() {
return _oidcDataSource.deleteAuthorityOidc();
Future<void> deleteOidcConfiguration() {
return _oidcDataSource.deleteOidcConfiguration();
}
@override
@@ -0,0 +1 @@
class UserCancelledLogoutOIDCFlowException implements Exception {}
@@ -4,6 +4,7 @@ import 'package:core/data/network/config/service_path.dart';
import 'package:core/utils/platform_info.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
@@ -12,32 +13,29 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
String get redirectUrl {
if (PlatformInfo.isWeb) {
return AppConfig.domainRedirectUrl.endsWith('/')
? AppConfig.domainRedirectUrl + loginRedirectOidcWeb
: '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb';
? AppConfig.domainRedirectUrl + OIDCConstant.loginRedirectOidcWeb
: '${AppConfig.domainRedirectUrl}/${OIDCConstant.loginRedirectOidcWeb}';
} else {
return _isSaasAuthority(authority)
return isTWP
? OIDCConstant.twakeWorkplaceRedirectUrl
: redirectOidcMobile;
: OIDCConstant.redirectOidcMobile;
}
}
String get logoutRedirectUrl {
if (PlatformInfo.isWeb) {
if (AppConfig.domainRedirectUrl.endsWith('/')) {
return AppConfig.domainRedirectUrl + logoutRedirectOidcWeb;
return AppConfig.domainRedirectUrl + OIDCConstant.logoutRedirectOidcWeb;
} else {
return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb';
return '${AppConfig.domainRedirectUrl}/${OIDCConstant.logoutRedirectOidcWeb}';
}
} else {
return _isSaasAuthority(authority)
return isTWP
? OIDCConstant.twakeWorkplaceRedirectUrl
: redirectOidcMobile;
: OIDCConstant.redirectOidcMobile;
}
}
bool _isSaasAuthority(String authority) =>
authority == AppConfig.saasRegistrationUrl;
String get signInTWPUrl => ServicePath(authority)
.withQueryParameters([
StringQueryParameter(
@@ -57,4 +55,8 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
StringQueryParameter('app', OIDCConstant.appParameter),
])
.generateEndpointPath();
OidcConfigurationCache toOidcConfigurationCache() {
return OidcConfigurationCache(authority, isTWP);
}
}
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCRepository {
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
Future<void> persistAuthorityOidc(String authority);
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
Future<void> deleteAuthorityOidc();
Future<void> deleteOidcConfiguration();
Future<OIDCConfiguration> getStoredOidcConfiguration();
@@ -17,7 +17,7 @@ class DeleteAuthorityOidcInteractor {
try {
await Future.wait([
_credentialRepository.removeBaseUrl(),
_authenticationOIDCRepository.deleteAuthorityOidc(),
_authenticationOIDCRepository.deleteOidcConfiguration(),
_authenticationOIDCRepository.deleteTokenOIDC(),
]);
return Right(DeleteAuthorityOidcSuccess());
@@ -15,7 +15,7 @@ class GetOIDCConfigurationInteractor {
try {
yield Right<Failure, Success>(GetOIDCConfigurationLoading());
final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(oidcResponse);
await _oidcRepository.persistAuthorityOidc(oidcConfiguration.authority);
await _oidcRepository.persistOidcConfiguration(oidcConfiguration);
yield Right<Failure, Success>(GetOIDCConfigurationSuccess(oidcConfiguration));
} catch (e) {
log('GetOIDCConfigurationInteractor::execute(): ERROR: $e');
@@ -35,7 +35,7 @@ class GetTokenOIDCInteractor {
await Future.wait([
_credentialRepository.saveBaseUrl(baseUrl),
authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
authenticationOIDCRepository.persistAuthorityOidc(config.authority),
authenticationOIDCRepository.persistOidcConfiguration(config),
]);
await _accountRepository.setCurrentAccount(
@@ -58,6 +58,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_
import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -163,6 +164,7 @@ class LoginController extends ReloadableController {
} else if (failure is GetAuthenticatedAccountFailure) {
_checkOIDCIsAvailable();
} else if (failure is GetSessionFailure) {
SmartDialog.dismiss();
clearAllData();
} else if (failure is DNSLookupToGetJmapUrlFailure) {
_username = null;
@@ -218,6 +220,7 @@ class LoginController extends ReloadableController {
_handleNoSuitableBrowserOIDC(failure)
.map((stillFailed) => _handleCommonOIDCFailure());
} else if (failure is GetSessionFailure) {
SmartDialog.dismiss();
clearAllData();
} else {
super.handleUrgentException(failure: failure, exception: exception);
@@ -234,12 +237,6 @@ class LoginController extends ReloadableController {
);
}
@override
void handleGetSessionFailure(GetSessionFailure failure) {
SmartDialog.dismiss();
super.handleGetSessionFailure(failure);
}
void _registerDeepLinks() {
_deepLinksManager = getBinding<DeepLinksManager>();
_deepLinksManager?.clearPendingDeepLinkData();
@@ -250,16 +247,21 @@ class LoginController extends ReloadableController {
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
if (deepLinkData == null) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
_deepLinksManager?.handleDeepLinksWhenAppRunning(
deepLinkData: deepLinkData,
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
onSuccessCallback: (deepLinkData) {
if (deepLinkData is! OpenAppDeepLinkData) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: deepLinkData,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
);
},
);
}
@@ -0,0 +1,29 @@
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart';
import 'package:tmail_ui_user/main/routes/router_arguments.dart';
class LoginNavigateArguments extends RouterArguments {
final LoginNavigateType navigateType;
final AutoSignInViaDeepLinkSuccess? autoSignInViaDeepLinkSuccess;
LoginNavigateArguments({
required this.navigateType,
this.autoSignInViaDeepLinkSuccess,
});
factory LoginNavigateArguments.autoSignIn({
required AutoSignInViaDeepLinkSuccess autoSignInViaDeepLinkSuccess,
}) {
return LoginNavigateArguments(
navigateType: LoginNavigateType.autoSignIn,
autoSignInViaDeepLinkSuccess: autoSignInViaDeepLinkSuccess,
);
}
@override
List<Object?> get props => [
navigateType,
autoSignInViaDeepLinkSuccess,
];
}
@@ -0,0 +1,3 @@
enum LoginNavigateType {
autoSignIn
}
@@ -8,6 +8,7 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart';
import 'package:email_recovery/email_recovery/email_recovery_action_id.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -74,9 +75,12 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_recovery_arguments.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/features/home/domain/usecases/store_session_interactor.dart';
import 'package:tmail_ui_user/features/identity_creator/domain/state/get_identity_cache_on_web_state.dart';
import 'package:tmail_ui_user/features/identity_creator/domain/usecase/get_identity_cache_on_web_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
@@ -157,6 +161,9 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_e
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
@@ -254,6 +261,8 @@ class MailboxDashBoardController extends ReloadableController
StreamSubscription? _receivingFileSharingStreamSubscription;
StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription;
bool _isFirstSessionLoad = false;
DeepLinksManager? _deepLinksManager;
StreamSubscription<DeepLinkData?>? _deepLinkDataStreamSubscription;
final StreamController<Either<Failure, Success>> _progressStateController =
StreamController<Either<Failure, Success>>.broadcast();
@@ -301,6 +310,7 @@ class MailboxDashBoardController extends ReloadableController
void onInit() {
if (PlatformInfo.isMobile) {
_registerReceivingFileSharingStream();
_registerDeepLinks();
}
_registerStreamListener();
BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard);
@@ -467,6 +477,8 @@ class MailboxDashBoardController extends ReloadableController
@override
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
SmartDialog.dismiss();
if (failure is SendEmailFailure && exception is NoNetworkError) {
_storeSendingEmailInCaseOfSendingFailureInMobile(failure);
} else {
@@ -563,6 +575,71 @@ class MailboxDashBoardController extends ReloadableController
);
}
void _registerDeepLinks() {
_deepLinksManager = getBinding<DeepLinksManager>();
_deepLinksManager?.clearPendingDeepLinkData();
_deepLinkDataStreamSubscription = _deepLinksManager
?.pendingDeepLinkData.stream
.listen(_handlePendingDeepLinkDataStream);
}
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('MailboxDashBoardController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
_deepLinksManager?.handleDeepLinksWhenAppRunning(
deepLinkData: deepLinkData,
onSuccessCallback: (deepLinkData) {
if (deepLinkData is! OpenAppDeepLinkData) return;
_deepLinksManager?.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: true,
username: sessionCurrent?.username,
onConfirmLogoutCallback: _handleLogOutAndSignInNewAccount,
);
},
);
}
void _handleLogOutAndSignInNewAccount(OpenAppDeepLinkData openAppDeepLinkData) {
if (sessionCurrent == null || accountId.value == null) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
logoutToSignInNewAccount(
session: sessionCurrent!,
accountId: accountId.value!,
onFailureCallback: ({exception}) {
if (exception is UserCancelledLogoutOIDCFlowException) {
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: SmartDialog.dismiss,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
);
} else {
SmartDialog.dismiss();
}
},
onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: SmartDialog.dismiss,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
),
);
}
void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) {
SmartDialog.dismiss();
pushAndPopAll(
AppRoutes.home,
arguments: LoginNavigateArguments.autoSignIn(
autoSignInViaDeepLinkSuccess: success,
),
);
}
void _registerPendingCurrentEmailIdInNotification() {
_iosNotificationManager = getBinding<IOSNotificationManager>();
_currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager
@@ -1625,6 +1702,8 @@ class MailboxDashBoardController extends ReloadableController
@override
void handleReloaded(Session session) {
log('MailboxDashBoardController::handleReloaded():');
SmartDialog.dismiss();
_getRouteParameters();
_setUpComponentsFromSession(session);
if (PlatformInfo.isWeb) {
@@ -3144,6 +3223,7 @@ class MailboxDashBoardController extends ReloadableController
_pendingSharedFileInfoSubscription?.cancel();
_receivingFileSharingStreamSubscription?.cancel();
_emailReceiveManager.closeEmailReceiveManagerStream();
_deepLinkDataStreamSubscription?.cancel();
}
_progressStateController.close();
_refreshActionEventController.close();
@@ -17,6 +17,7 @@ extension KeychainSharingSessionExtension on KeychainSharingSession {
tokenEndpoint: tokenEndpoint,
oidcScopes: oidcScopes,
mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification,
isTWP: isTWP,
);
}
}
@@ -27,6 +27,7 @@ class KeychainSharingSession with EquatableMixin {
String? tokenEndpoint;
List<String>? oidcScopes;
List<MailboxId>? mailboxIdsBlockNotification;
bool isTWP;
KeychainSharingSession({
required this.accountId,
@@ -40,6 +41,7 @@ class KeychainSharingSession with EquatableMixin {
this.tokenEndpoint,
this.oidcScopes,
this.mailboxIdsBlockNotification,
this.isTWP = false,
});
factory KeychainSharingSession.fromJson(Map<String, dynamic> json) => _$KeychainSharingSessionFromJson(json);
@@ -59,5 +61,6 @@ class KeychainSharingSession with EquatableMixin {
tokenEndpoint,
oidcScopes,
mailboxIdsBlockNotification,
isTWP,
];
}
@@ -36,7 +36,7 @@ class SignInTwakeWorkplaceInteractor {
await Future.wait([
_credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]);
await _accountRepository.setCurrentAccount(
@@ -36,7 +36,7 @@ class SignUpTwakeWorkplaceInteractor {
await Future.wait([
_credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]);
await _accountRepository.setCurrentAccount(
@@ -1,4 +1,3 @@
import 'package:core/utils/platform_info.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart';
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
@@ -38,9 +37,6 @@ class TwakeWelcomeBindings extends BaseBindings {
Get.find<AccountRepository>(),
Get.find<CredentialRepository>(),
));
if (PlatformInfo.isMobile) {
}
}
@override
@@ -23,6 +23,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twak
import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -61,16 +62,21 @@ class TwakeWelcomeController extends ReloadableController {
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('TwakeWelcomeController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
if (deepLinkData == null) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
_deepLinksManager?.handleDeepLinksWhenAppRunning(
deepLinkData: deepLinkData,
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
onSuccessCallback: (deepLinkData) {
if (deepLinkData is! OpenAppDeepLinkData) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: deepLinkData,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
);
},
);
}
@@ -107,7 +113,8 @@ class TwakeWelcomeController extends ReloadableController {
oidcConfiguration: OIDCConfiguration(
authority: AppConfig.saasRegistrationUrl,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes
scopes: AppConfig.oidcScopes,
isTWP: true,
)
));
}
@@ -127,7 +134,8 @@ class TwakeWelcomeController extends ReloadableController {
oidcConfiguration: OIDCConfiguration(
authority: AppConfig.saasRegistrationUrl,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes
scopes: AppConfig.oidcScopes,
isTWP: true,
)
));
}
@@ -178,6 +186,12 @@ class TwakeWelcomeController extends ReloadableController {
toastManager.showMessageFailure(failure);
}
@override
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
SmartDialog.dismiss();
super.handleUrgentExceptionOnMobile(failure: failure, exception: exception);
}
void _synchronizeTokenAndGetSession({
required Uri baseUri,
required TokenOIDC tokenOIDC,