From 2811ddd4b59577007ae17f5bd887cb51c5d94baf Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 18 Nov 2024 16:39:12 +0700 Subject: [PATCH 1/7] TF-3278 Handle open tmail app deep link but it was installed but not signed in --- android/app/src/main/AndroidManifest.xml | 11 ++++ .../reloadable/reloadable_controller.dart | 16 ++++-- .../home/presentation/home_controller.dart | 57 +++++++++++++++++++ lib/main/bindings/core/core_bindings.dart | 4 ++ lib/main/deep_links/deep_link_data.dart | 45 +++++++++++++++ lib/main/deep_links/deep_links_manager.dart | 42 ++++++++++++++ lib/main/utils/app_config.dart | 1 + pubspec.lock | 40 +++++++++++++ pubspec.yaml | 2 + 9 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 lib/main/deep_links/deep_link_data.dart create mode 100644 lib/main/deep_links/deep_links_manager.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e6ef49d68a..bb7ec1782e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -90,6 +90,17 @@ + + + + + + + + + (); _iosNotificationManager?.listenClickNotification(); } + + void _registerDeepLinks() { + _deepLinksManager = getBinding(); + } + + Future _handleDeepLinks() async { + final deepLinkData = await _deepLinksManager?.getDeepLinkData(); + log('HomeController::_handleDeepLinks:DeepLinkData = $deepLinkData'); + if (deepLinkData == null) { + goToLogin(); + return; + } + + if (deepLinkData.path == AppConfig.openAppHostDeepLink) { + _handleOpenApp(deepLinkData); + return; + } + + goToLogin(); + } + + void _handleOpenApp(DeepLinkData deepLinkData) { + if (deepLinkData.isValidToken()) { + setDataToInterceptors( + baseUrl: AppConfig.saasJmapServerUrl, + tokenOIDC: deepLinkData.getTokenOIDC(), + oidcConfiguration: OIDCConfiguration( + authority: AppConfig.saasRegistrationUrl, + clientId: OIDCConstant.clientId, + scopes: AppConfig.oidcScopes, + ) + ); + getSessionAction(); + } else { + goToLogin(); + } + } + + @override + void handleFailureViewState(Failure failure) { + if (PlatformInfo.isMobile && isNotSignedIn(failure)) { + _handleDeepLinks(); + } else { + super.handleFailureViewState(failure); + } + } } \ 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 9725bac0d5..7a311f7739 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_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'; @@ -72,6 +73,9 @@ class CoreBindings extends Bindings { if (PlatformInfo.isIOS) { Get.put(IOSNotificationManager()); } + if (PlatformInfo.isMobile) { + Get.put(DeepLinksManager()); + } } void _bindingIsolate() { diff --git a/lib/main/deep_links/deep_link_data.dart b/lib/main/deep_links/deep_link_data.dart new file mode 100644 index 0000000000..f7ff0ee5d9 --- /dev/null +++ b/lib/main/deep_links/deep_link_data.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:model/model.dart'; + +class DeepLinkData with EquatableMixin { + final String path; + final String? accessToken; + final String? refreshToken; + final String? idToken; + final int? expiresIn; + final String? username; + + DeepLinkData({ + required this.path, + 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, + ); + } + + @override + List get props => [ + path, + accessToken, + refreshToken, + idToken, + expiresIn, + username, + ]; +} diff --git a/lib/main/deep_links/deep_links_manager.dart b/lib/main/deep_links/deep_links_manager.dart new file mode 100644 index 0000000000..73d437f977 --- /dev/null +++ b/lib/main/deep_links/deep_links_manager.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:app_links/app_links.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; +import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; + +class DeepLinksManager { + Future getDeepLinkData() async { + final uriLink = await AppLinks().getInitialLink(); + if (uriLink == null) return null; + + final deepLinkData = parseDeepLink(uriLink.toString()); + return deepLinkData; + } + + DeepLinkData? parseDeepLink(String url) { + try { + final uri = Uri.parse(url.replaceFirst(OIDCConstant.twakeWorkplaceUrlScheme, 'https')); + + 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 = int.tryParse(expiresInStr); + + return DeepLinkData( + path: uri.path, + accessToken: accessToken, + refreshToken: refreshToken, + idToken: idToken, + expiresIn: expiresIn, + username: username, + ); + } catch (e) { + logError('DeepLinksManager::parseDeepLink:Exception = $e'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index ceb41963ad..3d06fa41f4 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -18,6 +18,7 @@ class AppConfig { static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy'; static const String saasRegistrationUrl = 'https://sign-up.stg.lin-saas.com'; static const String saasJmapServerUrl = 'https://jmap.stg.lin-saas.com'; + 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/pubspec.lock b/pubspec.lock index a5ca08fdd8..f0ff7b06ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" app_settings: dependency: "direct main" description: @@ -1142,6 +1174,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hive: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f87d9b5c2..29bf04d423 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -259,6 +259,8 @@ dependencies: flutter_web_auth_2: 3.1.1 + app_links: 6.3.2 + dev_dependencies: flutter_test: sdk: flutter From ba29b85ea8abee343c354ecea2ee1dddb59d613b Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 18 Nov 2024 16:52:48 +0700 Subject: [PATCH 2/7] TF-3278 Handle open tmail app deep link but it was installed but signed in with the same account --- .../home/presentation/home_controller.dart | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 4a41677476..77db04ebf2 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,6 +1,7 @@ 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:flutter/foundation.dart'; @@ -19,6 +20,8 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_lo 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/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'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -104,9 +107,9 @@ class HomeController extends ReloadableController { _deepLinksManager = getBinding(); } - Future _handleDeepLinks() async { + Future _handleDeepLinksNotSignedIn() async { final deepLinkData = await _deepLinksManager?.getDeepLinkData(); - log('HomeController::_handleDeepLinks:DeepLinkData = $deepLinkData'); + log('HomeController::_handleDeepLinksNotSignedIn:DeepLinkData = $deepLinkData'); if (deepLinkData == null) { goToLogin(); return; @@ -137,12 +140,55 @@ class HomeController extends ReloadableController { } } + Future _handleDeepLinksSignedIn(Success success) async { + String? username; + if (success is GetCredentialViewState) { + username = success.personalAccount.userName?.value; + } else if (success is GetStoredTokenOidcSuccess) { + username = success.personalAccount.userName?.value; + } + + final deepLinkData = await _deepLinksManager?.getDeepLinkData(); + log('HomeController::_handleDeepLinksSignedIn:DeepLinkData = $deepLinkData'); + if (deepLinkData == null) { + super.handleSuccessViewState(success); + return; + } + + if (deepLinkData.path == AppConfig.openAppHostDeepLink) { + if (deepLinkData.username?.isNotEmpty != true) { + _continueUsingTheApp(success); + return; + } + + if (deepLinkData.username == username) { + _continueUsingTheApp(success); + } + } + + super.handleSuccessViewState(success); + } + + void _continueUsingTheApp(Success success) { + super.handleSuccessViewState(success); + } + @override void handleFailureViewState(Failure failure) { if (PlatformInfo.isMobile && isNotSignedIn(failure)) { - _handleDeepLinks(); + _handleDeepLinksNotSignedIn(); } else { super.handleFailureViewState(failure); } } + + @override + void handleSuccessViewState(Success success) { + if (PlatformInfo.isMobile && + (success is GetCredentialViewState || success is GetStoredTokenOidcSuccess)) { + _handleDeepLinksSignedIn(success); + } else { + super.handleSuccessViewState(success); + } + } } \ No newline at end of file From 1fc520db3d5a0dc4dc49539e933ae7e075d9bb8d Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Nov 2024 02:16:12 +0700 Subject: [PATCH 3/7] TF-3278 Handle open tmail app deep link but it was installed but signed in with the other account --- lib/features/base/base_controller.dart | 72 ++++- .../mixin/message_dialog_action_mixin.dart | 20 +- .../reloadable/reloadable_controller.dart | 6 +- .../auto_sign_in_via_deep_link_state.dart | 21 ++ ...auto_sign_in_via_deep_link_interactor.dart | 55 ++++ .../home/presentation/home_bindings.dart | 11 + .../home/presentation/home_controller.dart | 250 +++++++++++++++--- lib/l10n/intl_messages.arb | 26 +- lib/main/deep_links/deep_link_data.dart | 6 +- lib/main/deep_links/deep_links_manager.dart | 25 +- lib/main/localizations/app_localizations.dart | 28 ++ test/main/deep_links/deep_links_manager.dart | 46 ++++ 12 files changed, 501 insertions(+), 65 deletions(-) create mode 100644 lib/features/home/domain/state/auto_sign_in_via_deep_link_state.dart create mode 100644 lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart create mode 100644 test/main/deep_links/deep_links_manager.dart diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 852b63873c..620f2ff800 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -452,6 +452,66 @@ abstract class BaseController extends GetxController } } + Future logoutToSignInNewAccount({ + required Session session, + required AccountId accountId, + required Function onSuccessCallback, + required Function 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) { + await _handleDeleteFCMAndClearData(); + onSuccessCallback(); + } else { + onFailureCallback(); + } + }, + ); + } else { + await _handleDeleteFCMAndClearData(); + onSuccessCallback(); + } + } catch (e) { + logError('BaseController::logoutToSignInNewAccount:Exception = $e'); + onFailureCallback(); + } + } + + Future _handleDeleteFCMAndClearData() async { + await Future.wait([ + if (_isFcmEnabled) + _handleDeleteFCMRegistration(), + clearAllData(), + ]); + } + + Future _handleDeleteFCMRegistration() async { + try { + _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; + } + }, + ); + } catch (e) { + logError('BaseController::_handleDeleteFCMRegistration:Exception = $e'); + } + } + void _destroyFirebaseRegistration(FirebaseRegistrationId firebaseRegistrationId) async { _destroyFirebaseRegistrationInteractor = getBinding(); if (_destroyFirebaseRegistrationInteractor != null) { @@ -482,10 +542,14 @@ abstract class BaseController extends GetxController } Future clearAllData() async { - if (isAuthenticatedWithOidc) { - await _clearOidcAuthData(); - } else { - await _clearBasicAuthData(); + try { + if (isAuthenticatedWithOidc) { + await _clearOidcAuthData(); + } else { + await _clearBasicAuthData(); + } + } catch (e) { + logError('BaseController::clearAllData:Exception = $e'); } } diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 3756683cb4..168ca43c71 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -37,11 +37,17 @@ mixin MessageDialogActionMixin { PopInvokedCallback? onPopInvoked, bool isArrangeActionButtonsVertical = false, int? titleActionButtonMaxLines, + EdgeInsetsGeometry? titlePadding, } ) async { final responsiveUtils = Get.find(); final imagePaths = Get.find(); + final paddingTitle = titlePadding ?? + (icon != null + ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) + : const EdgeInsetsDirectional.symmetric(horizontal: 24)); + if (alignCenter) { final childWidget = PointerInterceptor( child: (ConfirmDialogBuilder( @@ -57,10 +63,7 @@ mixin MessageDialogActionMixin { ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) ..marginIcon(icon != null ? (marginIcon ?? const EdgeInsets.only(top: 24)) : null) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24) - ) + ..paddingTitle(paddingTitle) ..radiusButton(12) ..paddingButton(paddingButton) ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) @@ -114,10 +117,7 @@ mixin MessageDialogActionMixin { ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24) - ) + ..paddingTitle(paddingTitle) ..marginIcon(EdgeInsets.zero) ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) ..marginButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) @@ -194,9 +194,7 @@ mixin MessageDialogActionMixin { ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) - ..paddingTitle(icon != null - ? const EdgeInsetsDirectional.only(top: 24, start: 24, end: 24) - : const EdgeInsetsDirectional.symmetric(horizontal: 24)) + ..paddingTitle(paddingTitle) ..marginIcon(EdgeInsets.zero) ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) ..marginButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 261fb43bfc..f530e7f9aa 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -23,7 +23,7 @@ import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; abstract class ReloadableController extends BaseController { - final GetSessionInteractor _getSessionInteractor = Get.find(); + final GetSessionInteractor getSessionInteractor = Get.find(); final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor = Get.find(); final UpdateAccountCacheInteractor _updateAccountCacheInteractor = Get.find(); @@ -35,7 +35,7 @@ abstract class ReloadableController extends BaseController { } else if (failure is GetSessionFailure) { logError('$runtimeType::handleFailureViewState():Failure = $failure'); handleGetSessionFailure(failure.exception); - } else if (failure is UpdateAccountCacheFailure) { + } else if (failure is UpdateAccountCacheFailure) { logError('$runtimeType::handleFailureViewState():Failure = $failure'); _handleUpdateAccountCacheCompleted( session: failure.session, @@ -122,7 +122,7 @@ abstract class ReloadableController extends BaseController { } void getSessionAction() { - consumeState(_getSessionInteractor.execute()); + consumeState(getSessionInteractor.execute()); } void handleGetSessionFailure(GetSessionFailure failure) { diff --git a/lib/features/home/domain/state/auto_sign_in_via_deep_link_state.dart b/lib/features/home/domain/state/auto_sign_in_via_deep_link_state.dart new file mode 100644 index 0000000000..1db16ab291 --- /dev/null +++ b/lib/features/home/domain/state/auto_sign_in_via_deep_link_state.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; + +class AutoSignInViaDeepLinkLoading extends LoadingState {} + +class AutoSignInViaDeepLinkSuccess extends Success { + final TokenOIDC tokenOIDC; + final Uri baseUri; + final OIDCConfiguration oidcConfiguration; + + AutoSignInViaDeepLinkSuccess(this.tokenOIDC, this.baseUri, this.oidcConfiguration); + + @override + List get props => [tokenOIDC, baseUri, oidcConfiguration]; +} + +class AutoSignInViaDeepLinkFailure extends FeatureFailure { + AutoSignInViaDeepLinkFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file 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 new file mode 100644 index 0000000000..90ade7c86b --- /dev/null +++ b/lib/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart @@ -0,0 +1,55 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.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'; + +class AutoSignInViaDeepLinkInteractor { + final AuthenticationOIDCRepository _authenticationOIDCRepository; + final AccountRepository _accountRepository; + final CredentialRepository _credentialRepository; + + const AutoSignInViaDeepLinkInteractor( + this._authenticationOIDCRepository, + this._accountRepository, + this._credentialRepository + ); + + Stream> execute({ + required Uri baseUri, + required TokenOIDC tokenOIDC, + required OIDCConfiguration oidcConfiguration + }) async* { + try { + yield Right(AutoSignInViaDeepLinkLoading()); + + await Future.wait([ + _credentialRepository.saveBaseUrl(baseUri), + _authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), + _authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority), + ]); + + await _accountRepository.setCurrentAccount( + PersonalAccount( + tokenOIDC.tokenIdHash, + AuthenticationType.oidc, + isSelected: true + ) + ); + + yield Right(AutoSignInViaDeepLinkSuccess( + tokenOIDC, + baseUri, + oidcConfiguration, + )); + } catch (e) { + yield Left(AutoSignInViaDeepLinkFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/home/presentation/home_bindings.dart b/lib/features/home/presentation/home_bindings.dart index 2e75157d2e..fe4ff19dc1 100644 --- a/lib/features/home/presentation/home_bindings.dart +++ b/lib/features/home/presentation/home_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/cleanup/data/datasource/cleanup_datasource.dart'; @@ -11,8 +12,11 @@ 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'; @@ -54,6 +58,13 @@ 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 77db04ebf2..3a03423413 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,12 +1,15 @@ 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'; @@ -19,10 +22,14 @@ 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/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'; +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'; @@ -40,6 +47,7 @@ class HomeController extends ReloadableController { IOSNotificationManager? _iosNotificationManager; DeepLinksManager? _deepLinksManager; + AutoSignInViaDeepLinkInteractor? _autoSignInViaDeepLinkInteractor; HomeController( this._cleanupEmailCacheInteractor, @@ -105,6 +113,7 @@ class HomeController extends ReloadableController { void _registerDeepLinks() { _deepLinksManager = getBinding(); + _autoSignInViaDeepLinkInteractor = getBinding(); } Future _handleDeepLinksNotSignedIn() async { @@ -115,68 +124,238 @@ class HomeController extends ReloadableController { return; } - if (deepLinkData.path == AppConfig.openAppHostDeepLink) { + if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { _handleOpenApp(deepLinkData); - return; + } else { + goToLogin(); } - - goToLogin(); } void _handleOpenApp(DeepLinkData deepLinkData) { - if (deepLinkData.isValidToken()) { - setDataToInterceptors( - baseUrl: AppConfig.saasJmapServerUrl, - tokenOIDC: deepLinkData.getTokenOIDC(), - oidcConfiguration: OIDCConfiguration( - authority: AppConfig.saasRegistrationUrl, - clientId: OIDCConstant.clientId, - scopes: AppConfig.oidcScopes, - ) - ); - getSessionAction(); + _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 { - goToLogin(); + _continueUsingTheApp(authenticationViewStateSuccess); } } - Future _handleDeepLinksSignedIn(Success success) async { - String? username; + void _continueUsingTheApp(Success authenticationViewStateSuccess) { + log('HomeController::_continueUsingTheApp:'); + super.handleSuccessViewState(authenticationViewStateSuccess); + } + + PersonalAccount? _getPersonalAccountFromViewStateSuccess(Success success) { if (success is GetCredentialViewState) { - username = success.personalAccount.userName?.value; + return success.personalAccount; } else if (success is GetStoredTokenOidcSuccess) { - username = success.personalAccount.userName?.value; + return success.personalAccount; } + return null; + } - final deepLinkData = await _deepLinksManager?.getDeepLinkData(); - log('HomeController::_handleDeepLinksSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { - super.handleSuccessViewState(success); + 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.doYouWantToLogoutOf), + 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, + required DeepLinkData deepLinkData, + }) { + if (authenticationViewStateSuccess is GetCredentialViewState) { + setDataToInterceptors( + baseUrl: authenticationViewStateSuccess.baseUrl.toString(), + userName: authenticationViewStateSuccess.userName, + password: authenticationViewStateSuccess.password, + ); + } else if (authenticationViewStateSuccess is GetStoredTokenOidcSuccess) { + setDataToInterceptors( + baseUrl: authenticationViewStateSuccess.baseUrl.toString(), + tokenOIDC: authenticationViewStateSuccess.tokenOidc, + oidcConfiguration: authenticationViewStateSuccess.oidcConfiguration, + ); + } + + _getSessionActionToLogOut( + authenticationViewStateSuccess: authenticationViewStateSuccess, + personalAccount: personalAccount, + deepLinkData: deepLinkData, + ); + } + + Future _getSessionActionToLogOut({ + required Success authenticationViewStateSuccess, + required PersonalAccount personalAccount, + required DeepLinkData deepLinkData, + }) async { + try { + final sessionViewState = await getSessionInteractor.execute().last; + + sessionViewState.fold( + (failure) => _handleGetSessionFailureToLogOut(authenticationViewStateSuccess), + (success) => _handleGetSessionSuccessToLogOut( + sessionViewStateSuccess: success, + authenticationViewStateSuccess: authenticationViewStateSuccess, + personalAccount: personalAccount, + deepLinkData: deepLinkData, + ), + ); + } catch (e) { + logError('HomeController::_getSessionActionToLogOut:Exception = $e'); + _handleGetSessionFailureToLogOut(authenticationViewStateSuccess); + } + } + + void _handleGetSessionSuccessToLogOut({ + required Success sessionViewStateSuccess, + required Success authenticationViewStateSuccess, + required PersonalAccount personalAccount, + required DeepLinkData deepLinkData, + }) { + if (sessionViewStateSuccess is GetSessionSuccess) { + logoutToSignInNewAccount( + session: sessionViewStateSuccess.session, + accountId: personalAccount.accountId!, + onFailureCallback: () => + _continueUsingTheApp(authenticationViewStateSuccess), + onSuccessCallback: () => _autoSignInViaDeepLink(deepLinkData), + ); + } else { + _continueUsingTheApp(authenticationViewStateSuccess); + } + } + + void _handleGetSessionFailureToLogOut(Success authenticationViewStateSuccess) { + _continueUsingTheApp(authenticationViewStateSuccess); + + if (currentContext == null || currentOverlayContext == null) { return; } - if (deepLinkData.path == AppConfig.openAppHostDeepLink) { - if (deepLinkData.username?.isNotEmpty != true) { - _continueUsingTheApp(success); + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).notFoundSession, + ); + } + + 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; } - if (deepLinkData.username == username) { - _continueUsingTheApp(success); - } + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).tokenInvalid, + ); + } + } + + void _handleAutoSignInViaDeepLinkFailure() { + goToLogin(); + + if (currentContext == null || currentOverlayContext == null) { + return; } - super.handleSuccessViewState(success); + appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).tokenInvalid, + ); } - void _continueUsingTheApp(Success success) { - super.handleSuccessViewState(success); + void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) { + setDataToInterceptors( + baseUrl: success.baseUri.toString(), + tokenOIDC: success.tokenOIDC, + oidcConfiguration: success.oidcConfiguration, + ); + getSessionAction(); } @override void handleFailureViewState(Failure failure) { if (PlatformInfo.isMobile && isNotSignedIn(failure)) { _handleDeepLinksNotSignedIn(); + } else if (failure is AutoSignInViaDeepLinkFailure) { + _handleAutoSignInViaDeepLinkFailure(); } else { super.handleFailureViewState(failure); } @@ -185,8 +364,11 @@ class HomeController extends ReloadableController { @override void handleSuccessViewState(Success success) { if (PlatformInfo.isMobile && - (success is GetCredentialViewState || success is GetStoredTokenOidcSuccess)) { + (success is GetCredentialViewState || + success is GetStoredTokenOidcSuccess)) { _handleDeepLinksSignedIn(success); + } else if (success is AutoSignInViaDeepLinkSuccess) { + _handleAutoSignInViaDeepLinkSuccess(success); } else { super.handleSuccessViewState(success); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index d4d794eb14..98774d0847 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-10-31T13:18:32.336494", + "@@last_modified": "2024-11-18T20:44:32.192113", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4071,5 +4071,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "logoutConfirmation": "Logout Confirmation", + "@logoutConfirmation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yesLogout": "Yes, Log out", + "@yesLogout": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "doYouWantToLogoutOf": "Do you want to log out of", + "@doYouWantToLogoutOf": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "tokenInvalid": "Token invalid", + "@tokenInvalid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ 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 f7ff0ee5d9..7a9b37cea0 100644 --- a/lib/main/deep_links/deep_link_data.dart +++ b/lib/main/deep_links/deep_link_data.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:model/model.dart'; class DeepLinkData with EquatableMixin { - final String path; + final String action; final String? accessToken; final String? refreshToken; final String? idToken; @@ -10,7 +10,7 @@ class DeepLinkData with EquatableMixin { final String? username; DeepLinkData({ - required this.path, + required this.action, this.accessToken, this.refreshToken, this.idToken, @@ -35,7 +35,7 @@ class DeepLinkData with EquatableMixin { @override List get props => [ - path, + action, accessToken, refreshToken, idToken, diff --git a/lib/main/deep_links/deep_links_manager.dart b/lib/main/deep_links/deep_links_manager.dart index 73d437f977..1e8f0357b9 100644 --- a/lib/main/deep_links/deep_links_manager.dart +++ b/lib/main/deep_links/deep_links_manager.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; class DeepLinksManager { Future getDeepLinkData() async { final uriLink = await AppLinks().getInitialLink(); + log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink'); if (uriLink == null) return null; final deepLinkData = parseDeepLink(uriLink.toString()); @@ -16,18 +17,24 @@ class DeepLinksManager { DeepLinkData? parseDeepLink(String url) { try { - final uri = Uri.parse(url.replaceFirst(OIDCConstant.twakeWorkplaceUrlScheme, 'https')); - - 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 updatedUrl = url.replaceFirst( + OIDCConstant.twakeWorkplaceUrlScheme, + 'https', + ); + final uri = Uri.parse(updatedUrl); + 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 = int.tryParse(expiresInStr); + final expiresIn = expiresInStr != null + ? int.tryParse(expiresInStr) + : null; return DeepLinkData( - path: uri.path, + action: action, accessToken: accessToken, refreshToken: refreshToken, idToken: idToken, diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9dac836f6b..8fe6b08917 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4273,4 +4273,32 @@ class AppLocalizations { name: 'createTwakeIdFailed', ); } + + String get logoutConfirmation { + return Intl.message( + 'Logout Confirmation', + name: 'logoutConfirmation', + ); + } + + String get yesLogout { + return Intl.message( + 'Yes, Log out', + name: 'yesLogout', + ); + } + + String get doYouWantToLogoutOf { + return Intl.message( + 'Do you want to log out of', + name: 'doYouWantToLogoutOf', + ); + } + + String get tokenInvalid { + return Intl.message( + 'Token invalid', + name: 'tokenInvalid', + ); + } } diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart new file mode 100644 index 0000000000..0d1de200cd --- /dev/null +++ b/test/main/deep_links/deep_links_manager.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.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'; + +void main() { + final deepLinkManager = DeepLinksManager(); + + 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 From 73d89db2cc8c524de32fbfb293d2bd96ae6d499e Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Nov 2024 04:17:59 +0700 Subject: [PATCH 4/7] TF-3278 Handle open app via deep link at TwakeWelcome screen --- .../home/presentation/home_bindings.dart | 11 - .../home/presentation/home_controller.dart | 188 ++++------------ .../twake_welcome/twake_welcome_bindings.dart | 4 + .../twake_welcome_controller.dart | 70 +++++- lib/l10n/intl_messages.arb | 8 +- lib/main.dart | 30 ++- lib/main/bindings/core/core_bindings.dart | 4 - .../deep_link/deep_link_bindings.dart | 20 ++ lib/main/bindings/main_bindings.dart | 5 + .../deep_links/deep_link_action_define.dart | 9 + lib/main/deep_links/deep_links_manager.dart | 213 +++++++++++++++++- lib/main/localizations/app_localizations.dart | 7 - test/main/deep_links/deep_links_manager.dart | 7 +- 13 files changed, 386 insertions(+), 190 deletions(-) create mode 100644 lib/main/bindings/deep_link/deep_link_bindings.dart create mode 100644 lib/main/deep_links/deep_link_action_define.dart diff --git a/lib/features/home/presentation/home_bindings.dart b/lib/features/home/presentation/home_bindings.dart index fe4ff19dc1..2e75157d2e 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 3a03423413..52ff01bf0a 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.doYouWantToLogoutOf), - 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 e8f4955f40..2fbe8bafc5 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 64b2e85958..960f40f352 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:jmap_dart_client/jmap/core/session/session.dart'; @@ -7,6 +11,7 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tip_dialog/tip_dialog.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 98774d0847..dcd9510717 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-11-18T20:44:32.192113", + "@@last_modified": "2024-11-19T03:05:11.711272", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4089,11 +4089,5 @@ "type": "text", "placeholders_order": [], "placeholders": {} - }, - "tokenInvalid": "Token invalid", - "@tokenInvalid": { - "type": "text", - "placeholders_order": [], - "placeholders": {} } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9ba0af392c..297daf62cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,19 @@ 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: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'; @@ -38,9 +41,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( @@ -76,4 +96,12 @@ class TMailApp extends StatelessWidget { initialRoute: AppRoutes.home, getPages: AppPages.pages); } + + @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 7a311f7739..9725bac0d5 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -15,7 +15,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/base/before_reconnect_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'; @@ -73,9 +72,6 @@ class CoreBindings extends Bindings { if (PlatformInfo.isIOS) { Get.put(IOSNotificationManager()); } - if (PlatformInfo.isMobile) { - Get.put(DeepLinksManager()); - } } void _bindingIsolate() { 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 0000000000..8887bd1f4b --- /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 358a6df72f..fc5618e0fa 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 0000000000..877a3bc148 --- /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 1e8f0357b9..e078305e26 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.doYouWantToLogoutOf), + 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 8fe6b08917..1ead12b69f 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4294,11 +4294,4 @@ class AppLocalizations { name: 'doYouWantToLogoutOf', ); } - - String get tokenInvalid { - return Intl.message( - 'Token invalid', - name: 'tokenInvalid', - ); - } } diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart index 0d1de200cd..e55053db02 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', () { From 13a1fdff53aa7b0da22058cb15e94e7479e18f23 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Nov 2024 09:41:14 +0700 Subject: [PATCH 5/7] TF-3278 Handle open app via deep link at Login screen --- .../login/presentation/login_controller.dart | 62 ++++++++++++++++++- .../twake_welcome_controller.dart | 18 +++--- .../twake_welcome/twake_welcome_view.dart | 58 +++++++---------- lib/main.dart | 5 +- pubspec.lock | 24 +++---- pubspec.yaml | 4 +- 6 files changed, 106 insertions(+), 65 deletions(-) diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 7946426e30..19f993b8a6 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:core/presentation/extensions/url_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -7,6 +9,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -16,6 +19,7 @@ import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_response.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/oidc_error.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; @@ -52,6 +56,9 @@ import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_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/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'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -89,6 +96,9 @@ class LoginController extends ReloadableController { Password? _password; Uri? _baseUri; + DeepLinksManager? _deepLinksManager; + StreamSubscription? _deepLinkDataStreamSubscription; + LoginController( this._authenticationInteractor, this._checkOIDCIsAvailableInteractor, @@ -106,6 +116,14 @@ class LoginController extends ReloadableController { this._signInTwakeWorkplaceInteractor, ); + @override + void onInit() { + super.onInit(); + if (PlatformInfo.isMobile) { + _registerDeepLinks(); + } + } + @override void onReady() { super.onReady(); @@ -208,9 +226,48 @@ class LoginController extends ReloadableController { @override void handleReloaded(Session session) { + SmartDialog.dismiss(); + popAndPush( RouteUtils.generateNavigationRoute(AppRoutes.dashboard), - arguments: session + arguments: session, + ); + } + + @override + void handleGetSessionFailure(GetSessionFailure failure) { + SmartDialog.dismiss(); + super.handleGetSessionFailure(failure); + } + + void _registerDeepLinks() { + _deepLinksManager = getBinding(); + _deepLinksManager?.clearPendingDeepLinkData(); + _deepLinkDataStreamSubscription = _deepLinksManager + ?.pendingDeepLinkData.stream + .listen(_handlePendingDeepLinkDataStream); + } + + void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) { + log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData'); + if (deepLinkData == null) return; + + if (currentContext != null) { + SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait); + } + + _deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn( + deepLinkData: deepLinkData, + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + onFailureCallback: SmartDialog.dismiss, + ); + } + + void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) { + _synchronizeTokenAndGetSession( + baseUri: success.baseUri, + tokenOIDC: success.tokenOIDC, + oidcConfiguration: success.oidcConfiguration, ); } @@ -570,6 +627,9 @@ class LoginController extends ReloadableController { urlInputController.dispose(); usernameInputController.dispose(); passwordInputController.dispose(); + if (PlatformInfo.isMobile) { + _deepLinkDataStreamSubscription?.cancel(); + } super.onClose(); } } \ No newline at end of file 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 960f40f352..969a49b52a 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 @@ -6,10 +6,10 @@ 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'; 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:tip_dialog/tip_dialog.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'; @@ -64,13 +64,13 @@ class TwakeWelcomeController extends ReloadableController { if (deepLinkData == null) return; if (currentContext != null) { - TipDialogHelper.loading(AppLocalizations.of(currentContext!).loadingPleaseWait); + SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait); } _deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn( deepLinkData: deepLinkData, onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, - onFailureCallback: TipDialogHelper.dismiss + onFailureCallback: SmartDialog.dismiss, ); } @@ -93,7 +93,7 @@ class TwakeWelcomeController extends ReloadableController { } void onClickSignIn(BuildContext context) { - TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait); + SmartDialog.showLoading(msg: AppLocalizations.of(context).loadingPleaseWait); final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl); @@ -113,7 +113,7 @@ class TwakeWelcomeController extends ReloadableController { } void onSignUpTwakeWorkplace(BuildContext context) { - TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait); + SmartDialog.showLoading(msg: AppLocalizations.of(context).loadingPleaseWait); final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl); @@ -164,7 +164,7 @@ class TwakeWelcomeController extends ReloadableController { @override void handleReloaded(Session session) { - TipDialogHelper.dismiss(); + SmartDialog.dismiss(); popAndPush( RouteUtils.generateNavigationRoute(AppRoutes.dashboard), @@ -173,7 +173,7 @@ class TwakeWelcomeController extends ReloadableController { @override void handleGetSessionFailure(GetSessionFailure failure) { - TipDialogHelper.dismiss(); + SmartDialog.dismiss(); toastManager.showMessageFailure(failure); } @@ -193,13 +193,13 @@ class TwakeWelcomeController extends ReloadableController { } void _handleSignInTwakeWorkplaceFailure(SignInTwakeWorkplaceFailure failure) { - TipDialogHelper.dismiss(); + SmartDialog.dismiss(); toastManager.showMessageFailure(failure); } void _handleSignUpTwakeWorkplaceFailure(SignUpTwakeWorkplaceFailure failure) { - TipDialogHelper.dismiss(); + SmartDialog.dismiss(); toastManager.showMessageFailure(failure); } diff --git a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_view.dart b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_view.dart index c16bc67a94..5d56b4aead 100644 --- a/lib/features/starting_page/presentation/twake_welcome/twake_welcome_view.dart +++ b/lib/features/starting_page/presentation/twake_welcome/twake_welcome_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; -import 'package:tip_dialog/tip_dialog.dart'; import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart'; import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_view_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -19,42 +18,29 @@ class TwakeWelcomeView extends GetWidget { DeviceOrientation.portraitDown ]); - return Stack( - children: [ - TwakeWelcomeScreen( - logo: Padding( - padding: const EdgeInsetsDirectional.only(bottom: 16), - child: SvgPicture.asset( - controller.imagePaths.icLogoTwakeWelcome, - fit: BoxFit.fill, - ), - ), - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - overlayColor: WidgetStateProperty.all(Colors.transparent), - signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '', - onSignInOnTap: () => controller.onClickSignIn(context), - onCreateTwakeIdOnTap: () => controller.onSignUpTwakeWorkplace(context), - createTwakeIdTitle: AppLocalizations.of(context).createTwakeId, - useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer, - description: AppLocalizations.of(context).descriptionWelcomeTo, - descriptionTextStyle: TwakeWelcomeViewStyle.descriptionTextStyle, - privacyPolicy: AppLocalizations.of(context).privacyPolicy, - descriptionPrivacyPolicy: AppLocalizations.of(context).byContinuingYouAreAgreeingToOur, - onPrivacyPolicyOnTap: controller.onClickPrivacyPolicy, - onUseCompanyServerOnTap: controller.handleUseCompanyServer, + return TwakeWelcomeScreen( + logo: Padding( + padding: const EdgeInsetsDirectional.only(bottom: 16), + child: SvgPicture.asset( + controller.imagePaths.icLogoTwakeWelcome, + fit: BoxFit.fill, ), - TipDialogContainer( - duration: const Duration(seconds: 2), - outsideTouchable: true, - onOutsideTouch: (tipDialog) { - if (tipDialog is TipDialog && tipDialog.type == TipDialogType.LOADING) { - TipDialogHelper.dismiss(); - } - } - ) - ], + ), + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + overlayColor: WidgetStateProperty.all(Colors.transparent), + signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '', + onSignInOnTap: () => controller.onClickSignIn(context), + onCreateTwakeIdOnTap: () => controller.onSignUpTwakeWorkplace(context), + createTwakeIdTitle: AppLocalizations.of(context).createTwakeId, + useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer, + description: AppLocalizations.of(context).descriptionWelcomeTo, + descriptionTextStyle: TwakeWelcomeViewStyle.descriptionTextStyle, + privacyPolicy: AppLocalizations.of(context).privacyPolicy, + descriptionPrivacyPolicy: AppLocalizations.of(context).byContinuingYouAreAgreeingToOur, + onPrivacyPolicyOnTap: controller.onClickPrivacyPolicy, + onUseCompanyServerOnTap: controller.handleUseCompanyServer, ); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 297daf62cd..af3d00786e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ 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'; @@ -94,7 +95,9 @@ class _TMailAppState extends State { unknownRoute: AppPages.unknownRoutePage, defaultTransition: Transition.noTransition, initialRoute: AppRoutes.home, - getPages: AppPages.pages); + getPages: AppPages.pages, + builder: FlutterSmartDialog.init(), + ); } @override diff --git a/pubspec.lock b/pubspec.lock index f0ff7b06ad..2c8973191d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1053,6 +1053,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + flutter_smart_dialog: + dependency: "direct main" + description: + name: flutter_smart_dialog + sha256: d7b915461fdc9bb8111d23a709b4ce910dbc4b9bef0fbd941655f74bf7de09a6 + url: "https://pub.dev" + source: hosted + version: "4.9.8+5" flutter_staggered_grid_view: dependency: "direct main" description: @@ -1386,14 +1394,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - loading_view: - dependency: transitive - description: - name: loading_view - sha256: "3d31c2c5293c2e3518b7330ffdc2fab5c83daaa9218cc45d4406c875f08f0795" - url: "https://pub.dev" - source: hosted - version: "1.1.0" logging: dependency: transitive description: @@ -2080,14 +2080,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - tip_dialog: - dependency: "direct main" - description: - name: tip_dialog - sha256: edc1ebbb4b9f9575220c85206cf6986921c2b63d173744be0babac00c2e48bd0 - url: "https://pub.dev" - source: hosted - version: "4.0.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 29bf04d423..3401052059 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -255,12 +255,12 @@ dependencies: web_socket_channel: 2.4.3 - tip_dialog: 4.0.0 - flutter_web_auth_2: 3.1.1 app_links: 6.3.2 + flutter_smart_dialog: 4.9.8+5 + dev_dependencies: flutter_test: sdk: flutter From 08f8961b2eeb2c1c03127b8e398f592b6350ca04 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 19 Nov 2024 13:28:35 +0700 Subject: [PATCH 6/7] TF-3278 Handle open app via deep link at MailboxDashboard screen --- ios/Podfile.lock | 6 + ios/Runner/Info.plist | 1 + lib/features/base/base_controller.dart | 30 ++++- .../home/presentation/home_controller.dart | 44 +++++++- .../data/network/config/oidc_constant.dart | 1 + .../domain/exceptions/logout_exception.dart | 1 + .../login/presentation/login_controller.dart | 6 + .../model/login_navigate_arguments.dart | 29 +++++ .../model/login_navigate_type.dart | 3 + .../mailbox_dashboard_controller.dart | 73 ++++++++++++ .../twake_welcome_controller.dart | 6 + lib/l10n/intl_messages.arb | 12 +- lib/main/deep_links/deep_links_manager.dart | 106 +++++++++++------- lib/main/localizations/app_localizations.dart | 13 ++- 14 files changed, 277 insertions(+), 54 deletions(-) create mode 100644 lib/features/login/domain/exceptions/logout_exception.dart create mode 100644 lib/features/login/presentation/model/login_navigate_arguments.dart create mode 100644 lib/features/login/presentation/model/login_navigate_type.dart 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 84f94739ab..407c2674aa 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/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 620f2ff800..0775bdb2a7 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -1,5 +1,5 @@ import 'dart:async'; - +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'; @@ -32,7 +32,9 @@ import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.da import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; 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/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'; @@ -213,6 +215,8 @@ abstract class BaseController extends GetxController clearDataAndGoToLoginPage(); } + void onCancelReconnectWhenSessionExpired() {} + void _handleConnectionErrorException() { if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( @@ -251,7 +255,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(); @@ -267,7 +273,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 + ); } } @@ -456,7 +464,7 @@ 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); @@ -465,7 +473,14 @@ abstract class BaseController extends GetxController final logoutViewState = await logoutOidcInteractor.execute().last; logoutViewState.fold( - (failure) => onFailureCallback(), + (failure) async { + if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) { + await _handleDeleteFCMAndClearData(); + onFailureCallback(exception: UserCancelledLogoutOIDCFlowException()); + } else { + onFailureCallback(); + } + }, (success) async { if (success is LogoutOidcSuccess) { await _handleDeleteFCMAndClearData(); @@ -485,6 +500,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) diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 52ff01bf0a..1c1d1b22fe 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,8 +22,11 @@ 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/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/deep_link_data.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.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(); } @@ -180,8 +209,17 @@ class HomeController extends ReloadableController { logoutToSignInNewAccount( session: sessionViewStateSuccess.session, accountId: personalAccount.accountId!, - onFailureCallback: () => - _continueUsingTheApp(authenticationViewStateSuccess), + onFailureCallback: ({exception}) { + if (exception is UserCancelledLogoutOIDCFlowException) { + _deepLinksManager?.autoSignInViaDeepLink( + deepLinkData: deepLinkData, + onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess), + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + ); + } else { + _continueUsingTheApp(authenticationViewStateSuccess); + } + }, onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink( deepLinkData: deepLinkData, onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess), diff --git a/lib/features/login/data/network/config/oidc_constant.dart b/lib/features/login/data/network/config/oidc_constant.dart index 2e4790c58b..895bc53256 100644 --- a/lib/features/login/data/network/config/oidc_constant.dart +++ b/lib/features/login/data/network/config/oidc_constant.dart @@ -11,6 +11,7 @@ class OIDCConstant { 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 String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId; } \ No newline at end of file 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/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 19f993b8a6..4305a6c253 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -240,6 +240,12 @@ class LoginController extends ReloadableController { super.handleGetSessionFailure(failure); } + @override + void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) { + SmartDialog.dismiss(); + super.handleUrgentExceptionOnMobile(failure: failure, exception: exception); + } + void _registerDeepLinks() { _deepLinksManager = getBinding(); _deepLinksManager?.clearPendingDeepLinkData(); 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 bd5ca0ee12..26e165e222 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,8 @@ 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/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -245,6 +251,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo StreamSubscription? _pendingSharedFileInfoSubscription; StreamSubscription? _receivingFileSharingStreamSubscription; StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; + DeepLinksManager? _deepLinksManager; + StreamSubscription? _deepLinkDataStreamSubscription; final StreamController> _progressStateController = StreamController>.broadcast(); @@ -288,6 +296,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo void onInit() { if (PlatformInfo.isMobile) { _registerReceivingFileSharingStream(); + _registerDeepLinks(); } _registerStreamListener(); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); @@ -437,6 +446,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 { @@ -533,6 +544,65 @@ 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'); + if (deepLinkData == null) return; + + _deepLinksManager?.handleDeepLinksWhenAppOnForegroundSignedIn( + deepLinkData: deepLinkData, + username: sessionCurrent?.username.value, + onConfirmCallback: _handleLogOutAndSignInNewAccount, + ); + } + + void _handleLogOutAndSignInNewAccount(DeepLinkData deepLinkData) { + 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( + deepLinkData: deepLinkData, + onFailureCallback: SmartDialog.dismiss, + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + ); + } else { + SmartDialog.dismiss(); + } + }, + onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink( + deepLinkData: deepLinkData, + onFailureCallback: SmartDialog.dismiss, + onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, + ), + ); + } + + void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) { + SmartDialog.dismiss(); + + pushAndPopAll( + AppRoutes.home, + arguments: LoginNavigateArguments.autoSignIn( + autoSignInViaDeepLinkSuccess: success, + ), + ); + } + void _registerPendingCurrentEmailIdInNotification() { _iosNotificationManager = getBinding(); _currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager @@ -1468,6 +1538,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo @override void handleReloaded(Session session) { log('MailboxDashBoardController::handleReloaded():'); + SmartDialog.dismiss(); + _getRouteParameters(); _setUpComponentsFromSession(session); if (PlatformInfo.isWeb) { @@ -2921,6 +2993,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo _pendingSharedFileInfoSubscription?.cancel(); _receivingFileSharingStreamSubscription?.cancel(); _emailReceiveManager.closeEmailReceiveManagerStream(); + _deepLinkDataStreamSubscription?.cancel(); } _progressStateController.close(); _refreshActionEventController.close(); 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..e60946daea 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 @@ -178,6 +178,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 dcd9510717..4bf8b9c4c8 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-11-19T03:05:11.711272", + "@@last_modified": "2024-11-19T12:50:39.757207", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4072,8 +4072,8 @@ "placeholders_order": [], "placeholders": {} }, - "logoutConfirmation": "Logout Confirmation", - "@logoutConfirmation": { + "swithAccountConfirmation": "Switch Account Confirmation", + "@swithAccountConfirmation": { "type": "text", "placeholders_order": [], "placeholders": {} @@ -4089,5 +4089,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "andSwitchAccount": "and switch account", + "@andSwitchAccount": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ 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 e078305e26..c2fa3098f4 100644 --- a/lib/main/deep_links/deep_links_manager.dart +++ b/lib/main/deep_links/deep_links_manager.dart @@ -100,15 +100,41 @@ class DeepLinksManager with MessageDialogActionMixin { OnDeepLinkFailureCallback? onFailureCallback, }) async { log('DeepLinksManager::handleDeepLinksWhenAppOnForegroundNotSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - _handleOpenApp( - deepLinkData: deepLinkData, - onFailureCallback: onFailureCallback, - onSuccessCallback: onSuccessCallback, - ); - } else { + if (deepLinkData.action.toLowerCase() != AppConfig.openAppHostDeepLink.toLowerCase()) { onFailureCallback?.call(); + return; } + + _handleOpenApp( + deepLinkData: deepLinkData, + onFailureCallback: onFailureCallback, + onSuccessCallback: onSuccessCallback, + ); + } + + Future handleDeepLinksWhenAppOnForegroundSignedIn({ + required DeepLinkData deepLinkData, + required String? username, + required OnDeepLinkConfirmLogoutCallback onConfirmCallback, + OnDeepLinkFailureCallback? onFailureCallback, + }) async { + log('DeepLinksManager::handleDeepLinksWhenAppOnForegroundNotSignedIn:DeepLinkData = $deepLinkData'); + if (deepLinkData.action.toLowerCase() != AppConfig.openAppHostDeepLink.toLowerCase() || + deepLinkData.username?.isNotEmpty != true || + username?.isNotEmpty != true || + deepLinkData.username == username || + currentContext == null) { + onFailureCallback?.call(); + return; + } + + _showConfirmDialogSwitchAccount( + context: currentContext!, + currentUsername: username!, + newUsername: deepLinkData.username!, + onConfirmAction: () => onConfirmCallback(deepLinkData), + onCancelAction: () => onFailureCallback?.call(), + ); } Future handleDeepLinksWhenAppTerminatedNotSignedIn({ @@ -117,20 +143,17 @@ class DeepLinksManager with MessageDialogActionMixin { }) async { final deepLinkData = await getDeepLinkData(); log('DeepLinksManager::handleDeepLinksWhenAppTerminatedNotSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { + if (deepLinkData == null || + deepLinkData.action.toLowerCase() != AppConfig.openAppHostDeepLink.toLowerCase()) { onFailureCallback(); return; } - if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { - _handleOpenApp( - deepLinkData: deepLinkData, - onFailureCallback: onFailureCallback, - onSuccessCallback: onSuccessCallback, - ); - } else { - onFailureCallback(); - } + _handleOpenApp( + deepLinkData: deepLinkData, + onFailureCallback: onFailureCallback, + onSuccessCallback: onSuccessCallback, + ); } Future handleDeepLinksWhenAppTerminatedSignedIn({ @@ -140,30 +163,23 @@ class DeepLinksManager with MessageDialogActionMixin { }) async { final deepLinkData = await getDeepLinkData(); log('DeepLinksManager::handleDeepLinksWhenAppTerminatedSignedIn:DeepLinkData = $deepLinkData'); - if (deepLinkData == null) { + if (deepLinkData == null || + deepLinkData.action.toLowerCase() != AppConfig.openAppHostDeepLink.toLowerCase() || + deepLinkData.username?.isNotEmpty != true || + username?.isNotEmpty != true || + deepLinkData.username == username || + currentContext == 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(); - } + _showConfirmDialogSwitchAccount( + context: currentContext!, + currentUsername: username!, + newUsername: deepLinkData.username!, + onConfirmAction: () => onConfirmCallback(deepLinkData), + onCancelAction: onFailureCallback, + ); } void _handleOpenApp({ @@ -216,7 +232,8 @@ class DeepLinksManager with MessageDialogActionMixin { void _showConfirmDialogSwitchAccount({ required BuildContext context, - required String username, + required String currentUsername, + required String newUsername, required Function onConfirmAction, required Function onCancelAction, }) { @@ -226,7 +243,7 @@ class DeepLinksManager with MessageDialogActionMixin { context, '', appLocalizations.yesLogout, - title: appLocalizations.logoutConfirmation, + title: appLocalizations.swithAccountConfirmation, alignCenter: true, outsideDismissible: false, titleActionButtonMaxLines: 1, @@ -239,7 +256,16 @@ class DeepLinksManager with MessageDialogActionMixin { listTextSpan: [ TextSpan(text: appLocalizations.doYouWantToLogoutOf), TextSpan( - text: ' $username', + text: ' $currentUsername ', + style: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + TextSpan(text: appLocalizations.andSwitchAccount), + TextSpan( + text: ' $newUsername', style: const TextStyle( color: AppColor.colorTextBody, fontSize: 15, diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 1ead12b69f..904a996314 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4274,10 +4274,10 @@ class AppLocalizations { ); } - String get logoutConfirmation { + String get swithAccountConfirmation { return Intl.message( - 'Logout Confirmation', - name: 'logoutConfirmation', + 'Switch Account Confirmation', + name: 'swithAccountConfirmation', ); } @@ -4294,4 +4294,11 @@ class AppLocalizations { name: 'doYouWantToLogoutOf', ); } + + String get andSwitchAccount { + return Intl.message( + 'and switch account', + name: 'andSwitchAccount', + ); + } } From cc2c216ff9f85f21001681a1615e6f73f919d569 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 20 Nov 2024 14:39:27 +0700 Subject: [PATCH 7/7] fixup! TF-3278 Handle open app via deep link at MailboxDashboard screen --- lib/main/deep_links/deep_links_manager.dart | 4 +- test/main/deep_links/deep_links_manager.dart | 41 ++++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/main/deep_links/deep_links_manager.dart b/lib/main/deep_links/deep_links_manager.dart index c2fa3098f4..12e2bad279 100644 --- a/lib/main/deep_links/deep_links_manager.dart +++ b/lib/main/deep_links/deep_links_manager.dart @@ -63,7 +63,9 @@ class DeepLinksManager with MessageDialogActionMixin { DeepLinkData? parseDeepLink(String url) { try { - final updatedUrl = url.replaceFirst( + final decodedUrl = Uri.decodeFull(url); + + final updatedUrl = decodedUrl.replaceFirst( OIDCConstant.twakeWorkplaceUrlScheme, 'https', ); diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart index e55053db02..469fc8b2b6 100644 --- a/test/main/deep_links/deep_links_manager.dart +++ b/test/main/deep_links/deep_links_manager.dart @@ -11,8 +11,8 @@ void main() { 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'; + test('SHOULD parse valid URL with all parameters', () { + const url = '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', @@ -21,31 +21,48 @@ void main() { expiresIn: 3600, username: 'user@example.com', ); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); + final deepLinkData = deepLinkManager.parseDeepLink(url); expect(deepLinkData, equals(expectedData)); }); - test('Deep link with no query parameters', () { - const deepLink = 'twake.mail://openApp'; + test('SHOULD handle URL with missing parameters', () { + const url = 'twake.mail://openApp'; final expectedData = DeepLinkData(action: 'openapp'); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); + final deepLinkData = deepLinkManager.parseDeepLink(url); expect(deepLinkData, expectedData); }); - test('Deep link with one query parameter', () { - const deepLink = 'twake.mail://openApp?access_token=ey123456'; + test('SHOULD parse valid URL with one query parameter', () { + const url = 'twake.mail://openApp?access_token=ey123456'; final expectedData = DeepLinkData(action: 'openapp', accessToken: 'ey123456',); - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); + final deepLinkData = deepLinkManager.parseDeepLink(url); expect(deepLinkData, equals(expectedData)); }); - test('Invalid deep link format', () { - const deepLink = 'Invalid link: invalid'; - final deepLinkData = deepLinkManager.parseDeepLink(deepLink); + test('SHOULD return null for an invalid URL', () { + const url = 'Invalid link: invalid'; + final deepLinkData = deepLinkManager.parseDeepLink(url); expect(deepLinkData, isNull); }); + + test('SHOULD decode encoded URL correctly', () { + const encodedUrl = 'twake.mail://openApp?access_token=abc%20123&username=test%20user'; + final result = deepLinkManager.parseDeepLink(encodedUrl); + + expect(result, isNotNull); + expect(result!.accessToken, 'abc 123'); + expect(result.username, 'test user'); + }); + + test('SHOULD return null WHEN Uri.decodeFull throws an exception', () { + const invalidUrl = 'twake-workplace://login?access_token=%ZZ'; + + final result = deepLinkManager.parseDeepLink(invalidUrl); + + expect(result, isNull); + }); }); } \ No newline at end of file