TF-3278 Handle open app via deep link at MailboxDashboard screen

This commit is contained in:
dab246
2024-11-22 23:30:37 +07:00
committed by Dat H. Pham
parent 8022559bc2
commit 223fdbf1fb
59 changed files with 1058 additions and 429 deletions
+1
View File
@@ -52,6 +52,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart'; export 'utils/list_utils.dart';
export 'utils/mail/domain.dart'; export 'utils/mail/domain.dart';
export 'utils/mail/mail_address.dart'; export 'utils/mail/mail_address.dart';
export 'utils/application_manager.dart';
export 'utils/preview_eml_file_utils.dart'; export 'utils/preview_eml_file_utils.dart';
// Views // Views
@@ -2,7 +2,18 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
typedef OnFailureCallback = void Function(Failure? failure);
typedef OnSuccessCallback<T> = void Function(T success);
extension EitherViewStateExtension on Either<Failure, Success> { extension EitherViewStateExtension on Either<Failure, Success> {
void foldSuccess<T>({
required OnSuccessCallback<T> onSuccess,
required OnFailureCallback onFailure,
}) {
fold(onFailure,
(success) => success is T ? onSuccess(success as T) : onFailure(null));
}
dynamic foldSuccessWithResult<T>() { dynamic foldSuccessWithResult<T>() {
return fold( return fold(
(failure) => failure, (failure) => failure,
+10 -2
View File
@@ -1,8 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/domain/exceptions/string_exception.dart'; import 'package:core/domain/exceptions/string_exception.dart';
class StringConvert { class StringConvert {
@@ -17,6 +16,15 @@ class StringConvert {
return text ?? ''; return text ?? '';
} }
static String decodeBase64ToString(String text) {
try {
return utf8.decode(base64Decode(text));
} catch (e) {
logError('StringConvert::decodeBase64ToString:Exception = $e');
return text;
}
}
static List<String> extractStrings(String input) { static List<String> extractStrings(String input) {
try { try {
// Check if the input is URL encoded // Check if the input is URL encoded
@@ -0,0 +1,79 @@
import 'package:core/presentation/extensions/either_view_state_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dartz/dartz.dart';
class MockFailure extends Failure {
final String message;
MockFailure(this.message);
@override
List<Object?> get props => [message];
}
class MockSuccess extends Success {
final String data;
MockSuccess(this.data);
@override
List<Object?> get props => [data];
}
class AnotherSuccess extends Success {
@override
List<Object?> get props => [];
}
void main() {
group('EitherViewStateExtension::foldSuccess::test', () {
test('Should calls onFailure when Either is Left', () {
final either = Left<Failure, Success>(MockFailure('Error occurred'));
bool failureCalled = false;
either.foldSuccess<Success>(
onSuccess: (_) => fail('onSuccess should not be called'),
onFailure: (failure) {
failureCalled = true;
expect(failure, isNotNull);
expect(failure, isA<MockFailure>());
},
);
expect(failureCalled, isTrue);
});
test('Should calls onSuccess when Either is Right with matching type', () {
final either = Right<Failure, Success>(MockSuccess('Successful'));
bool successCalled = false;
either.foldSuccess<Success>(
onSuccess: (success) {
successCalled = true;
expect(success, isNotNull);
expect(success, isA<MockSuccess>());
},
onFailure: (_) => fail('onFailure should not be called'),
);
expect(successCalled, isTrue);
});
test('Should calls onFailure when Either is Right with non-matching type', () {
final either = Right<Failure, Success>(MockSuccess('Successful'));
bool failureCalled = false;
either.foldSuccess<AnotherSuccess>(
onSuccess: (_) => fail('onSuccess should not be called'),
onFailure: (failure) {
failureCalled = true;
expect(failure, isNull);
},
);
expect(failureCalled, isTrue);
});
});
}
+36
View File
@@ -5,6 +5,42 @@ import 'package:core/utils/string_convert.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
group('StringConvert::decodeBase64ToString::test', () {
test('should decode a valid Base64 string to a normal string', () {
// Arrange
const base64Encoded = 'SGVsbG8gV29ybGQh';
const expectedDecoded = 'Hello World!';
// Act
final result = StringConvert.decodeBase64ToString(base64Encoded);
// Assert
expect(result, expectedDecoded);
});
test('should return the original string for invalid Base64 input', () {
// Arrange
const invalidBase64 = 'InvalidBase64@@';
// Act
final result = StringConvert.decodeBase64ToString(invalidBase64);
// Assert
expect(result, invalidBase64);
});
test('should return the original string for empty input', () {
// Arrange
const emptyInput = '';
// Act
final result = StringConvert.decodeBase64ToString(emptyInput);
// Assert
expect(result, emptyInput);
});
});
group('StringConvert::extractStrings::', () { group('StringConvert::extractStrings::', () {
group('Basic Functionality', () { group('Basic Functionality', () {
test('should extract strings separated by spaces', () { test('should extract strings separated by spaces', () {
+6
View File
@@ -1,4 +1,6 @@
PODS: PODS:
- app_links (0.0.2):
- Flutter
- app_settings (5.1.1): - app_settings (5.1.1):
- Flutter - Flutter
- AppAuth (1.7.4): - AppAuth (1.7.4):
@@ -204,6 +206,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- app_settings (from `.symlinks/plugins/app_settings/ios`) - app_settings (from `.symlinks/plugins/app_settings/ios`)
- better_open_file (from `.symlinks/plugins/better_open_file/ios`) - better_open_file (from `.symlinks/plugins/better_open_file/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
@@ -264,6 +267,8 @@ SPEC REPOS:
- UniversalDetector2 - UniversalDetector2
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
app_settings: app_settings:
:path: ".symlinks/plugins/app_settings/ios" :path: ".symlinks/plugins/app_settings/ios"
better_open_file: better_open_file:
@@ -334,6 +339,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios" :path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
AppAuth: 182c5b88630569df5acb672720534756c29b3358 AppAuth: 182c5b88630569df5acb672720534756c29b3358
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
+1
View File
@@ -33,6 +33,7 @@
<array> <array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>teammail.mobile</string> <string>teammail.mobile</string>
<string>twakemail.mobile</string>
<string>mailto</string> <string>mailto</string>
</array> </array>
</dict> </dict>
+3 -1
View File
@@ -24,6 +24,7 @@ class JmapClient {
basicAuth: String?, basicAuth: String?,
tokenEndpointUrl: String?, tokenEndpointUrl: String?,
oidcScopes: [String]?, oidcScopes: [String]?,
isTWP: Bool?,
onComplete: @escaping ([Email], [Error]) -> Void onComplete: @escaping ([Email], [Error]) -> Void
) { ) {
if (authenticationType == AuthenticationType.basic) { if (authenticationType == AuthenticationType.basic) {
@@ -42,7 +43,8 @@ class JmapClient {
tokenRefreshManager = TokenRefreshManager( tokenRefreshManager = TokenRefreshManager(
refreshToken: tokenOidc?.refreshToken ?? "", refreshToken: tokenOidc?.refreshToken ?? "",
tokenEndpoint: tokenEndpointUrl ?? "", tokenEndpoint: tokenEndpointUrl ?? "",
scopes: oidcScopes scopes: oidcScopes,
isTWP: isTWP
) )
} }
@@ -5,6 +5,7 @@ class TokenRefreshManager {
private let MOBIE_CLIENT_ID = "teammail-mobile" private let MOBIE_CLIENT_ID = "teammail-mobile"
private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect" private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect"
private let OIDC_SCOPES = ["openid", "profile", "email", "offline_access"] private let OIDC_SCOPES = ["openid", "profile", "email", "offline_access"]
private let TWP_MOBIE_REDIRECT_URL = "twakemail.mobile://redirect"
private let GRANT_TYPE = "grant_type" private let GRANT_TYPE = "grant_type"
private let REFRESH_TOKEN = "refresh_token" private let REFRESH_TOKEN = "refresh_token"
@@ -15,11 +16,13 @@ class TokenRefreshManager {
let refreshToken: String let refreshToken: String
let tokenEndpoint: String let tokenEndpoint: String
let scopes: [String]? let scopes: [String]?
let isTWP: Bool?
init(refreshToken: String, tokenEndpoint: String, scopes: [String]?) { init(refreshToken: String, tokenEndpoint: String, scopes: [String]?, isTWP: Bool?) {
self.refreshToken = refreshToken self.refreshToken = refreshToken
self.tokenEndpoint = tokenEndpoint self.tokenEndpoint = tokenEndpoint
self.scopes = scopes self.scopes = scopes
self.isTWP = isTWP
} }
private func getScopes() -> String { private func getScopes() -> String {
@@ -29,6 +32,14 @@ class TokenRefreshManager {
return OIDC_SCOPES.joined(separator: " ") return OIDC_SCOPES.joined(separator: " ")
} }
private func getRedirectUrl() -> String {
if (isTWP == true) {
return TWP_MOBIE_REDIRECT_URL
} else {
return MOBIE_REDIRECT_URL
}
}
func handleRefreshAccessToken( func handleRefreshAccessToken(
onSuccess: @escaping (TokenResponse) -> Void, onSuccess: @escaping (TokenResponse) -> Void,
onFailure: @escaping (Error) -> Void onFailure: @escaping (Error) -> Void
@@ -41,7 +52,7 @@ class TokenRefreshManager {
let params = [ let params = [
CLIENT_ID: MOBIE_CLIENT_ID, CLIENT_ID: MOBIE_CLIENT_ID,
GRANT_TYPE: REFRESH_TOKEN, GRANT_TYPE: REFRESH_TOKEN,
REDIRECT_URI: MOBIE_REDIRECT_URL, REDIRECT_URI: getRedirectUrl(),
REFRESH_TOKEN: refreshToken, REFRESH_TOKEN: refreshToken,
SCOPES: getScopes(), SCOPES: getScopes(),
] ]
@@ -12,6 +12,7 @@ struct KeychainSharingSession: Codable {
let tokenEndpoint: String? let tokenEndpoint: String?
let oidcScopes: [String]? let oidcScopes: [String]?
let mailboxIdsBlockNotification: [String]? let mailboxIdsBlockNotification: [String]?
let isTWP: Bool?
} }
extension KeychainSharingSession { extension KeychainSharingSession {
@@ -27,7 +28,8 @@ extension KeychainSharingSession {
basicAuth: self.basicAuth, basicAuth: self.basicAuth,
tokenEndpoint: self.tokenEndpoint, tokenEndpoint: self.tokenEndpoint,
oidcScopes: self.oidcScopes, oidcScopes: self.oidcScopes,
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
isTWP: self.isTWP
) )
} }
@@ -48,7 +50,8 @@ extension KeychainSharingSession {
basicAuth: self.basicAuth, basicAuth: self.basicAuth,
tokenEndpoint: self.tokenEndpoint, tokenEndpoint: self.tokenEndpoint,
oidcScopes: self.oidcScopes, oidcScopes: self.oidcScopes,
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
isTWP: self.isTWP
) )
} }
@@ -82,6 +82,7 @@ class NotificationService: UNNotificationServiceExtension {
basicAuth: keychainSharingSession.basicAuth, basicAuth: keychainSharingSession.basicAuth,
tokenEndpointUrl: keychainSharingSession.tokenEndpoint, tokenEndpointUrl: keychainSharingSession.tokenEndpoint,
oidcScopes: keychainSharingSession.oidcScopes, oidcScopes: keychainSharingSession.oidcScopes,
isTWP: keychainSharingSession.isTWP,
onComplete: { (emails, errors) in onComplete: { (emails, errors) in
do { do {
if emails.isEmpty { if emails.isEmpty {
+31 -30
View File
@@ -1,19 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:core/core.dart';
import 'package:flutter/services.dart' as services;
import 'package:contact/contact/model/capability_contact.dart'; import 'package:contact/contact/model/capability_contact.dart';
import 'package:core/data/network/config/dynamic_url_interceptors.dart';
import 'package:core/domain/exceptions/platform_exception.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/app_toast.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/views/toast/tmail_toast.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/application_manager.dart';
import 'package:core/utils/fps_manager.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:fcm/model/firebase_capability.dart'; import 'package:fcm/model/firebase_capability.dart';
import 'package:fcm/model/firebase_registration_id.dart'; import 'package:fcm/model/firebase_registration_id.dart';
@@ -34,7 +22,9 @@ import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixi
import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart';
import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/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_authority_oidc_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_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'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
@@ -203,6 +193,8 @@ abstract class BaseController extends GetxController
clearDataAndGoToLoginPage(); clearDataAndGoToLoginPage();
} }
void onCancelReconnectWhenSessionExpired() {}
void _handleConnectionErrorException() { void _handleConnectionErrorException() {
if (currentOverlayContext != null && currentContext != null) { if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage( appToast.showToastErrorMessage(
@@ -241,7 +233,9 @@ abstract class BaseController extends GetxController
outsideDismissible: false, outsideDismissible: false,
titleActionButtonMaxLines: 1, titleActionButtonMaxLines: 1,
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64), icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
onConfirmAction: _executeBeforeReconnectAndLogOut); onConfirmAction: _executeBeforeReconnectAndLogOut,
onCancelAction: onCancelReconnectWhenSessionExpired
);
} else if (PlatformInfo.isMobile) { } else if (PlatformInfo.isMobile) {
if (currentContext == null) { if (currentContext == null) {
clearDataAndGoToLoginPage(); clearDataAndGoToLoginPage();
@@ -257,7 +251,9 @@ abstract class BaseController extends GetxController
outsideDismissible: false, outsideDismissible: false,
titleActionButtonMaxLines: 1, titleActionButtonMaxLines: 1,
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64), icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
onConfirmAction: clearDataAndGoToLoginPage); onConfirmAction: clearDataAndGoToLoginPage,
onCancelAction: onCancelReconnectWhenSessionExpired
);
} }
} }
@@ -477,20 +473,22 @@ abstract class BaseController extends GetxController
required Session session, required Session session,
required AccountId accountId, required AccountId accountId,
required Function onSuccessCallback, required Function onSuccessCallback,
required Function onFailureCallback, required Function({Object? exception}) onFailureCallback,
}) async { }) async {
try { try {
_isFcmEnabled = _isFcmActivated(session, accountId); _isFcmEnabled = _isFcmActivated(session, accountId);
if (isAuthenticatedWithOidc) { if (isAuthenticatedWithOidc) {
final logoutViewState = await logoutOidcInteractor.execute().last; final logoutViewState = await logoutOidcInteractor.execute().last;
logoutViewState.foldSuccess<LogoutOidcSuccess>(
logoutViewState.fold( onSuccess: (success) async {
(failure) => onFailureCallback(), await _handleDeleteFCMAndClearData();
(success) async { onSuccessCallback();
if (success is LogoutOidcSuccess) { },
onFailure: (failure) async {
if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) {
await _handleDeleteFCMAndClearData(); await _handleDeleteFCMAndClearData();
onSuccessCallback(); onFailureCallback(exception: UserCancelledLogoutOIDCFlowException());
} else { } else {
onFailureCallback(); onFailureCallback();
} }
@@ -506,6 +504,11 @@ abstract class BaseController extends GetxController
} }
} }
bool _validateUserCancelledLogoutOidcFlow(dynamic exception) {
return exception is services.PlatformException &&
exception.code == OIDCConstant.endSessionFailedCode;
}
Future<void> _handleDeleteFCMAndClearData() async { Future<void> _handleDeleteFCMAndClearData() async {
await Future.wait([ await Future.wait([
if (_isFcmEnabled) if (_isFcmEnabled)
@@ -519,14 +522,12 @@ abstract class BaseController extends GetxController
_getStoredFirebaseRegistrationInteractor = getBinding<GetStoredFirebaseRegistrationInteractor>(); _getStoredFirebaseRegistrationInteractor = getBinding<GetStoredFirebaseRegistrationInteractor>();
final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last; final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last;
fcmRegistration?.fold( fcmRegistration?.foldSuccess<GetStoredFirebaseRegistrationSuccess>(
(failure) => null, onSuccess: (success) async {
(success) async { _destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
if (success is GetStoredFirebaseRegistrationSuccess) { await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
_destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
}
}, },
onFailure: (failure) {},
); );
} catch (e) { } catch (e) {
logError('BaseController::_handleDeleteFCMRegistration:Exception = $e'); logError('BaseController::_handleDeleteFCMRegistration:Exception = $e');
@@ -34,7 +34,7 @@ abstract class ReloadableController extends BaseController {
goToLogin(); goToLogin();
} else if (failure is GetSessionFailure) { } else if (failure is GetSessionFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure'); logError('$runtimeType::handleFailureViewState():Failure = $failure');
handleGetSessionFailure(failure.exception); handleGetSessionFailure(failure);
} else if (failure is UpdateAccountCacheFailure) { } else if (failure is UpdateAccountCacheFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure'); logError('$runtimeType::handleFailureViewState():Failure = $failure');
_handleUpdateAccountCacheCompleted( _handleUpdateAccountCacheCompleted(
@@ -0,0 +1,12 @@
import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
class OidcConfigurationCacheClient extends HiveCacheClient<OidcConfigurationCache> {
@override
String get tableName => CachingConstants.oidcConfigurationCacheBoxName;
@override
bool get encryption => true;
}
@@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/login/data/local/encryption_key_cache_man
import 'package:tmail_ui_user/features/login/data/model/account_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/account_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/encryption_key_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/encryption_key_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/recent_login_username_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_username_cache.dart';
import 'package:tmail_ui_user/features/login/data/model/token_oidc_cache.dart'; import 'package:tmail_ui_user/features/login/data/model/token_oidc_cache.dart';
@@ -185,6 +186,10 @@ class HiveCacheConfig {
SessionHiveObjAdapter(), SessionHiveObjAdapter(),
CachingConstants.SESSION_HIVE_CACHE_ID CachingConstants.SESSION_HIVE_CACHE_ID
); );
registerCacheAdapter<OidcConfigurationCache>(
OidcConfigurationCacheAdapter(),
CachingConstants.OIDC_CONFIGURATION_CACHE_ID,
);
} }
void registerCacheAdapter<T>(TypeAdapter<T> typeAdapter, int typeId) { void registerCacheAdapter<T>(TypeAdapter<T> typeAdapter, int typeId) {
@@ -19,6 +19,7 @@ class CachingConstants {
static const int DETAILED_EMAIL_HIVE_CACHE_ID = 17; static const int DETAILED_EMAIL_HIVE_CACHE_ID = 17;
static const int SENDING_EMAIL_HIVE_CACHE_ID = 18; static const int SENDING_EMAIL_HIVE_CACHE_ID = 18;
static const int SESSION_HIVE_CACHE_ID = 19; static const int SESSION_HIVE_CACHE_ID = 19;
static const int OIDC_CONFIGURATION_CACHE_ID = 20;
static const String fcmCacheBoxName = 'fcm_cache_box'; static const String fcmCacheBoxName = 'fcm_cache_box';
static const String newEmailCacheBoxName = 'new_email_cache_box'; static const String newEmailCacheBoxName = 'new_email_cache_box';
@@ -26,6 +27,9 @@ class CachingConstants {
static const String sendingEmailCacheBoxName = 'sending_email_cache_box'; static const String sendingEmailCacheBoxName = 'sending_email_cache_box';
static const String sessionCacheBoxName = 'session_cache_box'; static const String sessionCacheBoxName = 'session_cache_box';
static const String firebaseRegistrationCacheBoxName = 'firebase_registration_cache_box'; static const String firebaseRegistrationCacheBoxName = 'firebase_registration_cache_box';
static const String oidcConfigurationCacheBoxName = 'oidc_configuration_cache_box';
static const String oidcConfigurationCacheKeyName = 'oidc_configuration_cache_key';
static const String newEmailsContentFolderName = 'new_emails'; static const String newEmailsContentFolderName = 'new_emails';
static const String openedEmailContentFolderName = 'opened_email'; static const String openedEmailContentFolderName = 'opened_email';
@@ -32,7 +32,7 @@ class AutoSignInViaDeepLinkInteractor {
await Future.wait([ await Future.wait([
_credentialRepository.saveBaseUrl(baseUri), _credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), _authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority), _authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]); ]);
await _accountRepository.setCurrentAccount( await _accountRepository.setCurrentAccount(
@@ -6,6 +6,7 @@ import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_downloader/flutter_downloader.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:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/account/personal_account.dart'; import 'package:model/account/personal_account.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
@@ -21,9 +22,12 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_lo
import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; import 'package:tmail_ui_user/features/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/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/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_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/domain/state/get_stored_token_oidc_state.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_type.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.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_navigation.dart';
@@ -65,7 +69,7 @@ class HomeController extends ReloadableController {
@override @override
void onReady() { void onReady() {
_cleanupCache(); _handleNavigateToScreen();
super.onReady(); super.onReady();
} }
@@ -76,6 +80,11 @@ class HomeController extends ReloadableController {
arguments: session); arguments: session);
} }
@override
void onCancelReconnectWhenSessionExpired() {
clearDataAndGoToLoginPage();
}
void _initFlutterDownloader() { void _initFlutterDownloader() {
FlutterDownloader FlutterDownloader
.initialize(debug: kDebugMode) .initialize(debug: kDebugMode)
@@ -84,6 +93,15 @@ class HomeController extends ReloadableController {
static void downloadCallback(String id, DownloadTaskStatus status, int progress) {} static void downloadCallback(String id, DownloadTaskStatus status, int progress) {}
void _handleNavigateToScreen() {
final arguments = Get.arguments;
if (arguments is LoginNavigateArguments) {
_handleLoginNavigateArguments(arguments);
} else {
_cleanupCache();
}
}
Future<void> _cleanupCache() async { Future<void> _cleanupCache() async {
await HiveCacheConfig.instance.onUpgradeDatabase(cachingManager); await HiveCacheConfig.instance.onUpgradeDatabase(cachingManager);
@@ -95,6 +113,17 @@ class HomeController extends ReloadableController {
], eagerError: true).then((_) => getAuthenticatedAccountAction()); ], eagerError: true).then((_) => getAuthenticatedAccountAction());
} }
void _handleLoginNavigateArguments(LoginNavigateArguments arguments) {
switch (arguments.navigateType) {
case LoginNavigateType.autoSignIn:
_handleAutoSignInViaDeepLinkSuccess(arguments.autoSignInViaDeepLinkSuccess!);
break;
default:
_cleanupCache();
break;
}
}
void _registerReceivingFileSharing() { void _registerReceivingFileSharing() {
_emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed(); _emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed();
} }
@@ -124,7 +153,7 @@ class HomeController extends ReloadableController {
void _handleLogOutAndSignInNewAccount({ void _handleLogOutAndSignInNewAccount({
required Success authenticationViewStateSuccess, required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount, required PersonalAccount personalAccount,
required DeepLinkData deepLinkData, required OpenAppDeepLinkData openAppDeepLinkData,
}) { }) {
if (authenticationViewStateSuccess is GetCredentialViewState) { if (authenticationViewStateSuccess is GetCredentialViewState) {
setDataToInterceptors( setDataToInterceptors(
@@ -143,14 +172,14 @@ class HomeController extends ReloadableController {
_getSessionActionToLogOut( _getSessionActionToLogOut(
authenticationViewStateSuccess: authenticationViewStateSuccess, authenticationViewStateSuccess: authenticationViewStateSuccess,
personalAccount: personalAccount, personalAccount: personalAccount,
deepLinkData: deepLinkData, openAppDeepLinkData: openAppDeepLinkData,
); );
} }
Future<void> _getSessionActionToLogOut({ Future<void> _getSessionActionToLogOut({
required Success authenticationViewStateSuccess, required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount, required PersonalAccount personalAccount,
required DeepLinkData deepLinkData, required OpenAppDeepLinkData openAppDeepLinkData,
}) async { }) async {
try { try {
final sessionViewState = await getSessionInteractor.execute().last; final sessionViewState = await getSessionInteractor.execute().last;
@@ -161,7 +190,7 @@ class HomeController extends ReloadableController {
sessionViewStateSuccess: success, sessionViewStateSuccess: success,
authenticationViewStateSuccess: authenticationViewStateSuccess, authenticationViewStateSuccess: authenticationViewStateSuccess,
personalAccount: personalAccount, personalAccount: personalAccount,
deepLinkData: deepLinkData, openAppDeepLinkData: openAppDeepLinkData,
), ),
); );
} catch (e) { } catch (e) {
@@ -174,18 +203,27 @@ class HomeController extends ReloadableController {
required Success sessionViewStateSuccess, required Success sessionViewStateSuccess,
required Success authenticationViewStateSuccess, required Success authenticationViewStateSuccess,
required PersonalAccount personalAccount, required PersonalAccount personalAccount,
required DeepLinkData deepLinkData, required OpenAppDeepLinkData openAppDeepLinkData,
}) { }) {
if (sessionViewStateSuccess is GetSessionSuccess) { if (sessionViewStateSuccess is GetSessionSuccess) {
logoutToSignInNewAccount( logoutToSignInNewAccount(
session: sessionViewStateSuccess.session, session: sessionViewStateSuccess.session,
accountId: personalAccount.accountId!, accountId: personalAccount.accountId!,
onFailureCallback: () => onFailureCallback: ({exception}) {
_continueUsingTheApp(authenticationViewStateSuccess), if (exception is UserCancelledLogoutOIDCFlowException) {
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
);
} else {
_continueUsingTheApp(authenticationViewStateSuccess);
}
},
onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink( onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink(
deepLinkData: deepLinkData, openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess), onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
), ),
); );
} else { } else {
@@ -234,8 +272,19 @@ class HomeController extends ReloadableController {
@override @override
void handleFailureViewState(Failure failure) { void handleFailureViewState(Failure failure) {
if (_validateToHandleDeepLinksNotSignedIn(failure)) { if (_validateToHandleDeepLinksNotSignedIn(failure)) {
_deepLinksManager!.handleDeepLinksWhenAppTerminatedNotSignedIn( _deepLinksManager!.handleDeepLinksWhenAppTerminated(
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, onSuccessCallback: (deepLinkData) {
if (deepLinkData is OpenAppDeepLinkData) {
_deepLinksManager!.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: false,
onFailureCallback: goToLogin,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess
);
} else {
goToLogin();
}
},
onFailureCallback: goToLogin, onFailureCallback: goToLogin,
); );
} else { } else {
@@ -248,14 +297,25 @@ class HomeController extends ReloadableController {
if (_validateToHandleDeepLinksSignedIn(success)) { if (_validateToHandleDeepLinksSignedIn(success)) {
final personalAccount = _getPersonalAccountFromViewStateSuccess(success); final personalAccount = _getPersonalAccountFromViewStateSuccess(success);
_deepLinksManager!.handleDeepLinksWhenAppTerminatedSignedIn( _deepLinksManager!.handleDeepLinksWhenAppTerminated(
username: personalAccount?.userName?.value, onSuccessCallback: (deepLinkData) {
if (deepLinkData is OpenAppDeepLinkData) {
_deepLinksManager!.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: true,
username: personalAccount?.userName,
onConfirmLogoutCallback: (openAppDeepLinkData) => _handleLogOutAndSignInNewAccount(
authenticationViewStateSuccess: success,
personalAccount: personalAccount!,
openAppDeepLinkData: openAppDeepLinkData,
),
onFailureCallback: () => _continueUsingTheApp(success),
);
} else {
_continueUsingTheApp(success);
}
},
onFailureCallback: () => _continueUsingTheApp(success), onFailureCallback: () => _continueUsingTheApp(success),
onConfirmCallback: (deepLinkData) => _handleLogOutAndSignInNewAccount(
authenticationViewStateSuccess: success,
personalAccount: personalAccount!,
deepLinkData: deepLinkData,
),
); );
} else { } else {
super.handleSuccessViewState(success); super.handleSuccessViewState(success);
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCDataSource {
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash); Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
Future<void> persistAuthorityOidc(String authority); Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
Future<void> deleteAuthorityOidc(); Future<void> deleteOidcConfiguration();
Future<OIDCConfiguration> getStoredOidcConfiguration(); Future<OIDCConfiguration> getStoredOidcConfiguration();
@@ -74,9 +74,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
} }
@override @override
Future<void> persistAuthorityOidc(String authority) { Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
return Future.sync(() async { return Future.sync(() async {
return await _oidcConfigurationCacheManager.persistAuthorityOidc(authority); return await _oidcConfigurationCacheManager.persistOidcConfiguration(oidcConfiguration);
}).catchError(_cacheExceptionThrower.throwException); }).catchError(_cacheExceptionThrower.throwException);
} }
@@ -106,9 +106,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
} }
@override @override
Future<void> deleteAuthorityOidc() { Future<void> deleteOidcConfiguration() {
return Future.sync(() async { return Future.sync(() async {
return await _oidcConfigurationCacheManager.deleteAuthorityOidc(); return await _oidcConfigurationCacheManager.deleteOidcConfiguration();
}).catchError(_exceptionThrower.throwException); }).catchError(_exceptionThrower.throwException);
} }
@@ -0,0 +1,16 @@
import 'package:model/oidc/oidc_configuration.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
extension OidcConfigutationCacheExtension on OidcConfigurationCache {
OIDCConfiguration toOIDCConfiguration() {
return OIDCConfiguration(
authority: authority,
isTWP: isTWP,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes,
);
}
}
@@ -1,34 +1,54 @@
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/oidc_configuration.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tmail_ui_user/features/caching/clients/oidc_configuration_cache_client.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
import 'package:tmail_ui_user/features/login/data/extensions/oidc_configutation_cache_extension.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart';
class OidcConfigurationCacheManager { class OidcConfigurationCacheManager {
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
final OidcConfigurationCacheClient _oidcConfigurationCacheClient;
OidcConfigurationCacheManager(this._sharedPreferences); OidcConfigurationCacheManager(this._sharedPreferences, this._oidcConfigurationCacheClient);
Future<OIDCConfiguration> getOidcConfiguration() async { Future<OIDCConfiguration> getOidcConfiguration() async {
final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc); final oidcConfigurationCache = await _oidcConfigurationCacheClient.getItem(CachingConstants.oidcConfigurationCacheKeyName);
if (authority == null || authority.isEmpty) { if (oidcConfigurationCache == null) {
throw CanNotFoundOIDCAuthority(); final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc);
} else {
if (authority == null || authority.isEmpty) {
throw CanNotFoundOIDCAuthority();
}
return OIDCConfiguration( return OIDCConfiguration(
authority: authority, authority: authority,
clientId: OIDCConstant.clientId, clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes); scopes: AppConfig.oidcScopes,
);
} else {
return oidcConfigurationCache.toOIDCConfiguration();
} }
} }
Future<void> persistAuthorityOidc(String authority) async { Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) async {
log('OidcConfigurationCacheManager::persistAuthorityOidc(): $authority'); log('OidcConfigurationCacheManager::persistOidcConfiguration(): $oidcConfiguration');
await _sharedPreferences.setString(OIDCConstant.keyAuthorityOidc, authority); await _oidcConfigurationCacheClient.insertItem(
CachingConstants.oidcConfigurationCacheKeyName,
oidcConfiguration.toOidcConfigurationCache(),
);
} }
Future<void> deleteAuthorityOidc() async { Future<void> deleteOidcConfiguration() async {
log('OidcConfigurationCacheManager::deleteAuthorityOidc()'); log('OidcConfigurationCacheManager::deleteOidcConfiguration()');
await _sharedPreferences.remove(OIDCConstant.keyAuthorityOidc); await Future.wait([
_oidcConfigurationCacheClient.deleteItem(
CachingConstants.oidcConfigurationCacheKeyName,
),
_sharedPreferences.remove(OIDCConstant.keyAuthorityOidc),
]);
} }
} }
@@ -0,0 +1,20 @@
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
part 'oidc_configuration_cache.g.dart';
@HiveType(typeId: CachingConstants.OIDC_CONFIGURATION_CACHE_ID)
class OidcConfigurationCache extends HiveObject with EquatableMixin {
@HiveField(0)
final String authority;
@HiveField(1)
final bool isTWP;
OidcConfigurationCache(this.authority, this.isTWP);
@override
List<Object?> get props => [authority, isTWP];
}
@@ -4,13 +4,17 @@ import 'package:tmail_ui_user/main/utils/app_config.dart';
class OIDCConstant { class OIDCConstant {
static String get mobileOidcClientId => 'teammail-mobile'; static String get mobileOidcClientId => 'teammail-mobile';
static List<String> get oidcScope => ['openid', 'profile', 'email', 'offline_access']; static List<String> get oidcScope => ['openid', 'profile', 'email', 'offline_access'];
static const keyAuthorityOidc = 'KEY_AUTHORITY_OIDC'; static const String keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
static const authResponseKey = "auth_info"; static const String authResponseKey = "auth_info";
static const String twakeWorkplaceUrlScheme = 'twakemail.mobile'; static const String twakeWorkplaceUrlScheme = 'twakemail.mobile';
static const String twakeWorkplaceRedirectUrl = '$twakeWorkplaceUrlScheme://redirect'; static const String twakeWorkplaceRedirectUrl = '$twakeWorkplaceUrlScheme://redirect';
static const String appParameter = 'tmail'; static const String appParameter = 'tmail';
static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url'; static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url';
static const String postLoginRedirectUrlPathParams = 'post_login_redirect_url'; static const String postLoginRedirectUrlPathParams = 'post_login_redirect_url';
static const String endSessionFailedCode = 'end_session_failed';
static const String redirectOidcMobile = 'teammail.mobile://oauthredirect';
static const String loginRedirectOidcWeb = 'login-callback.html';
static const String logoutRedirectOidcWeb = 'logout-callback.html';
static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId; static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId;
} }
@@ -52,11 +52,14 @@ class OIDCHttpClient {
if (oidcResponse.links.isEmpty) { if (oidcResponse.links.isEmpty) {
throw CanNotFoundOIDCAuthority(); throw CanNotFoundOIDCAuthority();
} }
log('OIDCHttpClient::getOIDCConfiguration(): href: ${oidcResponse.links[0].href}'); final authority = oidcResponse.links[0].href.toString();
final isTWP = authority == AppConfig.saasRegistrationUrl;
log('OIDCHttpClient::getOIDCConfiguration():authority: $authority');
return OIDCConfiguration( return OIDCConfiguration(
authority: oidcResponse.links[0].href.toString(), authority: authority,
clientId: OIDCConstant.clientId, clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes scopes: AppConfig.oidcScopes,
isTWP: isTWP,
); );
} }
@@ -22,7 +22,6 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
return _oidcDataSource.getOIDCConfiguration(oidcResponse); return _oidcDataSource.getOIDCConfiguration(oidcResponse);
} }
@override @override
Future<OIDCDiscoveryResponse> discoverOIDC(OIDCConfiguration oidcConfiguration) { Future<OIDCDiscoveryResponse> discoverOIDC(OIDCConfiguration oidcConfiguration) {
return _oidcDataSource.discoverOIDC(oidcConfiguration); return _oidcDataSource.discoverOIDC(oidcConfiguration);
@@ -49,8 +48,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
} }
@override @override
Future<void> persistAuthorityOidc(String authority) { Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
return _oidcDataSource.persistAuthorityOidc(authority); return _oidcDataSource.persistOidcConfiguration(oidcConfiguration);
} }
@override @override
@@ -75,8 +74,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
} }
@override @override
Future<void> deleteAuthorityOidc() { Future<void> deleteOidcConfiguration() {
return _oidcDataSource.deleteAuthorityOidc(); return _oidcDataSource.deleteOidcConfiguration();
} }
@override @override
@@ -0,0 +1 @@
class UserCancelledLogoutOIDCFlowException implements Exception {}
@@ -4,6 +4,7 @@ import 'package:core/data/network/config/service_path.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/oidc_configuration.dart';
import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart';
import 'package:tmail_ui_user/features/login/data/model/oidc_configuration_cache.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart';
@@ -12,32 +13,29 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
String get redirectUrl { String get redirectUrl {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
return AppConfig.domainRedirectUrl.endsWith('/') return AppConfig.domainRedirectUrl.endsWith('/')
? AppConfig.domainRedirectUrl + loginRedirectOidcWeb ? AppConfig.domainRedirectUrl + OIDCConstant.loginRedirectOidcWeb
: '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb'; : '${AppConfig.domainRedirectUrl}/${OIDCConstant.loginRedirectOidcWeb}';
} else { } else {
return _isSaasAuthority(authority) return isTWP
? OIDCConstant.twakeWorkplaceRedirectUrl ? OIDCConstant.twakeWorkplaceRedirectUrl
: redirectOidcMobile; : OIDCConstant.redirectOidcMobile;
} }
} }
String get logoutRedirectUrl { String get logoutRedirectUrl {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
if (AppConfig.domainRedirectUrl.endsWith('/')) { if (AppConfig.domainRedirectUrl.endsWith('/')) {
return AppConfig.domainRedirectUrl + logoutRedirectOidcWeb; return AppConfig.domainRedirectUrl + OIDCConstant.logoutRedirectOidcWeb;
} else { } else {
return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb'; return '${AppConfig.domainRedirectUrl}/${OIDCConstant.logoutRedirectOidcWeb}';
} }
} else { } else {
return _isSaasAuthority(authority) return isTWP
? OIDCConstant.twakeWorkplaceRedirectUrl ? OIDCConstant.twakeWorkplaceRedirectUrl
: redirectOidcMobile; : OIDCConstant.redirectOidcMobile;
} }
} }
bool _isSaasAuthority(String authority) =>
authority == AppConfig.saasRegistrationUrl;
String get signInTWPUrl => ServicePath(authority) String get signInTWPUrl => ServicePath(authority)
.withQueryParameters([ .withQueryParameters([
StringQueryParameter( StringQueryParameter(
@@ -57,4 +55,8 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
StringQueryParameter('app', OIDCConstant.appParameter), StringQueryParameter('app', OIDCConstant.appParameter),
]) ])
.generateEndpointPath(); .generateEndpointPath();
OidcConfigurationCache toOidcConfigurationCache() {
return OidcConfigurationCache(authority, isTWP);
}
} }
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCRepository {
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash); Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
Future<void> persistAuthorityOidc(String authority); Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
Future<void> deleteAuthorityOidc(); Future<void> deleteOidcConfiguration();
Future<OIDCConfiguration> getStoredOidcConfiguration(); Future<OIDCConfiguration> getStoredOidcConfiguration();
@@ -17,7 +17,7 @@ class DeleteAuthorityOidcInteractor {
try { try {
await Future.wait([ await Future.wait([
_credentialRepository.removeBaseUrl(), _credentialRepository.removeBaseUrl(),
_authenticationOIDCRepository.deleteAuthorityOidc(), _authenticationOIDCRepository.deleteOidcConfiguration(),
_authenticationOIDCRepository.deleteTokenOIDC(), _authenticationOIDCRepository.deleteTokenOIDC(),
]); ]);
return Right(DeleteAuthorityOidcSuccess()); return Right(DeleteAuthorityOidcSuccess());
@@ -15,7 +15,7 @@ class GetOIDCConfigurationInteractor {
try { try {
yield Right<Failure, Success>(GetOIDCConfigurationLoading()); yield Right<Failure, Success>(GetOIDCConfigurationLoading());
final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(oidcResponse); final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(oidcResponse);
await _oidcRepository.persistAuthorityOidc(oidcConfiguration.authority); await _oidcRepository.persistOidcConfiguration(oidcConfiguration);
yield Right<Failure, Success>(GetOIDCConfigurationSuccess(oidcConfiguration)); yield Right<Failure, Success>(GetOIDCConfigurationSuccess(oidcConfiguration));
} catch (e) { } catch (e) {
log('GetOIDCConfigurationInteractor::execute(): ERROR: $e'); log('GetOIDCConfigurationInteractor::execute(): ERROR: $e');
@@ -35,7 +35,7 @@ class GetTokenOIDCInteractor {
await Future.wait([ await Future.wait([
_credentialRepository.saveBaseUrl(baseUrl), _credentialRepository.saveBaseUrl(baseUrl),
authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
authenticationOIDCRepository.persistAuthorityOidc(config.authority), authenticationOIDCRepository.persistOidcConfiguration(config),
]); ]);
await _accountRepository.setCurrentAccount( await _accountRepository.setCurrentAccount(
@@ -58,6 +58,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_
import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart'; import 'package:tmail_ui_user/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_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.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_navigation.dart';
@@ -163,6 +164,7 @@ class LoginController extends ReloadableController {
} else if (failure is GetAuthenticatedAccountFailure) { } else if (failure is GetAuthenticatedAccountFailure) {
_checkOIDCIsAvailable(); _checkOIDCIsAvailable();
} else if (failure is GetSessionFailure) { } else if (failure is GetSessionFailure) {
SmartDialog.dismiss();
clearAllData(); clearAllData();
} else if (failure is DNSLookupToGetJmapUrlFailure) { } else if (failure is DNSLookupToGetJmapUrlFailure) {
_username = null; _username = null;
@@ -218,6 +220,7 @@ class LoginController extends ReloadableController {
_handleNoSuitableBrowserOIDC(failure) _handleNoSuitableBrowserOIDC(failure)
.map((stillFailed) => _handleCommonOIDCFailure()); .map((stillFailed) => _handleCommonOIDCFailure());
} else if (failure is GetSessionFailure) { } else if (failure is GetSessionFailure) {
SmartDialog.dismiss();
clearAllData(); clearAllData();
} else { } else {
super.handleUrgentException(failure: failure, exception: exception); super.handleUrgentException(failure: failure, exception: exception);
@@ -234,12 +237,6 @@ class LoginController extends ReloadableController {
); );
} }
@override
void handleGetSessionFailure(GetSessionFailure failure) {
SmartDialog.dismiss();
super.handleGetSessionFailure(failure);
}
void _registerDeepLinks() { void _registerDeepLinks() {
_deepLinksManager = getBinding<DeepLinksManager>(); _deepLinksManager = getBinding<DeepLinksManager>();
_deepLinksManager?.clearPendingDeepLinkData(); _deepLinksManager?.clearPendingDeepLinkData();
@@ -250,16 +247,21 @@ class LoginController extends ReloadableController {
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) { void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData'); log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
if (deepLinkData == null) return; _deepLinksManager?.handleDeepLinksWhenAppRunning(
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
deepLinkData: deepLinkData, deepLinkData: deepLinkData,
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, onSuccessCallback: (deepLinkData) {
onFailureCallback: SmartDialog.dismiss, if (deepLinkData is! OpenAppDeepLinkData) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: deepLinkData,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
);
},
); );
} }
@@ -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<Object?> get props => [
navigateType,
autoSignInViaDeepLinkSuccess,
];
}
@@ -0,0 +1,3 @@
enum LoginNavigateType {
autoSignIn
}
@@ -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:email_recovery/email_recovery/email_recovery_action_id.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.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:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -74,9 +75,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/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/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/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/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/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/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/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/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
@@ -157,6 +161,9 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_e
import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/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/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/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.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/app_routes.dart';
@@ -254,6 +261,8 @@ class MailboxDashBoardController extends ReloadableController
StreamSubscription? _receivingFileSharingStreamSubscription; StreamSubscription? _receivingFileSharingStreamSubscription;
StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription;
bool _isFirstSessionLoad = false; bool _isFirstSessionLoad = false;
DeepLinksManager? _deepLinksManager;
StreamSubscription<DeepLinkData?>? _deepLinkDataStreamSubscription;
final StreamController<Either<Failure, Success>> _progressStateController = final StreamController<Either<Failure, Success>> _progressStateController =
StreamController<Either<Failure, Success>>.broadcast(); StreamController<Either<Failure, Success>>.broadcast();
@@ -301,6 +310,7 @@ class MailboxDashBoardController extends ReloadableController
void onInit() { void onInit() {
if (PlatformInfo.isMobile) { if (PlatformInfo.isMobile) {
_registerReceivingFileSharingStream(); _registerReceivingFileSharingStream();
_registerDeepLinks();
} }
_registerStreamListener(); _registerStreamListener();
BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard);
@@ -467,6 +477,8 @@ class MailboxDashBoardController extends ReloadableController
@override @override
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) { void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
SmartDialog.dismiss();
if (failure is SendEmailFailure && exception is NoNetworkError) { if (failure is SendEmailFailure && exception is NoNetworkError) {
_storeSendingEmailInCaseOfSendingFailureInMobile(failure); _storeSendingEmailInCaseOfSendingFailureInMobile(failure);
} else { } else {
@@ -563,6 +575,71 @@ class MailboxDashBoardController extends ReloadableController
); );
} }
void _registerDeepLinks() {
_deepLinksManager = getBinding<DeepLinksManager>();
_deepLinksManager?.clearPendingDeepLinkData();
_deepLinkDataStreamSubscription = _deepLinksManager
?.pendingDeepLinkData.stream
.listen(_handlePendingDeepLinkDataStream);
}
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('MailboxDashBoardController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
_deepLinksManager?.handleDeepLinksWhenAppRunning(
deepLinkData: deepLinkData,
onSuccessCallback: (deepLinkData) {
if (deepLinkData is! OpenAppDeepLinkData) return;
_deepLinksManager?.handleOpenAppDeepLinks(
openAppDeepLinkData: deepLinkData,
isSignedIn: true,
username: sessionCurrent?.username,
onConfirmLogoutCallback: _handleLogOutAndSignInNewAccount,
);
},
);
}
void _handleLogOutAndSignInNewAccount(OpenAppDeepLinkData openAppDeepLinkData) {
if (sessionCurrent == null || accountId.value == null) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
logoutToSignInNewAccount(
session: sessionCurrent!,
accountId: accountId.value!,
onFailureCallback: ({exception}) {
if (exception is UserCancelledLogoutOIDCFlowException) {
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: SmartDialog.dismiss,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
);
} else {
SmartDialog.dismiss();
}
},
onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onFailureCallback: SmartDialog.dismiss,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
),
);
}
void _handleAutoSignInViaDeepLinkSuccess(AutoSignInViaDeepLinkSuccess success) {
SmartDialog.dismiss();
pushAndPopAll(
AppRoutes.home,
arguments: LoginNavigateArguments.autoSignIn(
autoSignInViaDeepLinkSuccess: success,
),
);
}
void _registerPendingCurrentEmailIdInNotification() { void _registerPendingCurrentEmailIdInNotification() {
_iosNotificationManager = getBinding<IOSNotificationManager>(); _iosNotificationManager = getBinding<IOSNotificationManager>();
_currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager _currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager
@@ -1625,6 +1702,8 @@ class MailboxDashBoardController extends ReloadableController
@override @override
void handleReloaded(Session session) { void handleReloaded(Session session) {
log('MailboxDashBoardController::handleReloaded():'); log('MailboxDashBoardController::handleReloaded():');
SmartDialog.dismiss();
_getRouteParameters(); _getRouteParameters();
_setUpComponentsFromSession(session); _setUpComponentsFromSession(session);
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
@@ -3144,6 +3223,7 @@ class MailboxDashBoardController extends ReloadableController
_pendingSharedFileInfoSubscription?.cancel(); _pendingSharedFileInfoSubscription?.cancel();
_receivingFileSharingStreamSubscription?.cancel(); _receivingFileSharingStreamSubscription?.cancel();
_emailReceiveManager.closeEmailReceiveManagerStream(); _emailReceiveManager.closeEmailReceiveManagerStream();
_deepLinkDataStreamSubscription?.cancel();
} }
_progressStateController.close(); _progressStateController.close();
_refreshActionEventController.close(); _refreshActionEventController.close();
@@ -17,6 +17,7 @@ extension KeychainSharingSessionExtension on KeychainSharingSession {
tokenEndpoint: tokenEndpoint, tokenEndpoint: tokenEndpoint,
oidcScopes: oidcScopes, oidcScopes: oidcScopes,
mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification, mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification,
isTWP: isTWP,
); );
} }
} }
@@ -27,6 +27,7 @@ class KeychainSharingSession with EquatableMixin {
String? tokenEndpoint; String? tokenEndpoint;
List<String>? oidcScopes; List<String>? oidcScopes;
List<MailboxId>? mailboxIdsBlockNotification; List<MailboxId>? mailboxIdsBlockNotification;
bool isTWP;
KeychainSharingSession({ KeychainSharingSession({
required this.accountId, required this.accountId,
@@ -40,6 +41,7 @@ class KeychainSharingSession with EquatableMixin {
this.tokenEndpoint, this.tokenEndpoint,
this.oidcScopes, this.oidcScopes,
this.mailboxIdsBlockNotification, this.mailboxIdsBlockNotification,
this.isTWP = false,
}); });
factory KeychainSharingSession.fromJson(Map<String, dynamic> json) => _$KeychainSharingSessionFromJson(json); factory KeychainSharingSession.fromJson(Map<String, dynamic> json) => _$KeychainSharingSessionFromJson(json);
@@ -59,5 +61,6 @@ class KeychainSharingSession with EquatableMixin {
tokenEndpoint, tokenEndpoint,
oidcScopes, oidcScopes,
mailboxIdsBlockNotification, mailboxIdsBlockNotification,
isTWP,
]; ];
} }
@@ -36,7 +36,7 @@ class SignInTwakeWorkplaceInteractor {
await Future.wait([ await Future.wait([
_credentialRepository.saveBaseUrl(baseUri), _credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), _authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority), _authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]); ]);
await _accountRepository.setCurrentAccount( await _accountRepository.setCurrentAccount(
@@ -36,7 +36,7 @@ class SignUpTwakeWorkplaceInteractor {
await Future.wait([ await Future.wait([
_credentialRepository.saveBaseUrl(baseUri), _credentialRepository.saveBaseUrl(baseUri),
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), _authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority), _authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
]); ]);
await _accountRepository.setCurrentAccount( await _accountRepository.setCurrentAccount(
@@ -1,4 +1,3 @@
import 'package:core/utils/platform_info.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.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/data/network/authentication_client/authentication_client_base.dart';
@@ -38,9 +37,6 @@ class TwakeWelcomeBindings extends BaseBindings {
Get.find<AccountRepository>(), Get.find<AccountRepository>(),
Get.find<CredentialRepository>(), Get.find<CredentialRepository>(),
)); ));
if (PlatformInfo.isMobile) {
}
} }
@override @override
@@ -23,6 +23,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twak
import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_up_twake_workplace_interactor.dart'; import 'package:tmail_ui_user/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_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart'; import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.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_navigation.dart';
@@ -61,16 +62,21 @@ class TwakeWelcomeController extends ReloadableController {
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) { void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
log('TwakeWelcomeController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData'); log('TwakeWelcomeController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
if (deepLinkData == null) return; _deepLinksManager?.handleDeepLinksWhenAppRunning(
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
deepLinkData: deepLinkData, deepLinkData: deepLinkData,
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess, onSuccessCallback: (deepLinkData) {
onFailureCallback: SmartDialog.dismiss, if (deepLinkData is! OpenAppDeepLinkData) return;
if (currentContext != null) {
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
}
_deepLinksManager?.autoSignInViaDeepLink(
openAppDeepLinkData: deepLinkData,
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
onFailureCallback: SmartDialog.dismiss,
);
},
); );
} }
@@ -107,7 +113,8 @@ class TwakeWelcomeController extends ReloadableController {
oidcConfiguration: OIDCConfiguration( oidcConfiguration: OIDCConfiguration(
authority: AppConfig.saasRegistrationUrl, authority: AppConfig.saasRegistrationUrl,
clientId: OIDCConstant.clientId, clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes scopes: AppConfig.oidcScopes,
isTWP: true,
) )
)); ));
} }
@@ -127,7 +134,8 @@ class TwakeWelcomeController extends ReloadableController {
oidcConfiguration: OIDCConfiguration( oidcConfiguration: OIDCConfiguration(
authority: AppConfig.saasRegistrationUrl, authority: AppConfig.saasRegistrationUrl,
clientId: OIDCConstant.clientId, clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes scopes: AppConfig.oidcScopes,
isTWP: true,
) )
)); ));
} }
@@ -178,6 +186,12 @@ class TwakeWelcomeController extends ReloadableController {
toastManager.showMessageFailure(failure); toastManager.showMessageFailure(failure);
} }
@override
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
SmartDialog.dismiss();
super.handleUrgentExceptionOnMobile(failure: failure, exception: exception);
}
void _synchronizeTokenAndGetSession({ void _synchronizeTokenAndGetSession({
required Uri baseUri, required Uri baseUri,
required TokenOIDC tokenOIDC, required TokenOIDC tokenOIDC,
+19 -1
View File
@@ -1,5 +1,5 @@
{ {
"@@last_modified": "2024-12-05T18:34:48.830922", "@@last_modified": "2024-12-09T13:59:58.460897",
"initializing_data": "Initializing data...", "initializing_data": "Initializing data...",
"@initializing_data": { "@initializing_data": {
"type": "text", "type": "text",
@@ -4118,6 +4118,24 @@
"placeholders_order": [], "placeholders_order": [],
"placeholders": {} "placeholders": {}
}, },
"switchAccountConfirmation": "Switch Account Confirmation",
"@switchAccountConfirmation": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"youAreCurrentlyLoggedInWith": "You are currently logged in with",
"@youAreCurrentlyLoggedInWith": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"doYouWantToLogOutAndSwitchTo": "Do you want to log out and switch to",
"@doYouWantToLogOutAndSwitchTo": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"getHelpOrReportABug": "Get help or report a bug", "getHelpOrReportABug": "Get help or report a bug",
"@getHelpOrReportABug": { "@getHelpOrReportABug": {
"type": "text", "type": "text",
@@ -15,6 +15,6 @@ class DeepLinkBindings extends Bindings {
Get.find<AccountRepository>(), Get.find<AccountRepository>(),
Get.find<CredentialRepository>(), Get.find<CredentialRepository>(),
)); ));
Get.put(DeepLinksManager(Get.find<AutoSignInViaDeepLinkInteractor>())); Get.put(DeepLinksManager());
} }
} }
+3 -1
View File
@@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/caching/clients/firebase_registration_cac
import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart'; import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart';
import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart';
import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart';
import 'package:tmail_ui_user/features/caching/clients/oidc_configuration_cache_client.dart';
import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart';
import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart';
import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart';
@@ -70,7 +71,8 @@ class LocalBindings extends Bindings {
Get.put(EncryptionKeyCacheManager(Get.find<EncryptionKeyCacheClient>())); Get.put(EncryptionKeyCacheManager(Get.find<EncryptionKeyCacheClient>()));
Get.put(AuthenticationInfoCacheClient()); Get.put(AuthenticationInfoCacheClient());
Get.put(AuthenticationInfoCacheManager(Get.find<AuthenticationInfoCacheClient>())); Get.put(AuthenticationInfoCacheManager(Get.find<AuthenticationInfoCacheClient>()));
Get.put(OidcConfigurationCacheManager(Get.find<SharedPreferences>())); Get.put(OidcConfigurationCacheClient());
Get.put(OidcConfigurationCacheManager(Get.find<SharedPreferences>(), Get.find<OidcConfigurationCacheClient>()));
Get.put(LanguageCacheManager(Get.find<SharedPreferences>())); Get.put(LanguageCacheManager(Get.find<SharedPreferences>()));
Get.put(RecentLoginUrlCacheClient()); Get.put(RecentLoginUrlCacheClient());
Get.put(RecentLoginUrlCacheManager((Get.find<RecentLoginUrlCacheClient>()))); Get.put(RecentLoginUrlCacheManager((Get.find<RecentLoginUrlCacheClient>())));
@@ -1,9 +0,0 @@
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
typedef OnDeepLinkSuccessCallback = Function(AutoSignInViaDeepLinkSuccess success);
typedef OnDeepLinkFailureCallback = Function();
typedef OnDeepLinkConfirmLogoutCallback = Function(DeepLinkData deepLinkData);
@@ -0,0 +1,5 @@
enum DeepLinkActionType {
openApp,
unknown;
}
@@ -0,0 +1,11 @@
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
typedef OnAutoSignInViaDeepLinkSuccessCallback = Function(AutoSignInViaDeepLinkSuccess success);
typedef OnDeepLinkSuccessCallback = Function(DeepLinkData deepLinkData);
typedef OnDeepLinkFailureCallback = Function();
typedef OnDeepLinkConfirmLogoutCallback<T> = Function(T t);
+4 -38
View File
@@ -1,45 +1,11 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:model/model.dart'; import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart';
class DeepLinkData with EquatableMixin { class DeepLinkData with EquatableMixin {
final String action; final DeepLinkActionType actionType;
final String? accessToken;
final String? refreshToken;
final String? idToken;
final int? expiresIn;
final String? username;
DeepLinkData({ DeepLinkData({required this.actionType});
required this.action,
this.accessToken,
this.refreshToken,
this.idToken,
this.expiresIn,
this.username,
});
bool isValidToken() => accessToken?.isNotEmpty == true && username?.isNotEmpty == true;
TokenOIDC getTokenOIDC() {
final expiredTime = expiresIn == null
? null
: DateTime.now().add(Duration(seconds: expiresIn!));
return TokenOIDC(
accessToken!,
TokenId(idToken ?? ''),
refreshToken ?? '',
expiredTime: expiredTime,
);
}
@override @override
List<Object?> get props => [ List<Object?> get props => [actionType];
action,
accessToken,
refreshToken,
idToken,
expiresIn,
username,
];
} }
+20 -178
View File
@@ -1,24 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/utils/app_logger.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:rxdart/subjects.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.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/main/deep_links/deep_link_action_type.dart';
import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart'; import 'package:tmail_ui_user/main/deep_links/deep_link_callback_action_define.dart';
import 'package:tmail_ui_user/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/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_handler_mixin.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 { class DeepLinksManager with MessageDialogActionMixin, OpenAppDeepLinkHandlerMixin {
final AutoSignInViaDeepLinkInteractor _autoSignInViaDeepLinkInteractor;
BehaviorSubject<DeepLinkData?> _pendingDeepLinkData = BehaviorSubject.seeded(null); BehaviorSubject<DeepLinkData?> _pendingDeepLinkData = BehaviorSubject.seeded(null);
@@ -26,8 +17,6 @@ class DeepLinksManager with MessageDialogActionMixin {
StreamSubscription<Uri>? _deepLinkStreamSubscription; StreamSubscription<Uri>? _deepLinkStreamSubscription;
DeepLinksManager(this._autoSignInViaDeepLinkInteractor);
Future<DeepLinkData?> getDeepLinkData() async { Future<DeepLinkData?> getDeepLinkData() async {
final uriLink = await AppLinks().getInitialLink(); final uriLink = await AppLinks().getInitialLink();
log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink'); log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink');
@@ -63,194 +52,47 @@ class DeepLinksManager with MessageDialogActionMixin {
DeepLinkData? parseDeepLink(String url) { DeepLinkData? parseDeepLink(String url) {
try { try {
final updatedUrl = url.replaceFirst( final decodedUrl = Uri.decodeFull(url);
OIDCConstant.twakeWorkplaceUrlScheme, final uri = Uri.parse(decodedUrl);
'https',
);
final uri = Uri.parse(updatedUrl);
log('DeepLinksManager::parseDeepLink:uri = $uri'); log('DeepLinksManager::parseDeepLink:uri = $uri');
final action = uri.host; final action = uri.host;
final accessToken = uri.queryParameters['access_token'];
final refreshToken = uri.queryParameters['refresh_token'];
final idToken = uri.queryParameters['id_token'];
final expiresInStr = uri.queryParameters['expires_in'];
final username = uri.queryParameters['username'];
final expiresIn = expiresInStr != null if (action == DeepLinkActionType.openApp.name.toLowerCase()) {
? int.tryParse(expiresInStr) return parseOpenAppDeepLink(uri);
: null; } else {
return DeepLinkData(actionType: DeepLinkActionType.unknown);
return DeepLinkData( }
action: action,
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
expiresIn: expiresIn,
username: username,
);
} catch (e) { } catch (e) {
logError('DeepLinksManager::parseDeepLink:Exception = $e'); logError('DeepLinksManager::parseDeepLink:Exception = $e');
return null; return null;
} }
} }
Future<void> handleDeepLinksWhenAppOnForegroundNotSignedIn({ Future<void> handleDeepLinksWhenAppTerminated({
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<void> handleDeepLinksWhenAppTerminatedNotSignedIn({
required OnDeepLinkSuccessCallback onSuccessCallback, required OnDeepLinkSuccessCallback onSuccessCallback,
required OnDeepLinkFailureCallback onFailureCallback, required OnDeepLinkFailureCallback onFailureCallback,
}) async { }) async {
final deepLinkData = await getDeepLinkData(); final deepLinkData = await getDeepLinkData();
log('DeepLinksManager::handleDeepLinksWhenAppTerminatedNotSignedIn:DeepLinkData = $deepLinkData');
if (deepLinkData == null) { if (deepLinkData == null) {
onFailureCallback(); onFailureCallback();
return; return;
} }
if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { onSuccessCallback(deepLinkData);
_handleOpenApp(
deepLinkData: deepLinkData,
onFailureCallback: onFailureCallback,
onSuccessCallback: onSuccessCallback,
);
} else {
onFailureCallback();
}
} }
Future<void> handleDeepLinksWhenAppTerminatedSignedIn({ Future<void> handleDeepLinksWhenAppRunning({
required String? username, required DeepLinkData? deepLinkData,
required OnDeepLinkConfirmLogoutCallback onConfirmCallback, required OnDeepLinkSuccessCallback onSuccessCallback,
required OnDeepLinkFailureCallback onFailureCallback, OnDeepLinkFailureCallback? onFailureCallback,
}) async { }) async {
final deepLinkData = await getDeepLinkData();
log('DeepLinksManager::handleDeepLinksWhenAppTerminatedSignedIn:DeepLinkData = $deepLinkData');
if (deepLinkData == null) { if (deepLinkData == null) {
onFailureCallback(); onFailureCallback?.call();
return; return;
} }
if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) { onSuccessCallback(deepLinkData);
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<void> autoSignInViaDeepLink({
required DeepLinkData deepLinkData,
required OnDeepLinkSuccessCallback onSuccessCallback,
OnDeepLinkFailureCallback? onFailureCallback,
}) async {
try {
if (deepLinkData.isValidToken()) {
final autoSignInViewState = await _autoSignInViaDeepLinkInteractor.execute(
baseUri: Uri.parse(AppConfig.saasJmapServerUrl),
tokenOIDC: deepLinkData.getTokenOIDC(),
oidcConfiguration: OIDCConfiguration(
authority: AppConfig.saasRegistrationUrl,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes,
),
).last;
autoSignInViewState.fold(
(failure) => onFailureCallback?.call(),
(success) {
if (success is AutoSignInViaDeepLinkSuccess) {
onSuccessCallback(success);
} else {
onFailureCallback?.call();
}
},
);
} else {
onFailureCallback?.call();
}
} catch (e) {
logError('DeepLinksManager::_autoSignInViaDeepLink:Exception = $e');
onFailureCallback?.call();
}
}
void _showConfirmDialogSwitchAccount({
required BuildContext context,
required String username,
required Function onConfirmAction,
required Function onCancelAction,
}) {
final appLocalizations = AppLocalizations.of(context);
showConfirmDialogAction(
context,
'',
appLocalizations.yesLogout,
title: appLocalizations.logoutConfirmation,
alignCenter: true,
outsideDismissible: false,
titleActionButtonMaxLines: 1,
titlePadding: const EdgeInsetsDirectional.only(start: 24, top: 24, end: 24, bottom: 12),
messageStyle: const TextStyle(
color: AppColor.colorTextBody,
fontSize: 15,
fontWeight: FontWeight.w400,
),
listTextSpan: [
TextSpan(text: appLocalizations.messageConfirmationLogout),
TextSpan(
text: ' $username',
style: const TextStyle(
color: AppColor.colorTextBody,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const TextSpan(text: ' ?'),
],
onConfirmAction: onConfirmAction,
onCancelAction: onCancelAction,
);
} }
void dispose() { void dispose() {
@@ -0,0 +1,72 @@
import 'package:model/oidc/oidc_configuration.dart';
import 'package:model/oidc/token_id.dart';
import 'package:model/oidc/token_oidc.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
class OpenAppDeepLinkData extends DeepLinkData {
final String registrationUrl;
final String jmapUrl;
final String username;
final String accessToken;
final String? refreshToken;
final String? idToken;
final int? expiresIn;
OpenAppDeepLinkData({
super.actionType = DeepLinkActionType.openApp,
required this.registrationUrl,
required this.jmapUrl,
required this.username,
required this.accessToken,
this.refreshToken,
this.idToken,
this.expiresIn,
});
bool isValidAuthentication() =>
accessToken.isNotEmpty &&
username.isNotEmpty &&
registrationUrl.isNotEmpty &&
jmapUrl.isNotEmpty;
DateTime? _getExpiredTime() {
return expiresIn != null
? DateTime.now().add(Duration(seconds: expiresIn!))
: null;
}
bool isLoggedInWith(String currentUsername) => username == currentUsername;
TokenOIDC get tokenOIDC {
return TokenOIDC(
accessToken,
TokenId(idToken ?? ''),
refreshToken ?? '',
expiredTime: _getExpiredTime(),
);
}
Uri get baseUri => Uri.parse(jmapUrl);
OIDCConfiguration get oidcConfiguration => OIDCConfiguration(
authority: registrationUrl,
clientId: OIDCConstant.clientId,
scopes: AppConfig.oidcScopes,
isTWP: true,
);
@override
List<Object?> get props => [
...super.props,
registrationUrl,
jmapUrl,
username,
accessToken,
refreshToken,
idToken,
expiresIn,
];
}
@@ -0,0 +1,163 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/extensions/either_view_state_extension.dart';
import 'package:core/utils/string_convert.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/user_name.dart';
import 'package:rich_text_composer/views/commons/logger.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_callback_action_define.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
mixin OpenAppDeepLinkHandlerMixin on MessageDialogActionMixin {
OpenAppDeepLinkData? parseOpenAppDeepLink(Uri uri) {
try {
final accessToken = uri.queryParameters['access_token'] ?? '';
final refreshToken = uri.queryParameters['refresh_token'];
final idToken = uri.queryParameters['id_token'];
final expiresInStr = uri.queryParameters['expires_in'];
final username = uri.queryParameters['username'];
final registrationUrl = uri.queryParameters['registrationUrl'] ?? '';
final jmapUrl = uri.queryParameters['jmapUrl'] ?? '';
final expiresIn = expiresInStr != null
? int.tryParse(expiresInStr)
: null;
final usernameDecoded = username?.isNotEmpty == true
? StringConvert.decodeBase64ToString(username!)
: username ?? '';
return OpenAppDeepLinkData(
registrationUrl: registrationUrl,
jmapUrl: jmapUrl,
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
expiresIn: expiresIn,
username: usernameDecoded,
);
} catch (e) {
logError('DeepLinksManager::parseOpenAppDeepLink:Exception = $e');
return null;
}
}
void handleOpenAppDeepLinks({
required OpenAppDeepLinkData openAppDeepLinkData,
OnDeepLinkFailureCallback? onFailureCallback,
OnAutoSignInViaDeepLinkSuccessCallback? onAutoSignInSuccessCallback,
OnDeepLinkConfirmLogoutCallback<OpenAppDeepLinkData>? onConfirmLogoutCallback,
UserName? username,
bool isSignedIn = true,
}) {
if (!openAppDeepLinkData.isValidAuthentication()) {
onFailureCallback?.call();
return;
}
if (isSignedIn) {
if (currentContext == null || username == null) {
onFailureCallback?.call();
return;
}
if (openAppDeepLinkData.isLoggedInWith(username.value)) {
onFailureCallback?.call();
return;
}
_showConfirmDialogSwitchAccount(
context: currentContext!,
currentUsername: username.value,
newUsername: openAppDeepLinkData.username,
onConfirmAction: () => onConfirmLogoutCallback?.call(openAppDeepLinkData),
onCancelAction: () => onFailureCallback?.call()
);
} else {
autoSignInViaDeepLink(
openAppDeepLinkData: openAppDeepLinkData,
onAutoSignInSuccessCallback: (viewState) => onAutoSignInSuccessCallback?.call(viewState),
onFailureCallback: () => onFailureCallback?.call(),
);
}
}
Future<void> autoSignInViaDeepLink({
required OpenAppDeepLinkData openAppDeepLinkData,
required OnAutoSignInViaDeepLinkSuccessCallback onAutoSignInSuccessCallback,
required OnDeepLinkFailureCallback onFailureCallback,
}) async {
try {
final autoSignInViaDeepLinkInteractor = Get.find<AutoSignInViaDeepLinkInteractor>();
final autoSignInViewState = await autoSignInViaDeepLinkInteractor.execute(
baseUri: openAppDeepLinkData.baseUri,
tokenOIDC: openAppDeepLinkData.tokenOIDC,
oidcConfiguration: openAppDeepLinkData.oidcConfiguration,
).last;
autoSignInViewState.foldSuccess<AutoSignInViaDeepLinkSuccess>(
onSuccess: onAutoSignInSuccessCallback,
onFailure: (failure) => onFailureCallback.call(),
);
} catch (e) {
logError('DeepLinksManager::_autoSignInViaDeepLink:Exception = $e');
onFailureCallback.call();
}
}
void _showConfirmDialogSwitchAccount({
required BuildContext context,
required String currentUsername,
required String newUsername,
required Function onConfirmAction,
required Function onCancelAction,
}) {
final appLocalizations = AppLocalizations.of(context);
showConfirmDialogAction(
context,
'',
appLocalizations.yes,
title: appLocalizations.switchAccountConfirmation,
alignCenter: true,
outsideDismissible: false,
titleActionButtonMaxLines: 1,
titlePadding: const EdgeInsetsDirectional.only(start: 24, top: 24, end: 24, bottom: 12),
messageStyle: const TextStyle(
color: AppColor.colorTextBody,
fontSize: 15,
fontWeight: FontWeight.w400,
),
listTextSpan: [
TextSpan(text: appLocalizations.youAreCurrentlyLoggedInWith),
TextSpan(
text: ' $currentUsername',
style: const TextStyle(
color: AppColor.colorTextBody,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const TextSpan(text: '. '),
TextSpan(text: appLocalizations.doYouWantToLogOutAndSwitchTo),
TextSpan(
text: ' $newUsername',
style: const TextStyle(
color: AppColor.colorTextBody,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const TextSpan(text: '?'),
],
onConfirmAction: onConfirmAction,
onCancelAction: onCancelAction,
);
}
}
@@ -4315,6 +4315,27 @@ class AppLocalizations {
); );
} }
String get switchAccountConfirmation {
return Intl.message(
'Switch Account Confirmation',
name: 'switchAccountConfirmation',
);
}
String get youAreCurrentlyLoggedInWith {
return Intl.message(
'You are currently logged in with',
name: 'youAreCurrentlyLoggedInWith',
);
}
String get doYouWantToLogOutAndSwitchTo {
return Intl.message(
'Do you want to log out and switch to',
name: 'doYouWantToLogOutAndSwitchTo',
);
}
String get getHelpOrReportABug { String get getHelpOrReportABug {
return Intl.message( return Intl.message(
'Get help or report a bug', 'Get help or report a bug',
-1
View File
@@ -19,7 +19,6 @@ class AppConfig {
static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy'; static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy';
static const String saasRegistrationUrl = 'https://sign-up.twake.app'; static const String saasRegistrationUrl = 'https://sign-up.twake.app';
static const String saasJmapServerUrl = 'https://jmap.twake.app'; static const String saasJmapServerUrl = 'https://jmap.twake.app';
static const String openAppHostDeepLink = 'openApp';
static String get baseUrl => dotenv.get('SERVER_URL', fallback: ''); static String get baseUrl => dotenv.get('SERVER_URL', fallback: '');
static String get domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: ''); static String get domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: '');
+7 -4
View File
@@ -82,7 +82,7 @@ class IOSSharingManager {
userName: personalAccount.userName! userName: personalAccount.userName!
); );
final tokenRecords = await _getTokenEndpointAndScopes(); final tokenRecords = await _getTokenEndpointScopesAndIsTWP();
final mailboxIdsBlockNotification = await _getMailboxIdsBlockNotification( final mailboxIdsBlockNotification = await _getMailboxIdsBlockNotification(
accountId: personalAccount.accountId!, accountId: personalAccount.accountId!,
@@ -99,7 +99,9 @@ class IOSSharingManager {
basicAuth: credentialInfo, basicAuth: credentialInfo,
tokenEndpoint: tokenRecords?.tokenEndpoint, tokenEndpoint: tokenRecords?.tokenEndpoint,
oidcScopes: tokenRecords?.scopes, oidcScopes: tokenRecords?.scopes,
mailboxIdsBlockNotification: mailboxIdsBlockNotification); mailboxIdsBlockNotification: mailboxIdsBlockNotification,
isTWP: tokenRecords?.isTWP ?? false,
);
await _keychainSharingManager.save(keychainSharingSession); await _keychainSharingManager.save(keychainSharingSession);
@@ -171,13 +173,14 @@ class IOSSharingManager {
} }
} }
Future<({String? tokenEndpoint, List<String>? scopes})?> _getTokenEndpointAndScopes() async { Future<({String? tokenEndpoint, List<String>? scopes, bool isTWP})?> _getTokenEndpointScopesAndIsTWP() async {
try { try {
final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration(); final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration();
final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig); final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig);
return ( return (
tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint, tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint,
scopes: oidcConfig.scopes scopes: oidcConfig.scopes,
isTWP: oidcConfig.isTWP,
); );
} catch (e) { } catch (e) {
logError('IOSSharingManager::_getTokenEndpointAndScopes:Exception: $e'); logError('IOSSharingManager::_getTokenEndpointAndScopes:Exception: $e');
+8 -8
View File
@@ -2,26 +2,25 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class OIDCConfiguration with EquatableMixin { class OIDCConfiguration with EquatableMixin {
final redirectOidcMobile = 'teammail.mobile://oauthredirect'; static const String _wellKnownOpenId = '.well-known/openid-configuration';
final wellKnownOpenId = '.well-known/openid-configuration';
final loginRedirectOidcWeb = 'login-callback.html';
final logoutRedirectOidcWeb = 'logout-callback.html';
final String authority; final String authority;
final String clientId; final String clientId;
final List<String> scopes; final List<String> scopes;
final bool isTWP;
OIDCConfiguration({ OIDCConfiguration({
required this.authority, required this.authority,
required this.clientId, required this.clientId,
required this.scopes required this.scopes,
this.isTWP = false,
}); });
String get discoveryUrl { String get discoveryUrl {
if (authority.endsWith('/')) { if (authority.endsWith('/')) {
return authority + wellKnownOpenId; return authority + _wellKnownOpenId;
} else { } else {
return '$authority/$wellKnownOpenId'; return '$authority/$_wellKnownOpenId';
} }
} }
@@ -29,6 +28,7 @@ class OIDCConfiguration with EquatableMixin {
List<Object?> get props => [ List<Object?> get props => [
authority, authority,
clientId, clientId,
scopes scopes,
isTWP,
]; ];
} }
@@ -68,6 +68,7 @@ void main() {
apiUrl: 'https://jmap.domain.com/oidc/jmap', apiUrl: 'https://jmap.domain.com/oidc/jmap',
tokenEndpoint: 'https://jmap.domain.com/oidc/jmap', tokenEndpoint: 'https://jmap.domain.com/oidc/jmap',
oidcScopes: ['email'], oidcScopes: ['email'],
isTWP: true,
emailState: 'ae08b34da40b48f30ec0b94', emailState: 'ae08b34da40b48f30ec0b94',
tokenOIDC: TokenOIDC( tokenOIDC: TokenOIDC(
'ae08b34da40b48f30ec0b94', 'ae08b34da40b48f30ec0b94',
@@ -88,6 +89,7 @@ void main() {
expect(keychainSession?.emailState, equals('ae08b34da40b48f30ec0b94')); expect(keychainSession?.emailState, equals('ae08b34da40b48f30ec0b94'));
expect(keychainSession?.tokenEndpoint, equals('https://jmap.domain.com/oidc/jmap',)); expect(keychainSession?.tokenEndpoint, equals('https://jmap.domain.com/oidc/jmap',));
expect(keychainSession?.oidcScopes, equals(['email'])); expect(keychainSession?.oidcScopes, equals(['email']));
expect(keychainSession?.isTWP, isTrue);
}); });
}); });
} }
@@ -1,51 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
class MockAutoSignInViaDeepLinkInteractor extends Mock implements AutoSignInViaDeepLinkInteractor {}
void main() {
final mockAutoSignInViaDeepLinkInteractor = MockAutoSignInViaDeepLinkInteractor();
final deepLinkManager = DeepLinksManager(mockAutoSignInViaDeepLinkInteractor);
group('DeepLinksManager::parseDeepLink::test', () {
test('Valid deep link with multiple query parameters', () {
const deepLink = 'twake.mail://openApp?access_token=ey123456&refresh_token=ey7890&id_token=token&expires_in=3600&username=user@example.com';
final expectedData = DeepLinkData(
action: 'openapp',
accessToken: 'ey123456',
refreshToken: 'ey7890',
idToken: 'token',
expiresIn: 3600,
username: 'user@example.com',
);
final deepLinkData = deepLinkManager.parseDeepLink(deepLink);
expect(deepLinkData, equals(expectedData));
});
test('Deep link with no query parameters', () {
const deepLink = 'twake.mail://openApp';
final expectedData = DeepLinkData(action: 'openapp');
final deepLinkData = deepLinkManager.parseDeepLink(deepLink);
expect(deepLinkData, expectedData);
});
test('Deep link with one query parameter', () {
const deepLink = 'twake.mail://openApp?access_token=ey123456';
final expectedData = DeepLinkData(action: 'openapp', accessToken: 'ey123456',);
final deepLinkData = deepLinkManager.parseDeepLink(deepLink);
expect(deepLinkData, equals(expectedData));
});
test('Invalid deep link format', () {
const deepLink = 'Invalid link: invalid';
final deepLinkData = deepLinkManager.parseDeepLink(deepLink);
expect(deepLinkData, isNull);
});
});
}
@@ -0,0 +1,151 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
void main() {
final deepLinkManager = DeepLinksManager();
group('DeepLinksManager::test', () {
group('parseDeepLink::test', () {
test('SHOULD returns correct DeepLinkData for valid openApp deep link', () {
const url = 'twake://openApp/some-data';
final result = deepLinkManager.parseDeepLink(url);
expect(result, isNotNull);
expect(result?.actionType, DeepLinkActionType.openApp);
});
test('SHOULD returns DeepLinkData with unknown action for unhandled action', () {
const url = 'twake://unknownAction/some-data';
final result = deepLinkManager.parseDeepLink(url);
expect(result, isNotNull);
expect(result?.actionType, DeepLinkActionType.unknown);
});
test('SHOULD returns null for malformed URL', () {
const url = 'Invalid link: invalid';
final result = deepLinkManager.parseDeepLink(url);
expect(result, isNull);
});
test('SHOULD returns null for exception during parsing', () {
const url = 'twake://malformedurl%';
final result = deepLinkManager.parseDeepLink(url);
expect(result, isNull);
});
});
group('parseOpenAppDeepLink::test', () {
test('SHOULD returns OpenAppDeepLinkData with all valid parameters', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&refresh_token=refresh123'
'&id_token=id123'
'&expires_in=3600'
'&username=dXNlcg=='
'&registrationUrl=https://registration.url'
'&jmapUrl=https://jmap.url',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.accessToken, 'token123');
expect(result?.refreshToken, 'refresh123');
expect(result?.idToken, 'id123');
expect(result?.expiresIn, 3600);
expect(result?.username, 'user');
expect(result?.registrationUrl, 'https://registration.url');
expect(result?.jmapUrl, 'https://jmap.url');
expect(result?.isValidAuthentication(), isTrue);
});
test('SHOULD returns OpenAppDeepLinkData with missing optional parameters', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&username=user@example.com'
'&registrationUrl=https://registration.url'
'&jmapUrl=https://jmap.url',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.accessToken, 'token123');
expect(result?.refreshToken, isNull);
expect(result?.idToken, isNull);
expect(result?.expiresIn, isNull);
expect(result?.username, 'user@example.com');
expect(result?.registrationUrl, 'https://registration.url');
expect(result?.jmapUrl, 'https://jmap.url');
expect(result?.isValidAuthentication(), isTrue);
});
test('SHOULD returns OpenAppDeepLinkData with invalid expires_in', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&expires_in=not_a_number'
'&registrationUrl=https://registration.url'
'&jmapUrl=https://jmap.url',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.expiresIn, isNull);
});
test('SHOULD returns OpenAppDeepLinkData with origin username if Base64 decoding fails', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&username=invalid_base64'
'&registrationUrl=https://registration.url'
'&jmapUrl=https://jmap.url',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.username, 'invalid_base64');
});
test('SHOULD returns OpenAppDeepLinkData with jmapUrl contains sub-path and port', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&refresh_token=refresh123'
'&id_token=id123'
'&expires_in=3600'
'&username=dXNlcg=='
'&registrationUrl=https://registration.url'
'&jmapUrl=https://jmap.url:1000/jmap',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.jmapUrl, 'https://jmap.url:1000/jmap');
});
test('SHOULD returns OpenAppDeepLinkData with registrationUrl contains sub-path and port', () {
final uri = Uri.parse(
'twake://openApp?access_token=token123'
'&refresh_token=refresh123'
'&id_token=id123'
'&expires_in=3600'
'&username=dXNlcg=='
'&registrationUrl=https://registration.url:1000/register'
'&jmapUrl=https://jmap.url',
);
final result = deepLinkManager.parseOpenAppDeepLink(uri);
expect(result, isNotNull);
expect(result?.registrationUrl, 'https://registration.url:1000/register');
});
});
});
}