diff --git a/lib/features/login/data/network/config/oidc_constant.dart b/lib/features/login/data/network/config/oidc_constant.dart index 894079136..27793be72 100644 --- a/lib/features/login/data/network/config/oidc_constant.dart +++ b/lib/features/login/data/network/config/oidc_constant.dart @@ -8,4 +8,8 @@ class OIDCConstant { static const authResponseKey = "auth_info"; static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId; + + static String get saasClientId => PlatformInfo.isMobile + ? mobileOidcClientId + : ''; } \ No newline at end of file diff --git a/lib/features/login/domain/exceptions/authentication_exception.dart b/lib/features/login/domain/exceptions/authentication_exception.dart index 2fd97c29e..170b5e6d2 100644 --- a/lib/features/login/domain/exceptions/authentication_exception.dart +++ b/lib/features/login/domain/exceptions/authentication_exception.dart @@ -44,4 +44,8 @@ class CanNotFoundPassword implements Exception {} class CanNotAuthenticationInfoOnWeb implements Exception {} -class NotFoundAuthenticationInfoCache implements Exception {} \ No newline at end of file +class NotFoundAuthenticationInfoCache implements Exception {} + +class CanNotFoundSaasServerUrl implements Exception {} + +class SaasServerUriIsNull implements Exception {} \ No newline at end of file diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index 4b24ca495..bb051da49 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -9,6 +9,7 @@ import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart' import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; +import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/dns_lookup_input_form.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/horizontal_progress_loading_button.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/login_back_button.dart'; diff --git a/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart b/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart new file mode 100644 index 000000000..2ac9a755f --- /dev/null +++ b/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart @@ -0,0 +1,7 @@ + +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; + +abstract class SaasAuthenticationDataSource { + Future signIn(OIDCConfiguration oidcConfiguration); +} \ No newline at end of file diff --git a/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart b/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart new file mode 100644 index 000000000..db533c33f --- /dev/null +++ b/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart @@ -0,0 +1,29 @@ +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; +import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class SaasAuthenticationDataSourceImpl extends SaasAuthenticationDataSource { + + final AuthenticationClientBase _authenticationClient; + final ExceptionThrower _exceptionThrower; + + SaasAuthenticationDataSourceImpl( + this._authenticationClient, + this._exceptionThrower, + ); + + @override + Future signIn(OIDCConfiguration oidcConfiguration) { + return Future.sync(() async { + return await _authenticationClient.getTokenOIDC( + oidcConfiguration.clientId, + oidcConfiguration.redirectUrl, + oidcConfiguration.discoveryUrl, + oidcConfiguration.scopes, + ); + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart b/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart new file mode 100644 index 000000000..d334e8979 --- /dev/null +++ b/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart @@ -0,0 +1,16 @@ +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart'; + +class SaasAuthenticationRepositoryImpl extends SaasAuthenticationRepository { + + final SaasAuthenticationDataSource _saasAuthenticationDataSource; + + SaasAuthenticationRepositoryImpl(this._saasAuthenticationDataSource); + + @override + Future signIn(OIDCConfiguration oidcConfiguration) { + return _saasAuthenticationDataSource.signIn(oidcConfiguration); + } +} \ No newline at end of file diff --git a/lib/features/starting_page/domain/repository/saas_authentication_repository.dart b/lib/features/starting_page/domain/repository/saas_authentication_repository.dart new file mode 100644 index 000000000..d5ef10623 --- /dev/null +++ b/lib/features/starting_page/domain/repository/saas_authentication_repository.dart @@ -0,0 +1,6 @@ +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; + +abstract class SaasAuthenticationRepository { + Future signIn(OIDCConfiguration oidcConfiguration); +} \ No newline at end of file diff --git a/lib/features/starting_page/domain/state/sign_in_saas_state.dart b/lib/features/starting_page/domain/state/sign_in_saas_state.dart new file mode 100644 index 000000000..67aa56f71 --- /dev/null +++ b/lib/features/starting_page/domain/state/sign_in_saas_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 SignInSaasLoading extends LoadingState {} + +class SignInSaasSuccess extends Success { + final TokenOIDC tokenOIDC; + final Uri baseUri; + final OIDCConfiguration oidcConfiguration; + + SignInSaasSuccess(this.tokenOIDC, this.baseUri, this.oidcConfiguration); + + @override + List get props => [tokenOIDC, baseUri, oidcConfiguration]; +} + +class SignInSaasFailure extends FeatureFailure { + SignInSaasFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/starting_page/domain/usecase/sign_in_saas_interactor.dart b/lib/features/starting_page/domain/usecase/sign_in_saas_interactor.dart new file mode 100644 index 000000000..74117e54f --- /dev/null +++ b/lib/features/starting_page/domain/usecase/sign_in_saas_interactor.dart @@ -0,0 +1,66 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/services.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:rich_text_composer/views/commons/logger.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.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/starting_page/domain/repository/saas_authentication_repository.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_saas_state.dart'; + +class SignInSaasInteractor { + final SaasAuthenticationRepository _saasRepository; + final AuthenticationOIDCRepository _authenticationOIDCRepository; + final AccountRepository _accountRepository; + final CredentialRepository _credentialRepository; + + SignInSaasInteractor( + this._saasRepository, + this._authenticationOIDCRepository, + this._accountRepository, + this._credentialRepository + ); + + Stream> execute({ + required Uri baseUri, + required OIDCConfiguration oidcConfiguration + }) async* { + try { + yield Right(SignInSaasLoading()); + + final tokenOIDC = await _saasRepository.signIn(oidcConfiguration); + + 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(SignInSaasSuccess(tokenOIDC, baseUri, oidcConfiguration)); + } on PlatformException catch (e) { + logError('SignInSaasInteractor::execute():PlatformException ${e.message} - ${e.stacktrace}'); + if (NoSuitableBrowserForOIDCException.verifyException(e)) { + yield Left(SignInSaasFailure(NoSuitableBrowserForOIDCException())); + } else { + yield Left(SignInSaasFailure(e)); + } + } catch (e) { + logError('SignInSaasInteractor::execute():Exception = $e'); + yield Left(SignInSaasFailure(e)); + } + } +} \ No newline at end of file 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 91f03c4c3..a33b32292 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,9 +1,60 @@ 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'; +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/starting_page/data/datasource/saas_authentication_datasource.dart'; +import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart'; +import 'package:tmail_ui_user/features/starting_page/data/repository/saas_authentication_repository_impl.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_saas_interactor.dart'; import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class TwakeWelcomeBindings extends BaseBindings { -class TwakeWelcomeBindings extends Bindings { @override - void dependencies() { - Get.lazyPut(() => TwakeWelcomeController()); + void bindingsController() { + Get.lazyPut(() => TwakeWelcomeController( + Get.find(), + )); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => SignInSaasInteractor( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => SaasAuthenticationDataSourceImpl( + Get.find(), + Get.find(), + )); + } + + @override + void bindingsDataSource() { + Get.lazyPut( + () => Get.find()); + } + + @override + void bindingsRepository() { + Get.lazyPut( + () => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => SaasAuthenticationRepositoryImpl( + Get.find(), + )); } } \ 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 c17a745fc..662347759 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,12 +1,25 @@ -import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.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/login/data/network/config/oidc_constant.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; 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_saas_state.dart'; +import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_saas_interactor.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/utils/app_utils.dart'; -class TwakeWelcomeController extends BaseController { +class TwakeWelcomeController extends ReloadableController { + + final SignInSaasInteractor _signInSaasInteractor; + + TwakeWelcomeController(this._signInSaasInteractor); void handleUseCompanyServer() { popAndPush( @@ -17,4 +30,55 @@ class TwakeWelcomeController extends BaseController { void onClickPrivacyPolicy() { AppUtils.launchLink(AppConfig.linagoraPrivacyUrl); } + + void onClickSignIn() { + if (AppConfig.saasServerUrl.isEmpty) { + consumeState(Stream.value(Left(SignInSaasFailure(CanNotFoundSaasServerUrl())))); + return; + } + + final baseUri = Uri.tryParse(AppConfig.saasServerUrl); + + if (baseUri == null) { + consumeState(Stream.value(Left(SignInSaasFailure(SaasServerUriIsNull())))); + return; + } + + consumeState(_signInSaasInteractor.execute( + baseUri: baseUri, + oidcConfiguration: OIDCConfiguration( + authority: AppConfig.registrationUrl, + clientId: OIDCConstant.saasClientId, + scopes: AppConfig.oidcScopes + ) + )); + } + + @override + void handleSuccessViewState(Success success) { + if (success is SignInSaasSuccess) { + _handleSignInSaasSuccess(success); + } else { + super.handleSuccessViewState(success); + } + } + + @override + void handleReloaded(Session session) { + popAndPush( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: session); + } + + void _handleSignInSaasSuccess(SignInSaasSuccess success) { + dynamicUrlInterceptors.setJmapUrl(success.baseUri.toString()); + dynamicUrlInterceptors.changeBaseUrl(success.baseUri.toString()); + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: success.tokenOIDC, + newConfig: success.oidcConfiguration); + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + newToken: success.tokenOIDC, + newConfig: success.oidcConfiguration); + getSessionAction(); + } } \ No newline at end of file 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 1bddbed67..b95b20219 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 @@ -6,6 +6,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.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'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class TwakeWelcomeView extends GetWidget { @@ -31,8 +32,13 @@ class TwakeWelcomeView extends GetWidget { hoverColor: Colors.transparent, highlightColor: Colors.transparent, overlayColor: WidgetStateProperty.all(Colors.transparent), - signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '', - createTwakeIdTitle: AppLocalizations.of(context).createTwakeId, + signInTitle: AppConfig.isSaasPlatForm + ? AppLocalizations.of(context).signIn.capitalizeFirst ?? '' + : null, + onSignInOnTap: controller.onClickSignIn, + createTwakeIdTitle: AppConfig.isSaasPlatForm + ? AppLocalizations.of(context).createTwakeId + : null, useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer, description: AppLocalizations.of(context).descriptionWelcomeTo, privacyPolicy: AppLocalizations.of(context).privacyPolicy, diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index 2de4112e6..fdb99d350 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -66,4 +66,8 @@ class AppConfig { static String get _platformEnv => dotenv.get('PLATFORM', fallback: 'other'); static bool get isSaasPlatForm => _platformEnv.toLowerCase() == saasPlatform; + + static String get registrationUrl => dotenv.get('REGISTRATION_URL', fallback: ''); + + static String get saasServerUrl => dotenv.get('SAAS_SERVER_URL', fallback: ''); } \ No newline at end of file diff --git a/model/lib/oidc/oidc_configuration.dart b/model/lib/oidc/oidc_configuration.dart index ebec65a3d..37732695f 100644 --- a/model/lib/oidc/oidc_configuration.dart +++ b/model/lib/oidc/oidc_configuration.dart @@ -25,8 +25,6 @@ class OIDCConfiguration with EquatableMixin { } } - String get clientIdHash => clientId.hashCode.toString(); - @override List get props => [ authority,