From c6ed9f7c2bbfe37edd379c19a9b5a45655a4f6b2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 10 Dec 2024 15:21:48 +0700 Subject: [PATCH] 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 ++++- .../reloadable/reloadable_controller.dart | 2 +- .../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 | 256 +++++++++++++++--- lib/l10n/intl_messages.arb | 6 + lib/main/deep_links/deep_link_data.dart | 6 +- lib/main/deep_links/deep_links_manager.dart | 25 +- lib/main/localizations/app_localizations.dart | 7 + test/main/deep_links/deep_links_manager.dart | 46 ++++ 11 files changed, 453 insertions(+), 54 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 fe483b0a4..1aade6470 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -473,6 +473,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) { @@ -503,10 +563,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/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index cd65c4520..91314f1ff 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -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, 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 000000000..1db16ab29 --- /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 000000000..90ade7c86 --- /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 2e75157d2..fe4ff19dc 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 77db04ebf..ce661d701 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; - } - - 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(); } } - 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; - } + void _handleOpenApp(DeepLinkData deepLinkData) { + _autoSignInViaDeepLink(deepLinkData); + } + Future _handleDeepLinksSignedIn(Success authenticationViewStateSuccess) async { final deepLinkData = await _deepLinksManager?.getDeepLinkData(); log('HomeController::_handleDeepLinksSignedIn:DeepLinkData = $deepLinkData'); if (deepLinkData == null) { - super.handleSuccessViewState(success); + _continueUsingTheApp(authenticationViewStateSuccess); return; } - if (deepLinkData.path == AppConfig.openAppHostDeepLink) { - if (deepLinkData.username?.isNotEmpty != true) { - _continueUsingTheApp(success); + 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 == username) { - _continueUsingTheApp(success); + 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); } - - super.handleSuccessViewState(success); } - void _continueUsingTheApp(Success success) { - super.handleSuccessViewState(success); + void _continueUsingTheApp(Success authenticationViewStateSuccess) { + log('HomeController::_continueUsingTheApp:'); + super.handleSuccessViewState(authenticationViewStateSuccess); + } + + PersonalAccount? _getPersonalAccountFromViewStateSuccess(Success success) { + if (success is GetCredentialViewState) { + return success.personalAccount; + } else if (success is GetStoredTokenOidcSuccess) { + return success.personalAccount; + } + return null; + } + + void _showConfirmDialogSwitchAccount({ + required String username, + required Function onConfirmAction, + required Function onCancelAction, + }) { + final appLocalizations = AppLocalizations.of(currentContext!); + + showConfirmDialogAction( + currentContext!, + '', + appLocalizations.yesLogout, + title: appLocalizations.logoutConfirmation, + alignCenter: true, + outsideDismissible: false, + titleActionButtonMaxLines: 1, + titlePadding: const EdgeInsetsDirectional.only(start: 24, top: 24, end: 24, bottom: 12), + messageStyle: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.w400, + ), + listTextSpan: [ + TextSpan(text: appLocalizations.messageConfirmationLogout), + TextSpan( + text: ' $username', + style: const TextStyle( + color: AppColor.colorTextBody, + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + const TextSpan(text: ' ?'), + ], + onConfirmAction: onConfirmAction, + onCancelAction: onCancelAction + ); + } + + void _handleLogOutAndSignInNewAccount({ + required Success authenticationViewStateSuccess, + required PersonalAccount personalAccount, + 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; + } + + 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; + } + + 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(), + 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 c14feccd4..f2390b931 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4118,6 +4118,12 @@ "placeholders_order": [], "placeholders": {} }, + "tokenInvalid": "Token invalid", + "@tokenInvalid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "getHelpOrReportABug": "Get help or report a bug", "@getHelpOrReportABug": { "type": "text", diff --git a/lib/main/deep_links/deep_link_data.dart b/lib/main/deep_links/deep_link_data.dart index f7ff0ee5d..7a9b37cea 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 73d437f97..1e8f0357b 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 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 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 0971b760e..98bdc2842 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4315,6 +4315,13 @@ class AppLocalizations { ); } + String get tokenInvalid { + return Intl.message( + 'Token invalid', + name: 'tokenInvalid', + ); + } + String get getHelpOrReportABug { return Intl.message( 'Get help or report a bug', diff --git a/test/main/deep_links/deep_links_manager.dart b/test/main/deep_links/deep_links_manager.dart new file mode 100644 index 000000000..0d1de200c --- /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