diff --git a/lib/features/home/presentation/home_bindings.dart b/lib/features/home/presentation/home_bindings.dart index fe4ff19dc..2e75157d2 100644 --- a/lib/features/home/presentation/home_bindings.dart +++ b/lib/features/home/presentation/home_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/cleanup/data/datasource/cleanup_datasource.dart'; @@ -12,11 +11,8 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cac import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_url_cache_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_username_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; -import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart'; import 'package:tmail_ui_user/features/home/presentation/home_controller.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -58,13 +54,6 @@ class HomeBindings extends BaseBindings { Get.lazyPut(() => CleanupRecentLoginUrlCacheInteractor(Get.find())); Get.lazyPut(() => CleanupRecentLoginUsernameCacheInteractor(Get.find())); Get.lazyPut(() => CheckOIDCIsAvailableInteractor(Get.find())); - if (PlatformInfo.isMobile) { - Get.lazyPut(() => AutoSignInViaDeepLinkInteractor( - Get.find(), - Get.find(), - Get.find(), - )); - } } @override diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index ce661d701..52ff01bf0 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,16 +1,13 @@ import 'dart:async'; -import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/account/personal_account.dart'; -import 'package:model/oidc/oidc_configuration.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/cleanup_rule.dart'; @@ -24,8 +21,6 @@ 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/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/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'; @@ -33,7 +28,6 @@ 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'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; @@ -47,7 +41,6 @@ class HomeController extends ReloadableController { IOSNotificationManager? _iosNotificationManager; DeepLinksManager? _deepLinksManager; - AutoSignInViaDeepLinkInteractor? _autoSignInViaDeepLinkInteractor; HomeController( this._cleanupEmailCacheInteractor, @@ -113,65 +106,9 @@ class HomeController extends ReloadableController { void _registerDeepLinks() { _deepLinksManager = getBinding(); - _autoSignInViaDeepLinkInteractor = getBinding(); - } - - Future _handleDeepLinksNotSignedIn() async { - final deepLinkData = await _deepLinksManager?.getDeepLinkData(); - log('HomeController::_handleDeepLinksNotSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { - goToLogin(); - return; - } - - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - _handleOpenApp(deepLinkData); - } else { - goToLogin(); - } - } - - void _handleOpenApp(DeepLinkData deepLinkData) { - _autoSignInViaDeepLink(deepLinkData); - } - - Future _handleDeepLinksSignedIn(Success authenticationViewStateSuccess) async { - final deepLinkData = await _deepLinksManager?.getDeepLinkData(); - log('HomeController::_handleDeepLinksSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { - _continueUsingTheApp(authenticationViewStateSuccess); - return; - } - - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - final personalAccount = _getPersonalAccountFromViewStateSuccess(authenticationViewStateSuccess); - - if (deepLinkData.username?.isNotEmpty != true || - personalAccount?.userName?.value.isNotEmpty != true) { - _continueUsingTheApp(authenticationViewStateSuccess); - return; - } - - if (deepLinkData.username == personalAccount?.userName?.value || currentContext == null) { - _continueUsingTheApp(authenticationViewStateSuccess); - } else { - _showConfirmDialogSwitchAccount( - username: personalAccount!.userName!.value, - onConfirmAction: () => _handleLogOutAndSignInNewAccount( - authenticationViewStateSuccess: authenticationViewStateSuccess, - personalAccount: personalAccount, - deepLinkData: deepLinkData, - ), - onCancelAction: () => _continueUsingTheApp(authenticationViewStateSuccess) - ); - } - } else { - _continueUsingTheApp(authenticationViewStateSuccess); - } } void _continueUsingTheApp(Success authenticationViewStateSuccess) { - log('HomeController::_continueUsingTheApp:'); super.handleSuccessViewState(authenticationViewStateSuccess); } @@ -184,44 +121,6 @@ class HomeController extends ReloadableController { return null; } - void _showConfirmDialogSwitchAccount({ - required String username, - required Function onConfirmAction, - required Function onCancelAction, - }) { - final appLocalizations = AppLocalizations.of(currentContext!); - - showConfirmDialogAction( - currentContext!, - '', - 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 - ); - } - void _handleLogOutAndSignInNewAccount({ required Success authenticationViewStateSuccess, required PersonalAccount personalAccount, @@ -283,7 +182,11 @@ class HomeController extends ReloadableController { accountId: personalAccount.accountId!, onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess), - onSuccessCallback: () => _autoSignInViaDeepLink(deepLinkData), + onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink( + deepLinkData: deepLinkData, + onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess), + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + ), ); } else { _continueUsingTheApp(authenticationViewStateSuccess); @@ -303,44 +206,6 @@ class HomeController extends ReloadableController { ); } - void _autoSignInViaDeepLink(DeepLinkData deepLinkData) { - if (deepLinkData.isValidToken() && _autoSignInViaDeepLinkInteractor != null) { - consumeState(_autoSignInViaDeepLinkInteractor!.execute( - baseUri: Uri.parse(AppConfig.saasJmapServerUrl), - tokenOIDC: deepLinkData.getTokenOIDC(), - oidcConfiguration: OIDCConfiguration( - authority: AppConfig.saasRegistrationUrl, - clientId: OIDCConstant.clientId, - scopes: AppConfig.oidcScopes, - ), - )); - } else { - goToLogin(); - - if (currentContext == null || currentOverlayContext == null) { - return; - } - - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).tokenInvalid, - ); - } - } - - void _handleAutoSignInViaDeepLinkFailure() { - goToLogin(); - - if (currentContext == null || currentOverlayContext == null) { - return; - } - - appToast.showToastErrorMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).tokenInvalid, - ); - } - void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) { setDataToInterceptors( baseUrl: success.baseUri.toString(), @@ -350,12 +215,29 @@ class HomeController extends ReloadableController { getSessionAction(); } + bool _validateToHandleDeepLinksNotSignedIn(Failure failure) { + return PlatformInfo.isMobile && + isNotSignedIn(failure) && + _deepLinksManager != null; + } + + bool _validateToHandleDeepLinksSignedIn(Success success) { + final personalAccount = _getPersonalAccountFromViewStateSuccess(success); + + return PlatformInfo.isMobile && + (success is GetCredentialViewState || + success is GetStoredTokenOidcSuccess) && + personalAccount != null && + _deepLinksManager != null; + } + @override void handleFailureViewState(Failure failure) { - if (PlatformInfo.isMobile && isNotSignedIn(failure)) { - _handleDeepLinksNotSignedIn(); - } else if (failure is AutoSignInViaDeepLinkFailure) { - _handleAutoSignInViaDeepLinkFailure(); + if (_validateToHandleDeepLinksNotSignedIn(failure)) { + _deepLinksManager!.handleDeepLinksWhenAppTerminatedNotSignedIn( + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + onFailureCallback: goToLogin, + ); } else { super.handleFailureViewState(failure); } @@ -363,12 +245,18 @@ class HomeController extends ReloadableController { @override void handleSuccessViewState(Success success) { - if (PlatformInfo.isMobile && - (success is GetCredentialViewState || - success is GetStoredTokenOidcSuccess)) { - _handleDeepLinksSignedIn(success); - } else if (success is AutoSignInViaDeepLinkSuccess) { - _handleAutoSignInViaDeepLinkSuccess(success); + if (_validateToHandleDeepLinksSignedIn(success)) { + final personalAccount = _getPersonalAccountFromViewStateSuccess(success); + + _deepLinksManager!.handleDeepLinksWhenAppTerminatedSignedIn( + username: personalAccount?.userName?.value, + onFailureCallback: () => _continueUsingTheApp(success), + onConfirmCallback: (deepLinkData) => _handleLogOutAndSignInNewAccount( + authenticationViewStateSuccess: success, + personalAccount: personalAccount!, + deepLinkData: deepLinkData, + ), + ); } else { super.handleSuccessViewState(success); } 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 e8f4955f4..2fbe8bafc 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,3 +1,4 @@ +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'; @@ -37,6 +38,9 @@ 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 3791453bc..443bc186e 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 @@ -1,5 +1,9 @@ +import 'dart:async'; + import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -7,6 +11,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.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/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; @@ -16,6 +21,8 @@ import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_ import 'package:tmail_ui_user/features/starting_page/domain/state/sign_up_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart'; 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/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -28,11 +35,53 @@ class TwakeWelcomeController extends ReloadableController { final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor; final SignUpTwakeWorkplaceInteractor _signUpTwakeWorkplaceInteractor; + DeepLinksManager? _deepLinksManager; + StreamSubscription? _deepLinkDataStreamSubscription; + TwakeWelcomeController( this._signInTwakeWorkplaceInteractor, this._signUpTwakeWorkplaceInteractor, ); + @override + void onInit() { + super.onInit(); + if (PlatformInfo.isMobile) { + _registerDeepLinks(); + } + } + + void _registerDeepLinks() { + _deepLinksManager = getBinding(); + _deepLinksManager?.clearPendingDeepLinkData(); + _deepLinkDataStreamSubscription = _deepLinksManager + ?.pendingDeepLinkData.stream + .listen(_handlePendingDeepLinkDataStream); + } + + void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) { + log('TwakeWelcomeController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData'); + if (deepLinkData == null) return; + + if (currentContext != null) { + TipDialogHelper.loading(AppLocalizations.of(currentContext!).loadingPleaseWait); + } + + _deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn( + deepLinkData: deepLinkData, + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + onFailureCallback: TipDialogHelper.dismiss + ); + } + + void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) { + _synchronizeTokenAndGetSession( + baseUri: success.baseUri, + tokenOIDC: success.tokenOIDC, + oidcConfiguration: success.oidcConfiguration, + ); + } + void handleUseCompanyServer() { popAndPush( AppRoutes.login, @@ -134,14 +183,11 @@ class TwakeWelcomeController extends ReloadableController { required TokenOIDC tokenOIDC, required OIDCConfiguration oidcConfiguration, }) { - dynamicUrlInterceptors.setJmapUrl(baseUri.toString()); - dynamicUrlInterceptors.changeBaseUrl(baseUri.toString()); - authorizationInterceptors.setTokenAndAuthorityOidc( - newToken: tokenOIDC, - newConfig: oidcConfiguration); - authorizationIsolateInterceptors.setTokenAndAuthorityOidc( - newToken: tokenOIDC, - newConfig: oidcConfiguration); + setDataToInterceptors( + baseUrl: baseUri.toString(), + tokenOIDC: tokenOIDC, + oidcConfiguration: oidcConfiguration, + ); getSessionAction(); } @@ -157,4 +203,12 @@ class TwakeWelcomeController extends ReloadableController { toastManager.showMessageFailure(failure); } + + @override + void onClose() { + if (PlatformInfo.isMobile) { + _deepLinkDataStreamSubscription?.cancel(); + } + super.onClose(); + } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index f2390b931..c14feccd4 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4118,12 +4118,6 @@ "placeholders_order": [], "placeholders": {} }, - "tokenInvalid": "Token invalid", - "@tokenInvalid": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "getHelpOrReportABug": "Get help or report a bug", "@getHelpOrReportABug": { "type": "text", diff --git a/lib/main.dart b/lib/main.dart index 39fb43ef3..af3d00786 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,20 @@ import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; import 'package:tmail_ui_user/main/localizations/localization_service.dart'; import 'package:tmail_ui_user/main/pages/app_pages.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; import 'package:url_strategy/url_strategy.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -39,9 +42,26 @@ Future runTmail() async { runApp(const TMailApp()); } -class TMailApp extends StatelessWidget { +class TMailApp extends StatefulWidget { const TMailApp({Key? key}) : super(key: key); + @override + State createState() => _TMailAppState(); +} + +class _TMailAppState extends State { + + DeepLinksManager? _deepLinksManager; + + @override + void initState() { + super.initState(); + if (PlatformInfo.isMobile) { + _deepLinksManager = getBinding(); + _deepLinksManager?.registerDeepLinkStreamListener(); + } + } + @override Widget build(BuildContext context) { return GetMaterialApp( @@ -79,4 +99,12 @@ class TMailApp extends StatelessWidget { builder: FlutterSmartDialog.init(), ); } + + @override + void dispose() { + if (PlatformInfo.isMobile) { + _deepLinksManager?.dispose(); + } + super.dispose(); + } } \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index a3efcc36d..c932c96ba 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -18,7 +18,6 @@ import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart' import 'package:tmail_ui_user/features/caching/utils/session_storage_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; -import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; @@ -76,9 +75,6 @@ class CoreBindings extends Bindings { if (PlatformInfo.isIOS) { Get.put(IOSNotificationManager()); } - if (PlatformInfo.isMobile) { - Get.put(DeepLinksManager()); - } Get.put(PreviewEmlFileUtils()); } diff --git a/lib/main/bindings/deep_link/deep_link_bindings.dart b/lib/main/bindings/deep_link/deep_link_bindings.dart new file mode 100644 index 000000000..8887bd1f4 --- /dev/null +++ b/lib/main/bindings/deep_link/deep_link_bindings.dart @@ -0,0 +1,20 @@ + +import 'package:get/get.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/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; + +class DeepLinkBindings extends Bindings { + + @override + void dependencies() { + Get.put(AutoSignInViaDeepLinkInteractor( + Get.find(), + Get.find(), + Get.find(), + )); + Get.put(DeepLinksManager(Get.find())); + } +} \ No newline at end of file diff --git a/lib/main/bindings/main_bindings.dart b/lib/main/bindings/main_bindings.dart index 358a6df72..fc5618e0f 100644 --- a/lib/main/bindings/main_bindings.dart +++ b/lib/main/bindings/main_bindings.dart @@ -1,6 +1,8 @@ +import 'package:core/utils/platform_info.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/main/bindings/core/core_bindings.dart'; import 'package:tmail_ui_user/main/bindings/credential/credential_bindings.dart'; +import 'package:tmail_ui_user/main/bindings/deep_link/deep_link_bindings.dart'; import 'package:tmail_ui_user/main/bindings/local/local_bindings.dart'; import 'package:tmail_ui_user/main/bindings/local/local_isolate_bindings.dart'; import 'package:tmail_ui_user/main/bindings/network/network_bindings.dart'; @@ -19,5 +21,8 @@ class MainBindings extends Bindings { CredentialBindings().dependencies(); SessionBindings().dependencies(); NetWorkConnectionBindings().dependencies(); + if (PlatformInfo.isMobile) { + DeepLinkBindings().dependencies(); + } } } \ No newline at end of file diff --git a/lib/main/deep_links/deep_link_action_define.dart b/lib/main/deep_links/deep_link_action_define.dart new file mode 100644 index 000000000..877a3bc14 --- /dev/null +++ b/lib/main/deep_links/deep_link_action_define.dart @@ -0,0 +1,9 @@ + +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_links_manager.dart b/lib/main/deep_links/deep_links_manager.dart index 1e8f0357b..f70a04b37 100644 --- a/lib/main/deep_links/deep_links_manager.dart +++ b/lib/main/deep_links/deep_links_manager.dart @@ -1,11 +1,33 @@ 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_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'; + +class DeepLinksManager with MessageDialogActionMixin { + + final AutoSignInViaDeepLinkInteractor _autoSignInViaDeepLinkInteractor; + + BehaviorSubject _pendingDeepLinkData = BehaviorSubject.seeded(null); + + BehaviorSubject get pendingDeepLinkData => _pendingDeepLinkData; + + StreamSubscription? _deepLinkStreamSubscription; + + DeepLinksManager(this._autoSignInViaDeepLinkInteractor); -class DeepLinksManager { Future getDeepLinkData() async { final uriLink = await AppLinks().getInitialLink(); log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink'); @@ -15,6 +37,30 @@ class DeepLinksManager { return deepLinkData; } + void registerDeepLinkStreamListener() { + _deepLinkStreamSubscription = + AppLinks().uriLinkStream.listen(_handleUriLinkStream); + } + + void _handleUriLinkStream(Uri uri) { + final deepLinkData = parseDeepLink(uri.toString()); + log('DeepLinksManager::_handleUriLinkStream:DeepLinkData = $deepLinkData'); + setPendingDeepLinkData(deepLinkData); + } + + void setPendingDeepLinkData(DeepLinkData? deepLinkData) { + clearPendingDeepLinkData(); + _pendingDeepLinkData.add(deepLinkData); + } + + void clearPendingDeepLinkData() { + if(_pendingDeepLinkData.isClosed) { + _pendingDeepLinkData = BehaviorSubject.seeded(null); + } else { + _pendingDeepLinkData.add(null); + } + } + DeepLinkData? parseDeepLink(String url) { try { final updatedUrl = url.replaceFirst( @@ -22,6 +68,7 @@ class DeepLinksManager { 'https', ); final uri = Uri.parse(updatedUrl); + log('DeepLinksManager::parseDeepLink:uri = $uri'); final action = uri.host; final accessToken = uri.queryParameters['access_token']; final refreshToken = uri.queryParameters['refresh_token']; @@ -46,4 +93,168 @@ class DeepLinksManager { 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({ + 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, + ); + } + + Future autoSignInViaDeepLink({ + 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'); + onFailureCallback?.call(); + } + } + + 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, + ); + } + + void dispose() { + _deepLinkStreamSubscription?.cancel(); + _pendingDeepLinkData.close(); + } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 98bdc2842..0971b760e 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4315,13 +4315,6 @@ class AppLocalizations { ); } - String get tokenInvalid { - return Intl.message( - 'Token invalid', - name: 'tokenInvalid', - ); - } - String get getHelpOrReportABug { return Intl.message( 'Get help or report a bug', diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart index 0d1de200c..e55053db0 100644 --- a/test/main/deep_links/deep_links_manager.dart +++ b/test/main/deep_links/deep_links_manager.dart @@ -1,9 +1,14 @@ 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 deepLinkManager = DeepLinksManager(); + final mockAutoSignInViaDeepLinkInteractor = MockAutoSignInViaDeepLinkInteractor(); + final deepLinkManager = DeepLinksManager(mockAutoSignInViaDeepLinkInteractor); group('DeepLinksManager::parseDeepLink::test', () { test('Valid deep link with multiple query parameters', () {