diff --git a/core/lib/core.dart b/core/lib/core.dart index bfe0664e63..6245d16725 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -16,6 +16,7 @@ export 'presentation/extensions/string_extension.dart'; export 'presentation/extensions/tap_down_details_extension.dart'; export 'domain/extensions/media_type_extension.dart'; export 'presentation/extensions/map_extensions.dart'; +export 'presentation/extensions/either_view_state_extension.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; @@ -50,6 +51,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart'; export 'utils/list_utils.dart'; export 'utils/mail/domain.dart'; export 'utils/mail/mail_address.dart'; +export 'utils/application_manager.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/presentation/extensions/either_view_state_extension.dart b/core/lib/presentation/extensions/either_view_state_extension.dart new file mode 100644 index 0000000000..59fc3d6cb1 --- /dev/null +++ b/core/lib/presentation/extensions/either_view_state_extension.dart @@ -0,0 +1,16 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; + +typedef OnFailureCallback = void Function(Failure? failure); +typedef OnSuccessCallback = void Function(T success); + +extension EitherViewStateExtension on Either { + void foldSuccess({ + required OnSuccessCallback onSuccess, + required OnFailureCallback onFailure, + }) { + fold(onFailure, + (success) => success is T ? onSuccess(success as T) : onFailure(null)); + } +} diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart index 3042a1c9b6..a025322cbb 100644 --- a/core/lib/utils/string_convert.dart +++ b/core/lib/utils/string_convert.dart @@ -1,10 +1,23 @@ +import 'dart:convert'; + +import 'package:core/utils/app_logger.dart'; + class StringConvert { - static String? writeEmptyToNull(String text) { + static String? writeEmptyToNull(String text) { if (text.isEmpty) return null; return text; } - static String writeNullToEmpty(String? text) { + static String writeNullToEmpty(String? text) { return text ?? ''; } + + static String decodeBase64ToString(String text) { + try { + return utf8.decode(base64Decode(text)); + } catch (e) { + logError('StringConvert::decodeBase64ToString:Exception = $e'); + return text; + } + } } diff --git a/core/test/presentation/extensions/either_view_state_extension_test.dart b/core/test/presentation/extensions/either_view_state_extension_test.dart new file mode 100644 index 0000000000..5ec15e910d --- /dev/null +++ b/core/test/presentation/extensions/either_view_state_extension_test.dart @@ -0,0 +1,79 @@ +import 'package:core/presentation/extensions/either_view_state_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:dartz/dartz.dart'; + +class MockFailure extends Failure { + final String message; + + MockFailure(this.message); + + @override + List get props => [message]; +} + +class MockSuccess extends Success { + final String data; + + MockSuccess(this.data); + + @override + List get props => [data]; +} + +class AnotherSuccess extends Success { + @override + List get props => []; +} + +void main() { + group('EitherViewStateExtension::foldSuccess::test', () { + test('Should calls onFailure when Either is Left', () { + final either = Left(MockFailure('Error occurred')); + bool failureCalled = false; + + either.foldSuccess( + onSuccess: (_) => fail('onSuccess should not be called'), + onFailure: (failure) { + failureCalled = true; + expect(failure, isNotNull); + expect(failure, isA()); + }, + ); + + expect(failureCalled, isTrue); + }); + + test('Should calls onSuccess when Either is Right with matching type', () { + final either = Right(MockSuccess('Successful')); + bool successCalled = false; + + either.foldSuccess( + onSuccess: (success) { + successCalled = true; + expect(success, isNotNull); + expect(success, isA()); + }, + onFailure: (_) => fail('onFailure should not be called'), + ); + + expect(successCalled, isTrue); + }); + + test('Should calls onFailure when Either is Right with non-matching type', () { + final either = Right(MockSuccess('Successful')); + bool failureCalled = false; + + either.foldSuccess( + onSuccess: (_) => fail('onSuccess should not be called'), + onFailure: (failure) { + failureCalled = true; + expect(failure, isNull); + }, + ); + + expect(failureCalled, isTrue); + }); + }); +} diff --git a/core/test/utils/string_convert_test.dart b/core/test/utils/string_convert_test.dart new file mode 100644 index 0000000000..2c8d727f51 --- /dev/null +++ b/core/test/utils/string_convert_test.dart @@ -0,0 +1,40 @@ +import 'package:core/utils/string_convert.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('StringConvert::decodeBase64ToString::test', () { + test('should decode a valid Base64 string to a normal string', () { + // Arrange + const base64Encoded = 'SGVsbG8gV29ybGQh'; + const expectedDecoded = 'Hello World!'; + + // Act + final result = StringConvert.decodeBase64ToString(base64Encoded); + + // Assert + expect(result, expectedDecoded); + }); + + test('should return the original string for invalid Base64 input', () { + // Arrange + const invalidBase64 = 'InvalidBase64@@'; + + // Act + final result = StringConvert.decodeBase64ToString(invalidBase64); + + // Assert + expect(result, invalidBase64); + }); + + test('should return the original string for empty input', () { + // Arrange + const emptyInput = ''; + + // Act + final result = StringConvert.decodeBase64ToString(emptyInput); + + // Assert + expect(result, emptyInput); + }); + }); +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cd8c1e4fa6..e27075f4dc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - app_links (0.0.2): + - Flutter - app_settings (5.1.1): - Flutter - AppAuth (1.7.4): @@ -204,6 +206,7 @@ PODS: - Flutter DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) - app_settings (from `.symlinks/plugins/app_settings/ios`) - better_open_file (from `.symlinks/plugins/better_open_file/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) @@ -264,6 +267,8 @@ SPEC REPOS: - UniversalDetector2 EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" app_settings: :path: ".symlinks/plugins/app_settings/ios" better_open_file: @@ -334,6 +339,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: + app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc AppAuth: 182c5b88630569df5acb672720534756c29b3358 better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7f5658898a..9fad3f0dfc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -33,6 +33,7 @@ ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) teammail.mobile + twakemail.mobile mailto diff --git a/ios/TwakeCore/Jmap/JmapClient.swift b/ios/TwakeCore/Jmap/JmapClient.swift index b2c9173b9e..5b73696940 100644 --- a/ios/TwakeCore/Jmap/JmapClient.swift +++ b/ios/TwakeCore/Jmap/JmapClient.swift @@ -24,6 +24,7 @@ class JmapClient { basicAuth: String?, tokenEndpointUrl: String?, oidcScopes: [String]?, + isTWP: Bool?, onComplete: @escaping ([Email], [Error]) -> Void ) { if (authenticationType == AuthenticationType.basic) { @@ -42,7 +43,8 @@ class JmapClient { tokenRefreshManager = TokenRefreshManager( refreshToken: tokenOidc?.refreshToken ?? "", tokenEndpoint: tokenEndpointUrl ?? "", - scopes: oidcScopes + scopes: oidcScopes, + isTWP: isTWP ) } diff --git a/ios/TwakeCore/Network/TokenRefreshManager.swift b/ios/TwakeCore/Network/TokenRefreshManager.swift index 8f73a1dc5c..c655bed31d 100644 --- a/ios/TwakeCore/Network/TokenRefreshManager.swift +++ b/ios/TwakeCore/Network/TokenRefreshManager.swift @@ -5,6 +5,7 @@ class TokenRefreshManager { private let MOBIE_CLIENT_ID = "teammail-mobile" private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect" private let OIDC_SCOPES = ["openid", "profile", "email", "offline_access"] + private let TWP_MOBIE_REDIRECT_URL = "twakemail.mobile://redirect" private let GRANT_TYPE = "grant_type" private let REFRESH_TOKEN = "refresh_token" @@ -15,11 +16,13 @@ class TokenRefreshManager { let refreshToken: String let tokenEndpoint: String let scopes: [String]? + let isTWP: Bool? - init(refreshToken: String, tokenEndpoint: String, scopes: [String]?) { + init(refreshToken: String, tokenEndpoint: String, scopes: [String]?, isTWP: Bool?) { self.refreshToken = refreshToken self.tokenEndpoint = tokenEndpoint self.scopes = scopes + self.isTWP = isTWP } private func getScopes() -> String { @@ -28,6 +31,14 @@ class TokenRefreshManager { } return OIDC_SCOPES.joined(separator: " ") } + + private func getRedirectUrl() -> String { + if (isTWP == true) { + return TWP_MOBIE_REDIRECT_URL + } else { + return MOBIE_REDIRECT_URL + } + } func handleRefreshAccessToken( onSuccess: @escaping (TokenResponse) -> Void, @@ -41,7 +52,7 @@ class TokenRefreshManager { let params = [ CLIENT_ID: MOBIE_CLIENT_ID, GRANT_TYPE: REFRESH_TOKEN, - REDIRECT_URI: MOBIE_REDIRECT_URL, + REDIRECT_URI: getRedirectUrl(), REFRESH_TOKEN: refreshToken, SCOPES: getScopes(), ] diff --git a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift index 4ac23ce332..0f99aa31d7 100644 --- a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift +++ b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift @@ -12,6 +12,7 @@ struct KeychainSharingSession: Codable { let tokenEndpoint: String? let oidcScopes: [String]? let mailboxIdsBlockNotification: [String]? + let isTWP: Bool? } extension KeychainSharingSession { @@ -27,7 +28,8 @@ extension KeychainSharingSession { basicAuth: self.basicAuth, tokenEndpoint: self.tokenEndpoint, oidcScopes: self.oidcScopes, - mailboxIdsBlockNotification: self.mailboxIdsBlockNotification + mailboxIdsBlockNotification: self.mailboxIdsBlockNotification, + isTWP: self.isTWP ) } @@ -48,7 +50,8 @@ extension KeychainSharingSession { basicAuth: self.basicAuth, tokenEndpoint: self.tokenEndpoint, oidcScopes: self.oidcScopes, - mailboxIdsBlockNotification: self.mailboxIdsBlockNotification + mailboxIdsBlockNotification: self.mailboxIdsBlockNotification, + isTWP: self.isTWP ) } diff --git a/ios/TwakeMailNSE/NotificationService.swift b/ios/TwakeMailNSE/NotificationService.swift index 5388ac1a15..fae9d5f0eb 100644 --- a/ios/TwakeMailNSE/NotificationService.swift +++ b/ios/TwakeMailNSE/NotificationService.swift @@ -82,6 +82,7 @@ class NotificationService: UNNotificationServiceExtension { basicAuth: keychainSharingSession.basicAuth, tokenEndpointUrl: keychainSharingSession.tokenEndpoint, oidcScopes: keychainSharingSession.oidcScopes, + isTWP: keychainSharingSession.isTWP, onComplete: { (emails, errors) in do { if emails.isEmpty { diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 13173b53b5..e1e2ea2c6d 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -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'; @@ -216,6 +206,8 @@ abstract class BaseController extends GetxController clearDataAndGoToLoginPage(); } + void onCancelReconnectWhenSessionExpired() {} + void _handleConnectionErrorException() { if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( @@ -254,7 +246,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(); @@ -270,7 +264,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 + ); } } @@ -476,20 +472,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( + onSuccess: (success) async { + await _handleDeleteFCMAndClearData(); + onSuccessCallback(); + }, + onFailure: (failure) async { + if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) { await _handleDeleteFCMAndClearData(); - onSuccessCallback(); + onFailureCallback(exception: UserCancelledLogoutOIDCFlowException()); } else { onFailureCallback(); } @@ -505,6 +503,11 @@ abstract class BaseController extends GetxController } } + bool _validateUserCancelledLogoutOidcFlow(dynamic exception) { + return exception is services.PlatformException && + exception.code == OIDCConstant.endSessionFailedCode; + } + Future _handleDeleteFCMAndClearData() async { await Future.wait([ if (_isFcmEnabled) @@ -518,14 +521,12 @@ abstract class BaseController extends GetxController _getStoredFirebaseRegistrationInteractor = getBinding(); final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last; - fcmRegistration?.fold( - (failure) => null, - (success) async { - if (success is GetStoredFirebaseRegistrationSuccess) { - _destroyFirebaseRegistrationInteractor = getBinding(); - await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last; - } + fcmRegistration?.foldSuccess( + onSuccess: (success) async { + _destroyFirebaseRegistrationInteractor = getBinding(); + await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last; }, + onFailure: (failure) {}, ); } catch (e) { logError('BaseController::_handleDeleteFCMRegistration:Exception = $e'); diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index f530e7f9aa..4179617f5b 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -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( diff --git a/lib/features/caching/clients/oidc_configuration_cache_client.dart b/lib/features/caching/clients/oidc_configuration_cache_client.dart new file mode 100644 index 0000000000..2fcef1bac5 --- /dev/null +++ b/lib/features/caching/clients/oidc_configuration_cache_client.dart @@ -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 { + + @override + String get tableName => CachingConstants.oidcConfigurationCacheBoxName; + + @override + bool get encryption => true; +} \ No newline at end of file diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index 5ae303e82d..ea8df229ad 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -19,6 +19,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'; @@ -187,6 +188,10 @@ class HiveCacheConfig { SessionHiveObjAdapter(), CachingConstants.SESSION_HIVE_CACHE_ID ); + registerCacheAdapter( + OidcConfigurationCacheAdapter(), + CachingConstants.OIDC_CONFIGURATION_CACHE_ID, + ); } void registerCacheAdapter(TypeAdapter typeAdapter, int typeId) { diff --git a/lib/features/caching/utils/caching_constants.dart b/lib/features/caching/utils/caching_constants.dart index 4e1260cc84..4db4af99f5 100644 --- a/lib/features/caching/utils/caching_constants.dart +++ b/lib/features/caching/utils/caching_constants.dart @@ -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'; diff --git a/lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart b/lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart index 90ade7c86b..5b4f431621 100644 --- a/lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart +++ b/lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart @@ -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( diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 52ff01bf0a..1f02396fdb 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -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 _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 _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); diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart index d64a642430..9642c12bf3 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -16,9 +16,9 @@ abstract class AuthenticationOIDCDataSource { Future getStoredTokenOIDC(String tokenIdHash); - Future persistAuthorityOidc(String authority); + Future persistOidcConfiguration(OIDCConfiguration oidcConfiguration); - Future deleteAuthorityOidc(); + Future deleteOidcConfiguration(); Future getStoredOidcConfiguration(); diff --git a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart index 7f5a83b91a..1c9e135fce 100644 --- a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart @@ -74,9 +74,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { } @override - Future persistAuthorityOidc(String authority) { + Future 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 deleteAuthorityOidc() { + Future deleteOidcConfiguration() { return Future.sync(() async { - return await _oidcConfigurationCacheManager.deleteAuthorityOidc(); + return await _oidcConfigurationCacheManager.deleteOidcConfiguration(); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/login/data/extensions/oidc_configutation_cache_extension.dart b/lib/features/login/data/extensions/oidc_configutation_cache_extension.dart new file mode 100644 index 0000000000..d8438d2dc1 --- /dev/null +++ b/lib/features/login/data/extensions/oidc_configutation_cache_extension.dart @@ -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, + ); + } +} \ No newline at end of file diff --git a/lib/features/login/data/local/oidc_configuration_cache_manager.dart b/lib/features/login/data/local/oidc_configuration_cache_manager.dart index 88ed6988f8..bde2cd2d41 100644 --- a/lib/features/login/data/local/oidc_configuration_cache_manager.dart +++ b/lib/features/login/data/local/oidc_configuration_cache_manager.dart @@ -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 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 persistAuthorityOidc(String authority) async { - log('OidcConfigurationCacheManager::persistAuthorityOidc(): $authority'); - await _sharedPreferences.setString(OIDCConstant.keyAuthorityOidc, authority); + Future persistOidcConfiguration(OIDCConfiguration oidcConfiguration) async { + log('OidcConfigurationCacheManager::persistOidcConfiguration(): $oidcConfiguration'); + await _oidcConfigurationCacheClient.insertItem( + CachingConstants.oidcConfigurationCacheKeyName, + oidcConfiguration.toOidcConfigurationCache(), + ); } - Future deleteAuthorityOidc() async { - log('OidcConfigurationCacheManager::deleteAuthorityOidc()'); - await _sharedPreferences.remove(OIDCConstant.keyAuthorityOidc); + Future deleteOidcConfiguration() async { + log('OidcConfigurationCacheManager::deleteOidcConfiguration()'); + await Future.wait([ + _oidcConfigurationCacheClient.deleteItem( + CachingConstants.oidcConfigurationCacheKeyName, + ), + _sharedPreferences.remove(OIDCConstant.keyAuthorityOidc), + ]); } } \ No newline at end of file diff --git a/lib/features/login/data/model/oidc_configuration_cache.dart b/lib/features/login/data/model/oidc_configuration_cache.dart new file mode 100644 index 0000000000..4ba5afac7c --- /dev/null +++ b/lib/features/login/data/model/oidc_configuration_cache.dart @@ -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 get props => [authority, isTWP]; +} \ No newline at end of file diff --git a/lib/features/login/data/network/config/oidc_constant.dart b/lib/features/login/data/network/config/oidc_constant.dart index 2e4790c58b..bc2a78d99a 100644 --- a/lib/features/login/data/network/config/oidc_constant.dart +++ b/lib/features/login/data/network/config/oidc_constant.dart @@ -4,13 +4,17 @@ import 'package:tmail_ui_user/main/utils/app_config.dart'; class OIDCConstant { static String get mobileOidcClientId => 'teammail-mobile'; static List 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; } \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart index 214a75594e..1d14c38421 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -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, ); } diff --git a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart index 1d79b8cfbf..a8ad608d84 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -22,7 +22,6 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { return _oidcDataSource.getOIDCConfiguration(oidcResponse); } - @override Future discoverOIDC(OIDCConfiguration oidcConfiguration) { return _oidcDataSource.discoverOIDC(oidcConfiguration); @@ -49,8 +48,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { } @override - Future persistAuthorityOidc(String authority) { - return _oidcDataSource.persistAuthorityOidc(authority); + Future persistOidcConfiguration(OIDCConfiguration oidcConfiguration) { + return _oidcDataSource.persistOidcConfiguration(oidcConfiguration); } @override @@ -75,8 +74,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { } @override - Future deleteAuthorityOidc() { - return _oidcDataSource.deleteAuthorityOidc(); + Future deleteOidcConfiguration() { + return _oidcDataSource.deleteOidcConfiguration(); } @override diff --git a/lib/features/login/domain/exceptions/logout_exception.dart b/lib/features/login/domain/exceptions/logout_exception.dart new file mode 100644 index 0000000000..92b7d4b923 --- /dev/null +++ b/lib/features/login/domain/exceptions/logout_exception.dart @@ -0,0 +1 @@ +class UserCancelledLogoutOIDCFlowException implements Exception {} \ No newline at end of file diff --git a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart index 9e4b60e79d..0a05942ef8 100644 --- a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart +++ b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart @@ -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); + } } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_oidc_repository.dart b/lib/features/login/domain/repository/authentication_oidc_repository.dart index 11fa3fee3d..0148dd6a00 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -16,9 +16,9 @@ abstract class AuthenticationOIDCRepository { Future getStoredTokenOIDC(String tokenIdHash); - Future persistAuthorityOidc(String authority); + Future persistOidcConfiguration(OIDCConfiguration oidcConfiguration); - Future deleteAuthorityOidc(); + Future deleteOidcConfiguration(); Future getStoredOidcConfiguration(); diff --git a/lib/features/login/domain/usecases/delete_authority_oidc_interactor.dart b/lib/features/login/domain/usecases/delete_authority_oidc_interactor.dart index 958ffb46a2..b3f22abeda 100644 --- a/lib/features/login/domain/usecases/delete_authority_oidc_interactor.dart +++ b/lib/features/login/domain/usecases/delete_authority_oidc_interactor.dart @@ -17,7 +17,7 @@ class DeleteAuthorityOidcInteractor { try { await Future.wait([ _credentialRepository.removeBaseUrl(), - _authenticationOIDCRepository.deleteAuthorityOidc(), + _authenticationOIDCRepository.deleteOidcConfiguration(), _authenticationOIDCRepository.deleteTokenOIDC(), ]); return Right(DeleteAuthorityOidcSuccess()); diff --git a/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart b/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart index a2d9458a9c..7143582633 100644 --- a/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart +++ b/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart @@ -15,7 +15,7 @@ class GetOIDCConfigurationInteractor { try { yield Right(GetOIDCConfigurationLoading()); final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(oidcResponse); - await _oidcRepository.persistAuthorityOidc(oidcConfiguration.authority); + await _oidcRepository.persistOidcConfiguration(oidcConfiguration); yield Right(GetOIDCConfigurationSuccess(oidcConfiguration)); } catch (e) { log('GetOIDCConfigurationInteractor::execute(): ERROR: $e'); diff --git a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart index b04ef8b931..9eab8e3e01 100644 --- a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart +++ b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart @@ -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( diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 19f993b8a6..b53af0a4b2 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -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?.clearPendingDeepLinkData(); @@ -250,16 +247,21 @@ class LoginController extends ReloadableController { void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) { log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) return; + _deepLinksManager?.handleDeepLinksWhenAppRunning( + deepLinkData: deepLinkData, + onSuccessCallback: (deepLinkData) { + if (deepLinkData is! OpenAppDeepLinkData) return; - if (currentContext != null) { - SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait); - } + if (currentContext != null) { + SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait); + } - _deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn( - deepLinkData: deepLinkData, - onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, - onFailureCallback: SmartDialog.dismiss, + _deepLinksManager?.autoSignInViaDeepLink( + openAppDeepLinkData: deepLinkData, + onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + onFailureCallback: SmartDialog.dismiss, + ); + }, ); } diff --git a/lib/features/login/presentation/model/login_navigate_arguments.dart b/lib/features/login/presentation/model/login_navigate_arguments.dart new file mode 100644 index 0000000000..9f922652c2 --- /dev/null +++ b/lib/features/login/presentation/model/login_navigate_arguments.dart @@ -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 get props => [ + navigateType, + autoSignInViaDeepLinkSuccess, + ]; +} diff --git a/lib/features/login/presentation/model/login_navigate_type.dart b/lib/features/login/presentation/model/login_navigate_type.dart new file mode 100644 index 0000000000..88aba5ee46 --- /dev/null +++ b/lib/features/login/presentation/model/login_navigate_type.dart @@ -0,0 +1,3 @@ +enum LoginNavigateType { + autoSignIn +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index c0781ac35e..04b83736e9 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -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'; @@ -72,9 +73,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'; @@ -152,6 +156,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'; @@ -246,6 +253,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo StreamSubscription? _pendingSharedFileInfoSubscription; StreamSubscription? _receivingFileSharingStreamSubscription; StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; + DeepLinksManager? _deepLinksManager; + StreamSubscription? _deepLinkDataStreamSubscription; final StreamController> _progressStateController = StreamController>.broadcast(); @@ -289,6 +298,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo void onInit() { if (PlatformInfo.isMobile) { _registerReceivingFileSharingStream(); + _registerDeepLinks(); } _registerStreamListener(); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); @@ -438,6 +448,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo @override void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) { + SmartDialog.dismiss(); + if (failure is SendEmailFailure && exception is NoNetworkError) { _storeSendingEmailInCaseOfSendingFailureInMobile(failure); } else { @@ -534,6 +546,71 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo ); } + void _registerDeepLinks() { + _deepLinksManager = getBinding(); + _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(); _currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager @@ -1473,6 +1550,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo @override void handleReloaded(Session session) { log('MailboxDashBoardController::handleReloaded():'); + SmartDialog.dismiss(); + _getRouteParameters(); _setUpComponentsFromSession(session); if (PlatformInfo.isWeb) { @@ -2940,6 +3019,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo _pendingSharedFileInfoSubscription?.cancel(); _receivingFileSharingStreamSubscription?.cancel(); _emailReceiveManager.closeEmailReceiveManagerStream(); + _deepLinkDataStreamSubscription?.cancel(); } _progressStateController.close(); _refreshActionEventController.close(); diff --git a/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart b/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart index d2eb1bd2ae..c4919fbef6 100644 --- a/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart +++ b/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart @@ -17,6 +17,7 @@ extension KeychainSharingSessionExtension on KeychainSharingSession { tokenEndpoint: tokenEndpoint, oidcScopes: oidcScopes, mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification, + isTWP: isTWP, ); } } \ No newline at end of file diff --git a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart index a8d8391754..3ff6ab50fa 100644 --- a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart +++ b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart @@ -27,6 +27,7 @@ class KeychainSharingSession with EquatableMixin { String? tokenEndpoint; List? oidcScopes; List? 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 json) => _$KeychainSharingSessionFromJson(json); @@ -59,5 +61,6 @@ class KeychainSharingSession with EquatableMixin { tokenEndpoint, oidcScopes, mailboxIdsBlockNotification, + isTWP, ]; } \ No newline at end of file diff --git a/lib/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart b/lib/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart index 5c47df90d2..9baf4d3efa 100644 --- a/lib/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart +++ b/lib/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart @@ -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( diff --git a/lib/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart b/lib/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart index 1460fdf595..76919a0fa4 100644 --- a/lib/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart +++ b/lib/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart @@ -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( diff --git a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_bindings.dart b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_bindings.dart index 2fbe8bafc5..e8f4955f40 100644 --- a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_bindings.dart +++ b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_bindings.dart @@ -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(), Get.find(), )); - if (PlatformInfo.isMobile) { - - } } @override diff --git a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart index 969a49b52a..dd48e2a25d 100644 --- a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart +++ b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart @@ -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, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 51d22613c0..a4896cfef3 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-12-05T18:34:48.830922", + "@@last_modified": "2024-12-09T13:59:58.460897", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4175,5 +4175,23 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "switchAccountConfirmation": "Switch Account Confirmation", + "@switchAccountConfirmation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreCurrentlyLoggedInWith": "You are currently logged in with", + "@youAreCurrentlyLoggedInWith": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "doYouWantToLogOutAndSwitchTo": "Do you want to log out and switch to", + "@doYouWantToLogOutAndSwitchTo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/deep_link/deep_link_bindings.dart b/lib/main/bindings/deep_link/deep_link_bindings.dart index 8887bd1f4b..ccee79f782 100644 --- a/lib/main/bindings/deep_link/deep_link_bindings.dart +++ b/lib/main/bindings/deep_link/deep_link_bindings.dart @@ -15,6 +15,6 @@ class DeepLinkBindings extends Bindings { Get.find(), Get.find(), )); - Get.put(DeepLinksManager(Get.find())); + Get.put(DeepLinksManager()); } } \ No newline at end of file diff --git a/lib/main/bindings/local/local_bindings.dart b/lib/main/bindings/local/local_bindings.dart index ae3a6faee0..c44ecf98a7 100644 --- a/lib/main/bindings/local/local_bindings.dart +++ b/lib/main/bindings/local/local_bindings.dart @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/caching/clients/firebase_registration_cac import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart'; import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/oidc_configuration_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; @@ -70,7 +71,8 @@ class LocalBindings extends Bindings { Get.put(EncryptionKeyCacheManager(Get.find())); Get.put(AuthenticationInfoCacheClient()); Get.put(AuthenticationInfoCacheManager(Get.find())); - Get.put(OidcConfigurationCacheManager(Get.find())); + Get.put(OidcConfigurationCacheClient()); + Get.put(OidcConfigurationCacheManager(Get.find(), Get.find())); Get.put(LanguageCacheManager(Get.find())); Get.put(RecentLoginUrlCacheClient()); Get.put(RecentLoginUrlCacheManager((Get.find()))); diff --git a/lib/main/deep_links/deep_link_action_define.dart b/lib/main/deep_links/deep_link_action_define.dart deleted file mode 100644 index 877a3bc148..0000000000 --- a/lib/main/deep_links/deep_link_action_define.dart +++ /dev/null @@ -1,9 +0,0 @@ - -import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart'; -import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; - -typedef OnDeepLinkSuccessCallback = Function(AutoSignInViaDeepLinkSuccess success); - -typedef OnDeepLinkFailureCallback = Function(); - -typedef OnDeepLinkConfirmLogoutCallback = Function(DeepLinkData deepLinkData); \ No newline at end of file diff --git a/lib/main/deep_links/deep_link_action_type.dart b/lib/main/deep_links/deep_link_action_type.dart new file mode 100644 index 0000000000..9a6528b1d6 --- /dev/null +++ b/lib/main/deep_links/deep_link_action_type.dart @@ -0,0 +1,5 @@ + +enum DeepLinkActionType { + openApp, + unknown; +} \ No newline at end of file diff --git a/lib/main/deep_links/deep_link_callback_action_define.dart b/lib/main/deep_links/deep_link_callback_action_define.dart new file mode 100644 index 0000000000..1e90a7cac1 --- /dev/null +++ b/lib/main/deep_links/deep_link_callback_action_define.dart @@ -0,0 +1,11 @@ + +import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; + +typedef OnAutoSignInViaDeepLinkSuccessCallback = Function(AutoSignInViaDeepLinkSuccess success); + +typedef OnDeepLinkSuccessCallback = Function(DeepLinkData deepLinkData); + +typedef OnDeepLinkFailureCallback = Function(); + +typedef OnDeepLinkConfirmLogoutCallback = Function(T t); \ No newline at end of file diff --git a/lib/main/deep_links/deep_link_data.dart b/lib/main/deep_links/deep_link_data.dart index 7a9b37cea0..40d9c924ed 100644 --- a/lib/main/deep_links/deep_link_data.dart +++ b/lib/main/deep_links/deep_link_data.dart @@ -1,45 +1,11 @@ import 'package:equatable/equatable.dart'; -import 'package:model/model.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart'; class DeepLinkData with EquatableMixin { - final String action; - final String? accessToken; - final String? refreshToken; - final String? idToken; - final int? expiresIn; - final String? username; + final DeepLinkActionType actionType; - DeepLinkData({ - required this.action, - this.accessToken, - this.refreshToken, - this.idToken, - this.expiresIn, - this.username, - }); - - bool isValidToken() => accessToken?.isNotEmpty == true && username?.isNotEmpty == true; - - TokenOIDC getTokenOIDC() { - final expiredTime = expiresIn == null - ? null - : DateTime.now().add(Duration(seconds: expiresIn!)); - - return TokenOIDC( - accessToken!, - TokenId(idToken ?? ''), - refreshToken ?? '', - expiredTime: expiredTime, - ); - } + DeepLinkData({required this.actionType}); @override - List get props => [ - action, - accessToken, - refreshToken, - idToken, - expiresIn, - username, - ]; + List get props => [actionType]; } diff --git a/lib/main/deep_links/deep_links_manager.dart b/lib/main/deep_links/deep_links_manager.dart index f70a04b379..3016a8f4a4 100644 --- a/lib/main/deep_links/deep_links_manager.dart +++ b/lib/main/deep_links/deep_links_manager.dart @@ -1,24 +1,15 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:flutter/material.dart'; -import 'package:model/oidc/oidc_configuration.dart'; import 'package:rxdart/subjects.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.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/auto_sign_in_via_deep_link_interactor.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; -import 'package:tmail_ui_user/main/deep_links/deep_link_action_define.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_callback_action_define.dart'; import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; +import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_handler_mixin.dart'; -class DeepLinksManager with MessageDialogActionMixin { - - final AutoSignInViaDeepLinkInteractor _autoSignInViaDeepLinkInteractor; +class DeepLinksManager with MessageDialogActionMixin, OpenAppDeepLinkHandlerMixin { BehaviorSubject _pendingDeepLinkData = BehaviorSubject.seeded(null); @@ -26,8 +17,6 @@ class DeepLinksManager with MessageDialogActionMixin { StreamSubscription? _deepLinkStreamSubscription; - DeepLinksManager(this._autoSignInViaDeepLinkInteractor); - Future getDeepLinkData() async { final uriLink = await AppLinks().getInitialLink(); log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink'); @@ -63,194 +52,47 @@ class DeepLinksManager with MessageDialogActionMixin { DeepLinkData? parseDeepLink(String url) { try { - final updatedUrl = url.replaceFirst( - OIDCConstant.twakeWorkplaceUrlScheme, - 'https', - ); - final uri = Uri.parse(updatedUrl); + final decodedUrl = Uri.decodeFull(url); + final uri = Uri.parse(decodedUrl); log('DeepLinksManager::parseDeepLink:uri = $uri'); final action = uri.host; - final accessToken = uri.queryParameters['access_token']; - final refreshToken = uri.queryParameters['refresh_token']; - final idToken = uri.queryParameters['id_token']; - final expiresInStr = uri.queryParameters['expires_in']; - final username = uri.queryParameters['username']; - - final expiresIn = expiresInStr != null - ? int.tryParse(expiresInStr) - : null; - return DeepLinkData( - action: action, - accessToken: accessToken, - refreshToken: refreshToken, - idToken: idToken, - expiresIn: expiresIn, - username: username, - ); + if (action == DeepLinkActionType.openApp.name.toLowerCase()) { + return parseOpenAppDeepLink(uri); + } else { + return DeepLinkData(actionType: DeepLinkActionType.unknown); + } } catch (e) { logError('DeepLinksManager::parseDeepLink:Exception = $e'); return null; } } - Future handleDeepLinksWhenAppOnForegroundNotSignedIn({ - required DeepLinkData deepLinkData, - required OnDeepLinkSuccessCallback onSuccessCallback, - OnDeepLinkFailureCallback? onFailureCallback, - }) async { - log('DeepLinksManager::handleDeepLinksWhenAppOnForegroundNotSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - _handleOpenApp( - deepLinkData: deepLinkData, - onFailureCallback: onFailureCallback, - onSuccessCallback: onSuccessCallback, - ); - } else { - onFailureCallback?.call(); - } - } - - Future handleDeepLinksWhenAppTerminatedNotSignedIn({ + Future handleDeepLinksWhenAppTerminated({ required OnDeepLinkSuccessCallback onSuccessCallback, required OnDeepLinkFailureCallback onFailureCallback, }) async { final deepLinkData = await getDeepLinkData(); - log('DeepLinksManager::handleDeepLinksWhenAppTerminatedNotSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { - onFailureCallback(); - return; - } - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - _handleOpenApp( - deepLinkData: deepLinkData, - onFailureCallback: onFailureCallback, - onSuccessCallback: onSuccessCallback, - ); - } else { - onFailureCallback(); - } - } - - Future handleDeepLinksWhenAppTerminatedSignedIn({ - required String? username, - required OnDeepLinkConfirmLogoutCallback onConfirmCallback, - required OnDeepLinkFailureCallback onFailureCallback, - }) async { - final deepLinkData = await getDeepLinkData(); - log('DeepLinksManager::handleDeepLinksWhenAppTerminatedSignedIn:DeepLinkData = $deepLinkData'); if (deepLinkData == null) { onFailureCallback(); return; } - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - if (deepLinkData.username?.isNotEmpty != true || username?.isNotEmpty != true) { - onFailureCallback(); - return; - } - - if (deepLinkData.username == username || currentContext == null) { - onFailureCallback(); - } else { - _showConfirmDialogSwitchAccount( - context: currentContext!, - username: username!, - onConfirmAction: () => onConfirmCallback(deepLinkData), - onCancelAction: onFailureCallback, - ); - } - } else { - onFailureCallback(); - } - } - - void _handleOpenApp({ - required DeepLinkData deepLinkData, - required OnDeepLinkSuccessCallback onSuccessCallback, - OnDeepLinkFailureCallback? onFailureCallback, - }) { - autoSignInViaDeepLink( - deepLinkData: deepLinkData, - onFailureCallback: onFailureCallback, - onSuccessCallback: onSuccessCallback, - ); + onSuccessCallback(deepLinkData); } - Future autoSignInViaDeepLink({ - required DeepLinkData deepLinkData, + Future handleDeepLinksWhenAppRunning({ + required DeepLinkData? deepLinkData, required OnDeepLinkSuccessCallback onSuccessCallback, OnDeepLinkFailureCallback? onFailureCallback, }) async { - try { - if (deepLinkData.isValidToken()) { - final autoSignInViewState = await _autoSignInViaDeepLinkInteractor.execute( - baseUri: Uri.parse(AppConfig.saasJmapServerUrl), - tokenOIDC: deepLinkData.getTokenOIDC(), - oidcConfiguration: OIDCConfiguration( - authority: AppConfig.saasRegistrationUrl, - clientId: OIDCConstant.clientId, - scopes: AppConfig.oidcScopes, - ), - ).last; - - autoSignInViewState.fold( - (failure) => onFailureCallback?.call(), - (success) { - if (success is AutoSignInViaDeepLinkSuccess) { - onSuccessCallback(success); - } else { - onFailureCallback?.call(); - } - }, - ); - } else { - onFailureCallback?.call(); - } - } catch (e) { - logError('DeepLinksManager::_autoSignInViaDeepLink:Exception = $e'); + if (deepLinkData == null) { onFailureCallback?.call(); + return; } - } - - void _showConfirmDialogSwitchAccount({ - required BuildContext context, - required String username, - required Function onConfirmAction, - required Function onCancelAction, - }) { - final appLocalizations = AppLocalizations.of(context); - showConfirmDialogAction( - context, - '', - appLocalizations.yesLogout, - title: appLocalizations.logoutConfirmation, - alignCenter: true, - outsideDismissible: false, - titleActionButtonMaxLines: 1, - titlePadding: const EdgeInsetsDirectional.only(start: 24, top: 24, end: 24, bottom: 12), - messageStyle: const TextStyle( - color: AppColor.colorTextBody, - fontSize: 15, - fontWeight: FontWeight.w400, - ), - listTextSpan: [ - TextSpan(text: appLocalizations.messageConfirmationLogout), - TextSpan( - text: ' $username', - style: const TextStyle( - color: AppColor.colorTextBody, - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - const TextSpan(text: ' ?'), - ], - onConfirmAction: onConfirmAction, - onCancelAction: onCancelAction, - ); + onSuccessCallback(deepLinkData); } void dispose() { diff --git a/lib/main/deep_links/open_app_deep_link_data.dart b/lib/main/deep_links/open_app_deep_link_data.dart new file mode 100644 index 0000000000..894fca5454 --- /dev/null +++ b/lib/main/deep_links/open_app_deep_link_data.dart @@ -0,0 +1,72 @@ +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_id.dart'; +import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; + +class OpenAppDeepLinkData extends DeepLinkData { + final String registrationUrl; + final String jmapUrl; + final String username; + final String accessToken; + final String? refreshToken; + final String? idToken; + final int? expiresIn; + + OpenAppDeepLinkData({ + super.actionType = DeepLinkActionType.openApp, + required this.registrationUrl, + required this.jmapUrl, + required this.username, + required this.accessToken, + this.refreshToken, + this.idToken, + this.expiresIn, + }); + + bool isValidAuthentication() => + accessToken.isNotEmpty && + username.isNotEmpty && + registrationUrl.isNotEmpty && + jmapUrl.isNotEmpty; + + DateTime? _getExpiredTime() { + return expiresIn != null + ? DateTime.now().add(Duration(seconds: expiresIn!)) + : null; + } + + bool isLoggedInWith(String currentUsername) => username == currentUsername; + + TokenOIDC get tokenOIDC { + return TokenOIDC( + accessToken, + TokenId(idToken ?? ''), + refreshToken ?? '', + expiredTime: _getExpiredTime(), + ); + } + + Uri get baseUri => Uri.parse(jmapUrl); + + OIDCConfiguration get oidcConfiguration => OIDCConfiguration( + authority: registrationUrl, + clientId: OIDCConstant.clientId, + scopes: AppConfig.oidcScopes, + isTWP: true, + ); + + @override + List get props => [ + ...super.props, + registrationUrl, + jmapUrl, + username, + accessToken, + refreshToken, + idToken, + expiresIn, + ]; +} diff --git a/lib/main/deep_links/open_app_deep_link_handler_mixin.dart b/lib/main/deep_links/open_app_deep_link_handler_mixin.dart new file mode 100644 index 0000000000..f96d1b51d7 --- /dev/null +++ b/lib/main/deep_links/open_app_deep_link_handler_mixin.dart @@ -0,0 +1,163 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/either_view_state_extension.dart'; +import 'package:core/utils/string_convert.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:rich_text_composer/views/commons/logger.dart'; +import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.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/auto_sign_in_via_deep_link_interactor.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_callback_action_define.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/route_navigation.dart'; + +mixin OpenAppDeepLinkHandlerMixin on MessageDialogActionMixin { + OpenAppDeepLinkData? parseOpenAppDeepLink(Uri uri) { + try { + final accessToken = uri.queryParameters['access_token'] ?? ''; + final refreshToken = uri.queryParameters['refresh_token']; + final idToken = uri.queryParameters['id_token']; + final expiresInStr = uri.queryParameters['expires_in']; + final username = uri.queryParameters['username']; + final registrationUrl = uri.queryParameters['registrationUrl'] ?? ''; + final jmapUrl = uri.queryParameters['jmapUrl'] ?? ''; + + final expiresIn = expiresInStr != null + ? int.tryParse(expiresInStr) + : null; + + final usernameDecoded = username?.isNotEmpty == true + ? StringConvert.decodeBase64ToString(username!) + : username ?? ''; + + return OpenAppDeepLinkData( + registrationUrl: registrationUrl, + jmapUrl: jmapUrl, + accessToken: accessToken, + refreshToken: refreshToken, + idToken: idToken, + expiresIn: expiresIn, + username: usernameDecoded, + ); + } catch (e) { + logError('DeepLinksManager::parseOpenAppDeepLink:Exception = $e'); + return null; + } + } + + void handleOpenAppDeepLinks({ + required OpenAppDeepLinkData openAppDeepLinkData, + OnDeepLinkFailureCallback? onFailureCallback, + OnAutoSignInViaDeepLinkSuccessCallback? onAutoSignInSuccessCallback, + OnDeepLinkConfirmLogoutCallback? onConfirmLogoutCallback, + UserName? username, + bool isSignedIn = true, + }) { + if (!openAppDeepLinkData.isValidAuthentication()) { + onFailureCallback?.call(); + return; + } + + if (isSignedIn) { + if (currentContext == null || username == null) { + onFailureCallback?.call(); + return; + } + + if (openAppDeepLinkData.isLoggedInWith(username.value)) { + onFailureCallback?.call(); + return; + } + + _showConfirmDialogSwitchAccount( + context: currentContext!, + currentUsername: username.value, + newUsername: openAppDeepLinkData.username, + onConfirmAction: () => onConfirmLogoutCallback?.call(openAppDeepLinkData), + onCancelAction: () => onFailureCallback?.call() + ); + } else { + autoSignInViaDeepLink( + openAppDeepLinkData: openAppDeepLinkData, + onAutoSignInSuccessCallback: (viewState) => onAutoSignInSuccessCallback?.call(viewState), + onFailureCallback: () => onFailureCallback?.call(), + ); + } + } + + Future autoSignInViaDeepLink({ + required OpenAppDeepLinkData openAppDeepLinkData, + required OnAutoSignInViaDeepLinkSuccessCallback onAutoSignInSuccessCallback, + required OnDeepLinkFailureCallback onFailureCallback, + }) async { + try { + final autoSignInViaDeepLinkInteractor = Get.find(); + + final autoSignInViewState = await autoSignInViaDeepLinkInteractor.execute( + baseUri: openAppDeepLinkData.baseUri, + tokenOIDC: openAppDeepLinkData.tokenOIDC, + oidcConfiguration: openAppDeepLinkData.oidcConfiguration, + ).last; + + autoSignInViewState.foldSuccess( + onSuccess: onAutoSignInSuccessCallback, + onFailure: (failure) => onFailureCallback.call(), + ); + } catch (e) { + logError('DeepLinksManager::_autoSignInViaDeepLink:Exception = $e'); + onFailureCallback.call(); + } + } + + void _showConfirmDialogSwitchAccount({ + required BuildContext context, + required String currentUsername, + required String newUsername, + required Function onConfirmAction, + required Function onCancelAction, + }) { + final appLocalizations = AppLocalizations.of(context); + + showConfirmDialogAction( + context, + '', + appLocalizations.yes, + title: appLocalizations.switchAccountConfirmation, + alignCenter: true, + outsideDismissible: false, + titleActionButtonMaxLines: 1, + titlePadding: const EdgeInsetsDirectional.only(start: 24, top: 24, end: 24, bottom: 12), + messageStyle: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + listTextSpan: [ + TextSpan(text: appLocalizations.youAreCurrentlyLoggedInWith), + TextSpan( + text: ' $currentUsername', + style: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan(text: '. '), + TextSpan(text: appLocalizations.doYouWantToLogOutAndSwitchTo), + TextSpan( + text: ' $newUsername', + style: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan(text: '?'), + ], + onConfirmAction: onConfirmAction, + onCancelAction: onCancelAction, + ); + } +} diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index ecd67c9ad8..7836d5ba84 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4380,4 +4380,25 @@ class AppLocalizations { name: 'messageConfirmationLogout', ); } + + String get switchAccountConfirmation { + return Intl.message( + 'Switch Account Confirmation', + name: 'switchAccountConfirmation', + ); + } + + String get youAreCurrentlyLoggedInWith { + return Intl.message( + 'You are currently logged in with', + name: 'youAreCurrentlyLoggedInWith', + ); + } + + String get doYouWantToLogOutAndSwitchTo { + return Intl.message( + 'Do you want to log out and switch to', + name: 'doYouWantToLogOutAndSwitchTo', + ); + } } diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index 92ba058964..f749c7c468 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -18,7 +18,6 @@ class AppConfig { static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy'; static const String saasRegistrationUrl = 'https://sign-up.twake.app'; static const String saasJmapServerUrl = 'https://jmap.twake.app'; - static const String openAppHostDeepLink = 'openApp'; static String get baseUrl => dotenv.get('SERVER_URL', fallback: ''); static String get domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: ''); diff --git a/lib/main/utils/ios_sharing_manager.dart b/lib/main/utils/ios_sharing_manager.dart index aad25f60bb..93b1ad2269 100644 --- a/lib/main/utils/ios_sharing_manager.dart +++ b/lib/main/utils/ios_sharing_manager.dart @@ -82,7 +82,7 @@ class IOSSharingManager { userName: personalAccount.userName! ); - final tokenRecords = await _getTokenEndpointAndScopes(); + final tokenRecords = await _getTokenEndpointScopesAndIsTWP(); final mailboxIdsBlockNotification = await _getMailboxIdsBlockNotification( accountId: personalAccount.accountId!, @@ -99,7 +99,9 @@ class IOSSharingManager { basicAuth: credentialInfo, tokenEndpoint: tokenRecords?.tokenEndpoint, oidcScopes: tokenRecords?.scopes, - mailboxIdsBlockNotification: mailboxIdsBlockNotification); + mailboxIdsBlockNotification: mailboxIdsBlockNotification, + isTWP: tokenRecords?.isTWP ?? false, + ); await _keychainSharingManager.save(keychainSharingSession); @@ -171,13 +173,14 @@ class IOSSharingManager { } } - Future<({String? tokenEndpoint, List? scopes})?> _getTokenEndpointAndScopes() async { + Future<({String? tokenEndpoint, List? scopes, bool isTWP})?> _getTokenEndpointScopesAndIsTWP() async { try { final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration(); final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig); return ( tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint, - scopes: oidcConfig.scopes + scopes: oidcConfig.scopes, + isTWP: oidcConfig.isTWP, ); } catch (e) { logError('IOSSharingManager::_getTokenEndpointAndScopes:Exception: $e'); diff --git a/model/lib/oidc/oidc_configuration.dart b/model/lib/oidc/oidc_configuration.dart index 37732695f0..80cc6302f0 100644 --- a/model/lib/oidc/oidc_configuration.dart +++ b/model/lib/oidc/oidc_configuration.dart @@ -2,26 +2,25 @@ import 'package:equatable/equatable.dart'; class OIDCConfiguration with EquatableMixin { - final redirectOidcMobile = 'teammail.mobile://oauthredirect'; - final wellKnownOpenId = '.well-known/openid-configuration'; - final loginRedirectOidcWeb = 'login-callback.html'; - final logoutRedirectOidcWeb = 'logout-callback.html'; + static const String _wellKnownOpenId = '.well-known/openid-configuration'; final String authority; final String clientId; final List scopes; + final bool isTWP; OIDCConfiguration({ required this.authority, required this.clientId, - required this.scopes + required this.scopes, + this.isTWP = false, }); String get discoveryUrl { if (authority.endsWith('/')) { - return authority + wellKnownOpenId; + return authority + _wellKnownOpenId; } else { - return '$authority/$wellKnownOpenId'; + return '$authority/$_wellKnownOpenId'; } } @@ -29,6 +28,7 @@ class OIDCConfiguration with EquatableMixin { List get props => [ authority, clientId, - scopes + scopes, + isTWP, ]; } diff --git a/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart b/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart index 152bc73d5e..4e150610e7 100644 --- a/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart +++ b/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart @@ -68,6 +68,7 @@ void main() { apiUrl: 'https://jmap.domain.com/oidc/jmap', tokenEndpoint: 'https://jmap.domain.com/oidc/jmap', oidcScopes: ['email'], + isTWP: true, emailState: 'ae08b34da40b48f30ec0b94', tokenOIDC: TokenOIDC( 'ae08b34da40b48f30ec0b94', @@ -88,6 +89,7 @@ void main() { expect(keychainSession?.emailState, equals('ae08b34da40b48f30ec0b94')); expect(keychainSession?.tokenEndpoint, equals('https://jmap.domain.com/oidc/jmap',)); expect(keychainSession?.oidcScopes, equals(['email'])); + expect(keychainSession?.isTWP, isTrue); }); }); } diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart deleted file mode 100644 index e55053db02..0000000000 --- a/test/main/deep_links/deep_links_manager.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_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'; - -class MockAutoSignInViaDeepLinkInteractor extends Mock implements AutoSignInViaDeepLinkInteractor {} - -void main() { - final mockAutoSignInViaDeepLinkInteractor = MockAutoSignInViaDeepLinkInteractor(); - final deepLinkManager = DeepLinksManager(mockAutoSignInViaDeepLinkInteractor); - - group('DeepLinksManager::parseDeepLink::test', () { - test('Valid deep link with multiple query parameters', () { - const deepLink = 'twake.mail://openApp?access_token=ey123456&refresh_token=ey7890&id_token=token&expires_in=3600&username=user@example.com'; - final expectedData = DeepLinkData( - action: 'openapp', - accessToken: 'ey123456', - refreshToken: 'ey7890', - idToken: 'token', - expiresIn: 3600, - username: 'user@example.com', - ); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); - - expect(deepLinkData, equals(expectedData)); - }); - - test('Deep link with no query parameters', () { - const deepLink = 'twake.mail://openApp'; - final expectedData = DeepLinkData(action: 'openapp'); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); - - expect(deepLinkData, expectedData); - }); - - test('Deep link with one query parameter', () { - const deepLink = 'twake.mail://openApp?access_token=ey123456'; - final expectedData = DeepLinkData(action: 'openapp', accessToken: 'ey123456',); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); - - expect(deepLinkData, equals(expectedData)); - }); - - test('Invalid deep link format', () { - const deepLink = 'Invalid link: invalid'; - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); - expect(deepLinkData, isNull); - }); - }); -} \ No newline at end of file diff --git a/test/main/deep_links/deep_links_manager_test.dart b/test/main/deep_links/deep_links_manager_test.dart new file mode 100644 index 0000000000..3ae0ac489d --- /dev/null +++ b/test/main/deep_links/deep_links_manager_test.dart @@ -0,0 +1,151 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; + +void main() { + final deepLinkManager = DeepLinksManager(); + + group('DeepLinksManager::test', () { + group('parseDeepLink::test', () { + test('SHOULD returns correct DeepLinkData for valid openApp deep link', () { + const url = 'twake://openApp/some-data'; + final result = deepLinkManager.parseDeepLink(url); + + expect(result, isNotNull); + expect(result?.actionType, DeepLinkActionType.openApp); + }); + + test('SHOULD returns DeepLinkData with unknown action for unhandled action', () { + const url = 'twake://unknownAction/some-data'; + final result = deepLinkManager.parseDeepLink(url); + + expect(result, isNotNull); + expect(result?.actionType, DeepLinkActionType.unknown); + }); + + test('SHOULD returns null for malformed URL', () { + const url = 'Invalid link: invalid'; + final result = deepLinkManager.parseDeepLink(url); + + expect(result, isNull); + }); + + test('SHOULD returns null for exception during parsing', () { + const url = 'twake://malformedurl%'; + final result = deepLinkManager.parseDeepLink(url); + + expect(result, isNull); + }); + }); + + group('parseOpenAppDeepLink::test', () { + test('SHOULD returns OpenAppDeepLinkData with all valid parameters', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&refresh_token=refresh123' + '&id_token=id123' + '&expires_in=3600' + '&username=dXNlcg==' + '®istrationUrl=https://registration.url' + '&jmapUrl=https://jmap.url', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.accessToken, 'token123'); + expect(result?.refreshToken, 'refresh123'); + expect(result?.idToken, 'id123'); + expect(result?.expiresIn, 3600); + expect(result?.username, 'user'); + expect(result?.registrationUrl, 'https://registration.url'); + expect(result?.jmapUrl, 'https://jmap.url'); + expect(result?.isValidAuthentication(), isTrue); + }); + + test('SHOULD returns OpenAppDeepLinkData with missing optional parameters', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&username=user@example.com' + '®istrationUrl=https://registration.url' + '&jmapUrl=https://jmap.url', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.accessToken, 'token123'); + expect(result?.refreshToken, isNull); + expect(result?.idToken, isNull); + expect(result?.expiresIn, isNull); + expect(result?.username, 'user@example.com'); + expect(result?.registrationUrl, 'https://registration.url'); + expect(result?.jmapUrl, 'https://jmap.url'); + expect(result?.isValidAuthentication(), isTrue); + }); + + + test('SHOULD returns OpenAppDeepLinkData with invalid expires_in', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&expires_in=not_a_number' + '®istrationUrl=https://registration.url' + '&jmapUrl=https://jmap.url', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.expiresIn, isNull); + }); + + test('SHOULD returns OpenAppDeepLinkData with origin username if Base64 decoding fails', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&username=invalid_base64' + '®istrationUrl=https://registration.url' + '&jmapUrl=https://jmap.url', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.username, 'invalid_base64'); + }); + + test('SHOULD returns OpenAppDeepLinkData with jmapUrl contains sub-path and port', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&refresh_token=refresh123' + '&id_token=id123' + '&expires_in=3600' + '&username=dXNlcg==' + '®istrationUrl=https://registration.url' + '&jmapUrl=https://jmap.url:1000/jmap', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.jmapUrl, 'https://jmap.url:1000/jmap'); + }); + + test('SHOULD returns OpenAppDeepLinkData with registrationUrl contains sub-path and port', () { + final uri = Uri.parse( + 'twake://openApp?access_token=token123' + '&refresh_token=refresh123' + '&id_token=id123' + '&expires_in=3600' + '&username=dXNlcg==' + '®istrationUrl=https://registration.url:1000/register' + '&jmapUrl=https://jmap.url', + ); + + final result = deepLinkManager.parseOpenAppDeepLink(uri); + + expect(result, isNotNull); + expect(result?.registrationUrl, 'https://registration.url:1000/register'); + }); + }); + }); +} \ No newline at end of file