TF-3278 Handle open app via deep link at MailboxDashboard screen
This commit is contained in:
@@ -52,6 +52,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart';
|
||||
export 'utils/list_utils.dart';
|
||||
export 'utils/mail/domain.dart';
|
||||
export 'utils/mail/mail_address.dart';
|
||||
export 'utils/application_manager.dart';
|
||||
export 'utils/preview_eml_file_utils.dart';
|
||||
|
||||
// Views
|
||||
|
||||
@@ -2,7 +2,18 @@ import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.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> {
|
||||
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>() {
|
||||
return fold(
|
||||
(failure) => failure,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'app_logger.dart';
|
||||
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/domain/exceptions/string_exception.dart';
|
||||
|
||||
class StringConvert {
|
||||
@@ -17,6 +16,15 @@ class StringConvert {
|
||||
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) {
|
||||
try {
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,42 @@ import 'package:core/utils/string_convert.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
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('Basic Functionality', () {
|
||||
test('should extract strings separated by spaces', () {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
PODS:
|
||||
- app_links (0.0.2):
|
||||
- Flutter
|
||||
- app_settings (5.1.1):
|
||||
- Flutter
|
||||
- AppAuth (1.7.4):
|
||||
@@ -204,6 +206,7 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- app_settings (from `.symlinks/plugins/app_settings/ios`)
|
||||
- better_open_file (from `.symlinks/plugins/better_open_file/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
@@ -264,6 +267,8 @@ SPEC REPOS:
|
||||
- UniversalDetector2
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
app_settings:
|
||||
:path: ".symlinks/plugins/app_settings/ios"
|
||||
better_open_file:
|
||||
@@ -334,6 +339,7 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
|
||||
AppAuth: 182c5b88630569df5acb672720534756c29b3358
|
||||
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>teammail.mobile</string>
|
||||
<string>twakemail.mobile</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
@@ -24,6 +24,7 @@ class JmapClient {
|
||||
basicAuth: String?,
|
||||
tokenEndpointUrl: String?,
|
||||
oidcScopes: [String]?,
|
||||
isTWP: Bool?,
|
||||
onComplete: @escaping ([Email], [Error]) -> Void
|
||||
) {
|
||||
if (authenticationType == AuthenticationType.basic) {
|
||||
@@ -42,7 +43,8 @@ class JmapClient {
|
||||
tokenRefreshManager = TokenRefreshManager(
|
||||
refreshToken: tokenOidc?.refreshToken ?? "",
|
||||
tokenEndpoint: tokenEndpointUrl ?? "",
|
||||
scopes: oidcScopes
|
||||
scopes: oidcScopes,
|
||||
isTWP: isTWP
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ class TokenRefreshManager {
|
||||
private let MOBIE_CLIENT_ID = "teammail-mobile"
|
||||
private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect"
|
||||
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 REFRESH_TOKEN = "refresh_token"
|
||||
@@ -15,11 +16,13 @@ class TokenRefreshManager {
|
||||
let refreshToken: String
|
||||
let tokenEndpoint: 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.tokenEndpoint = tokenEndpoint
|
||||
self.scopes = scopes
|
||||
self.isTWP = isTWP
|
||||
}
|
||||
|
||||
private func getScopes() -> String {
|
||||
@@ -28,6 +31,14 @@ class TokenRefreshManager {
|
||||
}
|
||||
return OIDC_SCOPES.joined(separator: " ")
|
||||
}
|
||||
|
||||
private func getRedirectUrl() -> String {
|
||||
if (isTWP == true) {
|
||||
return TWP_MOBIE_REDIRECT_URL
|
||||
} else {
|
||||
return MOBIE_REDIRECT_URL
|
||||
}
|
||||
}
|
||||
|
||||
func handleRefreshAccessToken(
|
||||
onSuccess: @escaping (TokenResponse) -> Void,
|
||||
@@ -41,7 +52,7 @@ class TokenRefreshManager {
|
||||
let params = [
|
||||
CLIENT_ID: MOBIE_CLIENT_ID,
|
||||
GRANT_TYPE: REFRESH_TOKEN,
|
||||
REDIRECT_URI: MOBIE_REDIRECT_URL,
|
||||
REDIRECT_URI: getRedirectUrl(),
|
||||
REFRESH_TOKEN: refreshToken,
|
||||
SCOPES: getScopes(),
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ struct KeychainSharingSession: Codable {
|
||||
let tokenEndpoint: String?
|
||||
let oidcScopes: [String]?
|
||||
let mailboxIdsBlockNotification: [String]?
|
||||
let isTWP: Bool?
|
||||
}
|
||||
|
||||
extension KeychainSharingSession {
|
||||
@@ -27,7 +28,8 @@ extension KeychainSharingSession {
|
||||
basicAuth: self.basicAuth,
|
||||
tokenEndpoint: self.tokenEndpoint,
|
||||
oidcScopes: self.oidcScopes,
|
||||
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification
|
||||
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
|
||||
isTWP: self.isTWP
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +50,8 @@ extension KeychainSharingSession {
|
||||
basicAuth: self.basicAuth,
|
||||
tokenEndpoint: self.tokenEndpoint,
|
||||
oidcScopes: self.oidcScopes,
|
||||
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification
|
||||
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
|
||||
isTWP: self.isTWP
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
basicAuth: keychainSharingSession.basicAuth,
|
||||
tokenEndpointUrl: keychainSharingSession.tokenEndpoint,
|
||||
oidcScopes: keychainSharingSession.oidcScopes,
|
||||
isTWP: keychainSharingSession.isTWP,
|
||||
onComplete: { (emails, errors) in
|
||||
do {
|
||||
if emails.isEmpty {
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
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: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:fcm/model/firebase_capability.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/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/login/data/network/config/oidc_constant.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
||||
@@ -203,6 +193,8 @@ abstract class BaseController extends GetxController
|
||||
clearDataAndGoToLoginPage();
|
||||
}
|
||||
|
||||
void onCancelReconnectWhenSessionExpired() {}
|
||||
|
||||
void _handleConnectionErrorException() {
|
||||
if (currentOverlayContext != null && currentContext != null) {
|
||||
appToast.showToastErrorMessage(
|
||||
@@ -241,7 +233,9 @@ abstract class BaseController extends GetxController
|
||||
outsideDismissible: false,
|
||||
titleActionButtonMaxLines: 1,
|
||||
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
|
||||
onConfirmAction: _executeBeforeReconnectAndLogOut);
|
||||
onConfirmAction: _executeBeforeReconnectAndLogOut,
|
||||
onCancelAction: onCancelReconnectWhenSessionExpired
|
||||
);
|
||||
} else if (PlatformInfo.isMobile) {
|
||||
if (currentContext == null) {
|
||||
clearDataAndGoToLoginPage();
|
||||
@@ -257,7 +251,9 @@ abstract class BaseController extends GetxController
|
||||
outsideDismissible: false,
|
||||
titleActionButtonMaxLines: 1,
|
||||
icon: SvgPicture.asset(imagePaths.icTMailLogo, width: 64, height: 64),
|
||||
onConfirmAction: clearDataAndGoToLoginPage);
|
||||
onConfirmAction: clearDataAndGoToLoginPage,
|
||||
onCancelAction: onCancelReconnectWhenSessionExpired
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,20 +473,22 @@ abstract class BaseController extends GetxController
|
||||
required Session session,
|
||||
required AccountId accountId,
|
||||
required Function onSuccessCallback,
|
||||
required Function onFailureCallback,
|
||||
required Function({Object? exception}) onFailureCallback,
|
||||
}) async {
|
||||
try {
|
||||
_isFcmEnabled = _isFcmActivated(session, accountId);
|
||||
|
||||
if (isAuthenticatedWithOidc) {
|
||||
final logoutViewState = await logoutOidcInteractor.execute().last;
|
||||
|
||||
logoutViewState.fold(
|
||||
(failure) => onFailureCallback(),
|
||||
(success) async {
|
||||
if (success is LogoutOidcSuccess) {
|
||||
logoutViewState.foldSuccess<LogoutOidcSuccess>(
|
||||
onSuccess: (success) async {
|
||||
await _handleDeleteFCMAndClearData();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onFailure: (failure) async {
|
||||
if (failure is LogoutOidcFailure && _validateUserCancelledLogoutOidcFlow(failure.exception)) {
|
||||
await _handleDeleteFCMAndClearData();
|
||||
onSuccessCallback();
|
||||
onFailureCallback(exception: UserCancelledLogoutOIDCFlowException());
|
||||
} else {
|
||||
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 {
|
||||
await Future.wait([
|
||||
if (_isFcmEnabled)
|
||||
@@ -519,14 +522,12 @@ abstract class BaseController extends GetxController
|
||||
_getStoredFirebaseRegistrationInteractor = getBinding<GetStoredFirebaseRegistrationInteractor>();
|
||||
final fcmRegistration = await _getStoredFirebaseRegistrationInteractor?.execute().last;
|
||||
|
||||
fcmRegistration?.fold(
|
||||
(failure) => null,
|
||||
(success) async {
|
||||
if (success is GetStoredFirebaseRegistrationSuccess) {
|
||||
_destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
|
||||
await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
|
||||
}
|
||||
fcmRegistration?.foldSuccess<GetStoredFirebaseRegistrationSuccess>(
|
||||
onSuccess: (success) async {
|
||||
_destroyFirebaseRegistrationInteractor = getBinding<DestroyFirebaseRegistrationInteractor>();
|
||||
await _destroyFirebaseRegistrationInteractor?.execute(success.firebaseRegistration.id!).last;
|
||||
},
|
||||
onFailure: (failure) {},
|
||||
);
|
||||
} catch (e) {
|
||||
logError('BaseController::_handleDeleteFCMRegistration:Exception = $e');
|
||||
|
||||
@@ -34,7 +34,7 @@ abstract class ReloadableController extends BaseController {
|
||||
goToLogin();
|
||||
} else if (failure is GetSessionFailure) {
|
||||
logError('$runtimeType::handleFailureViewState():Failure = $failure');
|
||||
handleGetSessionFailure(failure.exception);
|
||||
handleGetSessionFailure(failure);
|
||||
} else if (failure is UpdateAccountCacheFailure) {
|
||||
logError('$runtimeType::handleFailureViewState():Failure = $failure');
|
||||
_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/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/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_username_cache.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/model/token_oidc_cache.dart';
|
||||
@@ -185,6 +186,10 @@ class HiveCacheConfig {
|
||||
SessionHiveObjAdapter(),
|
||||
CachingConstants.SESSION_HIVE_CACHE_ID
|
||||
);
|
||||
registerCacheAdapter<OidcConfigurationCache>(
|
||||
OidcConfigurationCacheAdapter(),
|
||||
CachingConstants.OIDC_CONFIGURATION_CACHE_ID,
|
||||
);
|
||||
}
|
||||
|
||||
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 SENDING_EMAIL_HIVE_CACHE_ID = 18;
|
||||
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 newEmailCacheBoxName = 'new_email_cache_box';
|
||||
@@ -26,6 +27,9 @@ class CachingConstants {
|
||||
static const String sendingEmailCacheBoxName = 'sending_email_cache_box';
|
||||
static const String sessionCacheBoxName = 'session_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 openedEmailContentFolderName = 'opened_email';
|
||||
|
||||
@@ -32,7 +32,7 @@ class AutoSignInViaDeepLinkInteractor {
|
||||
await Future.wait([
|
||||
_credentialRepository.saveBaseUrl(baseUri),
|
||||
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
|
||||
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
|
||||
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
|
||||
]);
|
||||
|
||||
await _accountRepository.setCurrentAccount(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||
import 'package:model/account/personal_account.dart';
|
||||
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
|
||||
@@ -21,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/home/domain/state/auto_sign_in_via_deep_link_state.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart';
|
||||
import 'package:tmail_ui_user/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/routes/app_routes.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -65,7 +69,7 @@ class HomeController extends ReloadableController {
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
_cleanupCache();
|
||||
_handleNavigateToScreen();
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
@@ -76,6 +80,11 @@ class HomeController extends ReloadableController {
|
||||
arguments: session);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCancelReconnectWhenSessionExpired() {
|
||||
clearDataAndGoToLoginPage();
|
||||
}
|
||||
|
||||
void _initFlutterDownloader() {
|
||||
FlutterDownloader
|
||||
.initialize(debug: kDebugMode)
|
||||
@@ -84,6 +93,15 @@ class HomeController extends ReloadableController {
|
||||
|
||||
static void downloadCallback(String id, DownloadTaskStatus status, int progress) {}
|
||||
|
||||
void _handleNavigateToScreen() {
|
||||
final arguments = Get.arguments;
|
||||
if (arguments is LoginNavigateArguments) {
|
||||
_handleLoginNavigateArguments(arguments);
|
||||
} else {
|
||||
_cleanupCache();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanupCache() async {
|
||||
await HiveCacheConfig.instance.onUpgradeDatabase(cachingManager);
|
||||
|
||||
@@ -95,6 +113,17 @@ class HomeController extends ReloadableController {
|
||||
], eagerError: true).then((_) => getAuthenticatedAccountAction());
|
||||
}
|
||||
|
||||
void _handleLoginNavigateArguments(LoginNavigateArguments arguments) {
|
||||
switch (arguments.navigateType) {
|
||||
case LoginNavigateType.autoSignIn:
|
||||
_handleAutoSignInViaDeepLinkSuccess(arguments.autoSignInViaDeepLinkSuccess!);
|
||||
break;
|
||||
default:
|
||||
_cleanupCache();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _registerReceivingFileSharing() {
|
||||
_emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed();
|
||||
}
|
||||
@@ -124,7 +153,7 @@ class HomeController extends ReloadableController {
|
||||
void _handleLogOutAndSignInNewAccount({
|
||||
required Success authenticationViewStateSuccess,
|
||||
required PersonalAccount personalAccount,
|
||||
required DeepLinkData deepLinkData,
|
||||
required OpenAppDeepLinkData openAppDeepLinkData,
|
||||
}) {
|
||||
if (authenticationViewStateSuccess is GetCredentialViewState) {
|
||||
setDataToInterceptors(
|
||||
@@ -143,14 +172,14 @@ class HomeController extends ReloadableController {
|
||||
_getSessionActionToLogOut(
|
||||
authenticationViewStateSuccess: authenticationViewStateSuccess,
|
||||
personalAccount: personalAccount,
|
||||
deepLinkData: deepLinkData,
|
||||
openAppDeepLinkData: openAppDeepLinkData,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _getSessionActionToLogOut({
|
||||
required Success authenticationViewStateSuccess,
|
||||
required PersonalAccount personalAccount,
|
||||
required DeepLinkData deepLinkData,
|
||||
required OpenAppDeepLinkData openAppDeepLinkData,
|
||||
}) async {
|
||||
try {
|
||||
final sessionViewState = await getSessionInteractor.execute().last;
|
||||
@@ -161,7 +190,7 @@ class HomeController extends ReloadableController {
|
||||
sessionViewStateSuccess: success,
|
||||
authenticationViewStateSuccess: authenticationViewStateSuccess,
|
||||
personalAccount: personalAccount,
|
||||
deepLinkData: deepLinkData,
|
||||
openAppDeepLinkData: openAppDeepLinkData,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -174,18 +203,27 @@ class HomeController extends ReloadableController {
|
||||
required Success sessionViewStateSuccess,
|
||||
required Success authenticationViewStateSuccess,
|
||||
required PersonalAccount personalAccount,
|
||||
required DeepLinkData deepLinkData,
|
||||
required OpenAppDeepLinkData openAppDeepLinkData,
|
||||
}) {
|
||||
if (sessionViewStateSuccess is GetSessionSuccess) {
|
||||
logoutToSignInNewAccount(
|
||||
session: sessionViewStateSuccess.session,
|
||||
accountId: personalAccount.accountId!,
|
||||
onFailureCallback: () =>
|
||||
_continueUsingTheApp(authenticationViewStateSuccess),
|
||||
onFailureCallback: ({exception}) {
|
||||
if (exception is UserCancelledLogoutOIDCFlowException) {
|
||||
_deepLinksManager?.autoSignInViaDeepLink(
|
||||
openAppDeepLinkData: openAppDeepLinkData,
|
||||
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
|
||||
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
);
|
||||
} else {
|
||||
_continueUsingTheApp(authenticationViewStateSuccess);
|
||||
}
|
||||
},
|
||||
onSuccessCallback: () => _deepLinksManager?.autoSignInViaDeepLink(
|
||||
deepLinkData: deepLinkData,
|
||||
openAppDeepLinkData: openAppDeepLinkData,
|
||||
onFailureCallback: () => _continueUsingTheApp(authenticationViewStateSuccess),
|
||||
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -234,8 +272,19 @@ class HomeController extends ReloadableController {
|
||||
@override
|
||||
void handleFailureViewState(Failure failure) {
|
||||
if (_validateToHandleDeepLinksNotSignedIn(failure)) {
|
||||
_deepLinksManager!.handleDeepLinksWhenAppTerminatedNotSignedIn(
|
||||
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
_deepLinksManager!.handleDeepLinksWhenAppTerminated(
|
||||
onSuccessCallback: (deepLinkData) {
|
||||
if (deepLinkData is OpenAppDeepLinkData) {
|
||||
_deepLinksManager!.handleOpenAppDeepLinks(
|
||||
openAppDeepLinkData: deepLinkData,
|
||||
isSignedIn: false,
|
||||
onFailureCallback: goToLogin,
|
||||
onAutoSignInSuccessCallback: _handleAutoSignInViaDeepLinkSuccess
|
||||
);
|
||||
} else {
|
||||
goToLogin();
|
||||
}
|
||||
},
|
||||
onFailureCallback: goToLogin,
|
||||
);
|
||||
} else {
|
||||
@@ -248,14 +297,25 @@ class HomeController extends ReloadableController {
|
||||
if (_validateToHandleDeepLinksSignedIn(success)) {
|
||||
final personalAccount = _getPersonalAccountFromViewStateSuccess(success);
|
||||
|
||||
_deepLinksManager!.handleDeepLinksWhenAppTerminatedSignedIn(
|
||||
username: personalAccount?.userName?.value,
|
||||
_deepLinksManager!.handleDeepLinksWhenAppTerminated(
|
||||
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),
|
||||
onConfirmCallback: (deepLinkData) => _handleLogOutAndSignInNewAccount(
|
||||
authenticationViewStateSuccess: success,
|
||||
personalAccount: personalAccount!,
|
||||
deepLinkData: deepLinkData,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
super.handleSuccessViewState(success);
|
||||
|
||||
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCDataSource {
|
||||
|
||||
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
|
||||
|
||||
Future<void> persistAuthorityOidc(String authority);
|
||||
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
|
||||
|
||||
Future<void> deleteAuthorityOidc();
|
||||
Future<void> deleteOidcConfiguration();
|
||||
|
||||
Future<OIDCConfiguration> getStoredOidcConfiguration();
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> persistAuthorityOidc(String authority) {
|
||||
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
|
||||
return Future.sync(() async {
|
||||
return await _oidcConfigurationCacheManager.persistAuthorityOidc(authority);
|
||||
return await _oidcConfigurationCacheManager.persistOidcConfiguration(oidcConfiguration);
|
||||
}).catchError(_cacheExceptionThrower.throwException);
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAuthorityOidc() {
|
||||
Future<void> deleteOidcConfiguration() {
|
||||
return Future.sync(() async {
|
||||
return await _oidcConfigurationCacheManager.deleteAuthorityOidc();
|
||||
return await _oidcConfigurationCacheManager.deleteOidcConfiguration();
|
||||
}).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:model/oidc/oidc_configuration.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/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';
|
||||
|
||||
class OidcConfigurationCacheManager {
|
||||
final SharedPreferences _sharedPreferences;
|
||||
final OidcConfigurationCacheClient _oidcConfigurationCacheClient;
|
||||
|
||||
OidcConfigurationCacheManager(this._sharedPreferences);
|
||||
OidcConfigurationCacheManager(this._sharedPreferences, this._oidcConfigurationCacheClient);
|
||||
|
||||
Future<OIDCConfiguration> getOidcConfiguration() async {
|
||||
final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc);
|
||||
if (authority == null || authority.isEmpty) {
|
||||
throw CanNotFoundOIDCAuthority();
|
||||
} else {
|
||||
final oidcConfigurationCache = await _oidcConfigurationCacheClient.getItem(CachingConstants.oidcConfigurationCacheKeyName);
|
||||
if (oidcConfigurationCache == null) {
|
||||
final authority = _sharedPreferences.getString(OIDCConstant.keyAuthorityOidc);
|
||||
|
||||
if (authority == null || authority.isEmpty) {
|
||||
throw CanNotFoundOIDCAuthority();
|
||||
}
|
||||
|
||||
return OIDCConfiguration(
|
||||
authority: authority,
|
||||
clientId: OIDCConstant.clientId,
|
||||
scopes: AppConfig.oidcScopes);
|
||||
authority: authority,
|
||||
clientId: OIDCConstant.clientId,
|
||||
scopes: AppConfig.oidcScopes,
|
||||
);
|
||||
} else {
|
||||
return oidcConfigurationCache.toOIDCConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> persistAuthorityOidc(String authority) async {
|
||||
log('OidcConfigurationCacheManager::persistAuthorityOidc(): $authority');
|
||||
await _sharedPreferences.setString(OIDCConstant.keyAuthorityOidc, authority);
|
||||
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) async {
|
||||
log('OidcConfigurationCacheManager::persistOidcConfiguration(): $oidcConfiguration');
|
||||
await _oidcConfigurationCacheClient.insertItem(
|
||||
CachingConstants.oidcConfigurationCacheKeyName,
|
||||
oidcConfiguration.toOidcConfigurationCache(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteAuthorityOidc() async {
|
||||
log('OidcConfigurationCacheManager::deleteAuthorityOidc()');
|
||||
await _sharedPreferences.remove(OIDCConstant.keyAuthorityOidc);
|
||||
Future<void> deleteOidcConfiguration() async {
|
||||
log('OidcConfigurationCacheManager::deleteOidcConfiguration()');
|
||||
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 {
|
||||
static String get mobileOidcClientId => 'teammail-mobile';
|
||||
static List<String> get oidcScope => ['openid', 'profile', 'email', 'offline_access'];
|
||||
static const keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
|
||||
static const authResponseKey = "auth_info";
|
||||
static const String keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
|
||||
static const String authResponseKey = "auth_info";
|
||||
static const String twakeWorkplaceUrlScheme = 'twakemail.mobile';
|
||||
static const String twakeWorkplaceRedirectUrl = '$twakeWorkplaceUrlScheme://redirect';
|
||||
static const String appParameter = 'tmail';
|
||||
static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url';
|
||||
static const String postLoginRedirectUrlPathParams = 'post_login_redirect_url';
|
||||
static const String endSessionFailedCode = 'end_session_failed';
|
||||
static 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;
|
||||
}
|
||||
@@ -52,11 +52,14 @@ class OIDCHttpClient {
|
||||
if (oidcResponse.links.isEmpty) {
|
||||
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(
|
||||
authority: oidcResponse.links[0].href.toString(),
|
||||
authority: authority,
|
||||
clientId: OIDCConstant.clientId,
|
||||
scopes: AppConfig.oidcScopes
|
||||
scopes: AppConfig.oidcScopes,
|
||||
isTWP: isTWP,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
|
||||
return _oidcDataSource.getOIDCConfiguration(oidcResponse);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<OIDCDiscoveryResponse> discoverOIDC(OIDCConfiguration oidcConfiguration) {
|
||||
return _oidcDataSource.discoverOIDC(oidcConfiguration);
|
||||
@@ -49,8 +48,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> persistAuthorityOidc(String authority) {
|
||||
return _oidcDataSource.persistAuthorityOidc(authority);
|
||||
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration) {
|
||||
return _oidcDataSource.persistOidcConfiguration(oidcConfiguration);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -75,8 +74,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAuthorityOidc() {
|
||||
return _oidcDataSource.deleteAuthorityOidc();
|
||||
Future<void> deleteOidcConfiguration() {
|
||||
return _oidcDataSource.deleteOidcConfiguration();
|
||||
}
|
||||
|
||||
@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: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/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';
|
||||
|
||||
@@ -12,32 +13,29 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
|
||||
String get redirectUrl {
|
||||
if (PlatformInfo.isWeb) {
|
||||
return AppConfig.domainRedirectUrl.endsWith('/')
|
||||
? AppConfig.domainRedirectUrl + loginRedirectOidcWeb
|
||||
: '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb';
|
||||
? AppConfig.domainRedirectUrl + OIDCConstant.loginRedirectOidcWeb
|
||||
: '${AppConfig.domainRedirectUrl}/${OIDCConstant.loginRedirectOidcWeb}';
|
||||
} else {
|
||||
return _isSaasAuthority(authority)
|
||||
return isTWP
|
||||
? OIDCConstant.twakeWorkplaceRedirectUrl
|
||||
: redirectOidcMobile;
|
||||
: OIDCConstant.redirectOidcMobile;
|
||||
}
|
||||
}
|
||||
|
||||
String get logoutRedirectUrl {
|
||||
if (PlatformInfo.isWeb) {
|
||||
if (AppConfig.domainRedirectUrl.endsWith('/')) {
|
||||
return AppConfig.domainRedirectUrl + logoutRedirectOidcWeb;
|
||||
return AppConfig.domainRedirectUrl + OIDCConstant.logoutRedirectOidcWeb;
|
||||
} else {
|
||||
return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb';
|
||||
return '${AppConfig.domainRedirectUrl}/${OIDCConstant.logoutRedirectOidcWeb}';
|
||||
}
|
||||
} else {
|
||||
return _isSaasAuthority(authority)
|
||||
return isTWP
|
||||
? OIDCConstant.twakeWorkplaceRedirectUrl
|
||||
: redirectOidcMobile;
|
||||
: OIDCConstant.redirectOidcMobile;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSaasAuthority(String authority) =>
|
||||
authority == AppConfig.saasRegistrationUrl;
|
||||
|
||||
String get signInTWPUrl => ServicePath(authority)
|
||||
.withQueryParameters([
|
||||
StringQueryParameter(
|
||||
@@ -57,4 +55,8 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
|
||||
StringQueryParameter('app', OIDCConstant.appParameter),
|
||||
])
|
||||
.generateEndpointPath();
|
||||
|
||||
OidcConfigurationCache toOidcConfigurationCache() {
|
||||
return OidcConfigurationCache(authority, isTWP);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@ abstract class AuthenticationOIDCRepository {
|
||||
|
||||
Future<TokenOIDC> getStoredTokenOIDC(String tokenIdHash);
|
||||
|
||||
Future<void> persistAuthorityOidc(String authority);
|
||||
Future<void> persistOidcConfiguration(OIDCConfiguration oidcConfiguration);
|
||||
|
||||
Future<void> deleteAuthorityOidc();
|
||||
Future<void> deleteOidcConfiguration();
|
||||
|
||||
Future<OIDCConfiguration> getStoredOidcConfiguration();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class DeleteAuthorityOidcInteractor {
|
||||
try {
|
||||
await Future.wait([
|
||||
_credentialRepository.removeBaseUrl(),
|
||||
_authenticationOIDCRepository.deleteAuthorityOidc(),
|
||||
_authenticationOIDCRepository.deleteOidcConfiguration(),
|
||||
_authenticationOIDCRepository.deleteTokenOIDC(),
|
||||
]);
|
||||
return Right(DeleteAuthorityOidcSuccess());
|
||||
|
||||
@@ -15,7 +15,7 @@ class GetOIDCConfigurationInteractor {
|
||||
try {
|
||||
yield Right<Failure, Success>(GetOIDCConfigurationLoading());
|
||||
final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(oidcResponse);
|
||||
await _oidcRepository.persistAuthorityOidc(oidcConfiguration.authority);
|
||||
await _oidcRepository.persistOidcConfiguration(oidcConfiguration);
|
||||
yield Right<Failure, Success>(GetOIDCConfigurationSuccess(oidcConfiguration));
|
||||
} catch (e) {
|
||||
log('GetOIDCConfigurationInteractor::execute(): ERROR: $e');
|
||||
|
||||
@@ -35,7 +35,7 @@ class GetTokenOIDCInteractor {
|
||||
await Future.wait([
|
||||
_credentialRepository.saveBaseUrl(baseUrl),
|
||||
authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
|
||||
authenticationOIDCRepository.persistAuthorityOidc(config.authority),
|
||||
authenticationOIDCRepository.persistOidcConfiguration(config),
|
||||
]);
|
||||
|
||||
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/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/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -163,6 +164,7 @@ class LoginController extends ReloadableController {
|
||||
} else if (failure is GetAuthenticatedAccountFailure) {
|
||||
_checkOIDCIsAvailable();
|
||||
} else if (failure is GetSessionFailure) {
|
||||
SmartDialog.dismiss();
|
||||
clearAllData();
|
||||
} else if (failure is DNSLookupToGetJmapUrlFailure) {
|
||||
_username = null;
|
||||
@@ -218,6 +220,7 @@ class LoginController extends ReloadableController {
|
||||
_handleNoSuitableBrowserOIDC(failure)
|
||||
.map((stillFailed) => _handleCommonOIDCFailure());
|
||||
} else if (failure is GetSessionFailure) {
|
||||
SmartDialog.dismiss();
|
||||
clearAllData();
|
||||
} else {
|
||||
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() {
|
||||
_deepLinksManager = getBinding<DeepLinksManager>();
|
||||
_deepLinksManager?.clearPendingDeepLinkData();
|
||||
@@ -250,16 +247,21 @@ class LoginController extends ReloadableController {
|
||||
|
||||
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
|
||||
log('LoginController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
|
||||
if (deepLinkData == null) return;
|
||||
|
||||
if (currentContext != null) {
|
||||
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
|
||||
}
|
||||
|
||||
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
|
||||
_deepLinksManager?.handleDeepLinksWhenAppRunning(
|
||||
deepLinkData: deepLinkData,
|
||||
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
onFailureCallback: SmartDialog.dismiss,
|
||||
onSuccessCallback: (deepLinkData) {
|
||||
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
|
||||
}
|
||||
+80
@@ -8,6 +8,7 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart';
|
||||
import 'package:email_recovery/email_recovery/email_recovery_action_id.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
@@ -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_recovery/presentation/model/email_recovery_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/usecases/store_session_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/domain/state/get_identity_cache_on_web_state.dart';
|
||||
import 'package:tmail_ui_user/features/identity_creator/domain/usecase/get_identity_cache_on_web_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/exceptions/logout_exception.dart';
|
||||
import 'package:tmail_ui_user/features/login/presentation/model/login_navigate_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart';
|
||||
@@ -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/move_multiple_email_to_mailbox_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
|
||||
import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||
@@ -254,6 +261,8 @@ class MailboxDashBoardController extends ReloadableController
|
||||
StreamSubscription? _receivingFileSharingStreamSubscription;
|
||||
StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription;
|
||||
bool _isFirstSessionLoad = false;
|
||||
DeepLinksManager? _deepLinksManager;
|
||||
StreamSubscription<DeepLinkData?>? _deepLinkDataStreamSubscription;
|
||||
|
||||
final StreamController<Either<Failure, Success>> _progressStateController =
|
||||
StreamController<Either<Failure, Success>>.broadcast();
|
||||
@@ -301,6 +310,7 @@ class MailboxDashBoardController extends ReloadableController
|
||||
void onInit() {
|
||||
if (PlatformInfo.isMobile) {
|
||||
_registerReceivingFileSharingStream();
|
||||
_registerDeepLinks();
|
||||
}
|
||||
_registerStreamListener();
|
||||
BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard);
|
||||
@@ -467,6 +477,8 @@ class MailboxDashBoardController extends ReloadableController
|
||||
|
||||
@override
|
||||
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
|
||||
SmartDialog.dismiss();
|
||||
|
||||
if (failure is SendEmailFailure && exception is NoNetworkError) {
|
||||
_storeSendingEmailInCaseOfSendingFailureInMobile(failure);
|
||||
} 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() {
|
||||
_iosNotificationManager = getBinding<IOSNotificationManager>();
|
||||
_currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager
|
||||
@@ -1625,6 +1702,8 @@ class MailboxDashBoardController extends ReloadableController
|
||||
@override
|
||||
void handleReloaded(Session session) {
|
||||
log('MailboxDashBoardController::handleReloaded():');
|
||||
SmartDialog.dismiss();
|
||||
|
||||
_getRouteParameters();
|
||||
_setUpComponentsFromSession(session);
|
||||
if (PlatformInfo.isWeb) {
|
||||
@@ -3144,6 +3223,7 @@ class MailboxDashBoardController extends ReloadableController
|
||||
_pendingSharedFileInfoSubscription?.cancel();
|
||||
_receivingFileSharingStreamSubscription?.cancel();
|
||||
_emailReceiveManager.closeEmailReceiveManagerStream();
|
||||
_deepLinkDataStreamSubscription?.cancel();
|
||||
}
|
||||
_progressStateController.close();
|
||||
_refreshActionEventController.close();
|
||||
|
||||
@@ -17,6 +17,7 @@ extension KeychainSharingSessionExtension on KeychainSharingSession {
|
||||
tokenEndpoint: tokenEndpoint,
|
||||
oidcScopes: oidcScopes,
|
||||
mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification,
|
||||
isTWP: isTWP,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class KeychainSharingSession with EquatableMixin {
|
||||
String? tokenEndpoint;
|
||||
List<String>? oidcScopes;
|
||||
List<MailboxId>? mailboxIdsBlockNotification;
|
||||
bool isTWP;
|
||||
|
||||
KeychainSharingSession({
|
||||
required this.accountId,
|
||||
@@ -40,6 +41,7 @@ class KeychainSharingSession with EquatableMixin {
|
||||
this.tokenEndpoint,
|
||||
this.oidcScopes,
|
||||
this.mailboxIdsBlockNotification,
|
||||
this.isTWP = false,
|
||||
});
|
||||
|
||||
factory KeychainSharingSession.fromJson(Map<String, dynamic> json) => _$KeychainSharingSessionFromJson(json);
|
||||
@@ -59,5 +61,6 @@ class KeychainSharingSession with EquatableMixin {
|
||||
tokenEndpoint,
|
||||
oidcScopes,
|
||||
mailboxIdsBlockNotification,
|
||||
isTWP,
|
||||
];
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class SignInTwakeWorkplaceInteractor {
|
||||
await Future.wait([
|
||||
_credentialRepository.saveBaseUrl(baseUri),
|
||||
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
|
||||
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
|
||||
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
|
||||
]);
|
||||
|
||||
await _accountRepository.setCurrentAccount(
|
||||
|
||||
@@ -36,7 +36,7 @@ class SignUpTwakeWorkplaceInteractor {
|
||||
await Future.wait([
|
||||
_credentialRepository.saveBaseUrl(baseUri),
|
||||
_authenticationOIDCRepository.persistTokenOIDC(tokenOIDC),
|
||||
_authenticationOIDCRepository.persistAuthorityOidc(oidcConfiguration.authority),
|
||||
_authenticationOIDCRepository.persistOidcConfiguration(oidcConfiguration),
|
||||
]);
|
||||
|
||||
await _accountRepository.setCurrentAccount(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tmail_ui_user/features/base/base_bindings.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
|
||||
@@ -38,9 +37,6 @@ class TwakeWelcomeBindings extends BaseBindings {
|
||||
Get.find<AccountRepository>(),
|
||||
Get.find<CredentialRepository>(),
|
||||
));
|
||||
if (PlatformInfo.isMobile) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+25
-11
@@ -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/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/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -61,16 +62,21 @@ class TwakeWelcomeController extends ReloadableController {
|
||||
|
||||
void _handlePendingDeepLinkDataStream(DeepLinkData? deepLinkData) {
|
||||
log('TwakeWelcomeController::_handlePendingDeepLinkDataStream:DeepLinkData = $deepLinkData');
|
||||
if (deepLinkData == null) return;
|
||||
|
||||
if (currentContext != null) {
|
||||
SmartDialog.showLoading(msg: AppLocalizations.of(currentContext!).loadingPleaseWait);
|
||||
}
|
||||
|
||||
_deepLinksManager?.handleDeepLinksWhenAppOnForegroundNotSignedIn(
|
||||
_deepLinksManager?.handleDeepLinksWhenAppRunning(
|
||||
deepLinkData: deepLinkData,
|
||||
onSuccessCallback: _handleAutoSignInViaDeepLinkSuccess,
|
||||
onFailureCallback: SmartDialog.dismiss,
|
||||
onSuccessCallback: (deepLinkData) {
|
||||
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(
|
||||
authority: AppConfig.saasRegistrationUrl,
|
||||
clientId: OIDCConstant.clientId,
|
||||
scopes: AppConfig.oidcScopes
|
||||
scopes: AppConfig.oidcScopes,
|
||||
isTWP: true,
|
||||
)
|
||||
));
|
||||
}
|
||||
@@ -127,7 +134,8 @@ class TwakeWelcomeController extends ReloadableController {
|
||||
oidcConfiguration: OIDCConfiguration(
|
||||
authority: AppConfig.saasRegistrationUrl,
|
||||
clientId: OIDCConstant.clientId,
|
||||
scopes: AppConfig.oidcScopes
|
||||
scopes: AppConfig.oidcScopes,
|
||||
isTWP: true,
|
||||
)
|
||||
));
|
||||
}
|
||||
@@ -178,6 +186,12 @@ class TwakeWelcomeController extends ReloadableController {
|
||||
toastManager.showMessageFailure(failure);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleUrgentExceptionOnMobile({Failure? failure, Exception? exception}) {
|
||||
SmartDialog.dismiss();
|
||||
super.handleUrgentExceptionOnMobile(failure: failure, exception: exception);
|
||||
}
|
||||
|
||||
void _synchronizeTokenAndGetSession({
|
||||
required Uri baseUri,
|
||||
required TokenOIDC tokenOIDC,
|
||||
|
||||
@@ -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": {
|
||||
"type": "text",
|
||||
@@ -4118,6 +4118,24 @@
|
||||
"placeholders_order": [],
|
||||
"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": {
|
||||
"type": "text",
|
||||
|
||||
@@ -15,6 +15,6 @@ class DeepLinkBindings extends Bindings {
|
||||
Get.find<AccountRepository>(),
|
||||
Get.find<CredentialRepository>(),
|
||||
));
|
||||
Get.put(DeepLinksManager(Get.find<AutoSignInViaDeepLinkInteractor>()));
|
||||
Get.put(DeepLinksManager());
|
||||
}
|
||||
}
|
||||
@@ -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/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/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/recent_login_url_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(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(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);
|
||||
@@ -1,45 +1,11 @@
|
||||
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 {
|
||||
final String action;
|
||||
final String? accessToken;
|
||||
final String? refreshToken;
|
||||
final String? idToken;
|
||||
final int? expiresIn;
|
||||
final String? username;
|
||||
final DeepLinkActionType actionType;
|
||||
|
||||
DeepLinkData({
|
||||
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,
|
||||
);
|
||||
}
|
||||
DeepLinkData({required this.actionType});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
action,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
expiresIn,
|
||||
username,
|
||||
];
|
||||
List<Object?> get props => [actionType];
|
||||
}
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:core/presentation/extensions/color_extension.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:model/oidc/oidc_configuration.dart';
|
||||
import 'package:rxdart/subjects.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
|
||||
import 'package:tmail_ui_user/features/home/domain/usecases/auto_sign_in_via_deep_link_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_link_action_define.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_link_action_type.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_link_callback_action_define.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_handler_mixin.dart';
|
||||
|
||||
class DeepLinksManager with MessageDialogActionMixin {
|
||||
|
||||
final AutoSignInViaDeepLinkInteractor _autoSignInViaDeepLinkInteractor;
|
||||
class DeepLinksManager with MessageDialogActionMixin, OpenAppDeepLinkHandlerMixin {
|
||||
|
||||
BehaviorSubject<DeepLinkData?> _pendingDeepLinkData = BehaviorSubject.seeded(null);
|
||||
|
||||
@@ -26,8 +17,6 @@ class DeepLinksManager with MessageDialogActionMixin {
|
||||
|
||||
StreamSubscription<Uri>? _deepLinkStreamSubscription;
|
||||
|
||||
DeepLinksManager(this._autoSignInViaDeepLinkInteractor);
|
||||
|
||||
Future<DeepLinkData?> getDeepLinkData() async {
|
||||
final uriLink = await AppLinks().getInitialLink();
|
||||
log('DeepLinksManager::getDeepLinkData:uriLink = $uriLink');
|
||||
@@ -63,194 +52,47 @@ class DeepLinksManager with MessageDialogActionMixin {
|
||||
|
||||
DeepLinkData? parseDeepLink(String url) {
|
||||
try {
|
||||
final updatedUrl = url.replaceFirst(
|
||||
OIDCConstant.twakeWorkplaceUrlScheme,
|
||||
'https',
|
||||
);
|
||||
final uri = Uri.parse(updatedUrl);
|
||||
final decodedUrl = Uri.decodeFull(url);
|
||||
final uri = Uri.parse(decodedUrl);
|
||||
log('DeepLinksManager::parseDeepLink:uri = $uri');
|
||||
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
|
||||
? int.tryParse(expiresInStr)
|
||||
: null;
|
||||
|
||||
return DeepLinkData(
|
||||
action: action,
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
idToken: idToken,
|
||||
expiresIn: expiresIn,
|
||||
username: username,
|
||||
);
|
||||
if (action == DeepLinkActionType.openApp.name.toLowerCase()) {
|
||||
return parseOpenAppDeepLink(uri);
|
||||
} else {
|
||||
return DeepLinkData(actionType: DeepLinkActionType.unknown);
|
||||
}
|
||||
} catch (e) {
|
||||
logError('DeepLinksManager::parseDeepLink:Exception = $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleDeepLinksWhenAppOnForegroundNotSignedIn({
|
||||
required DeepLinkData deepLinkData,
|
||||
required OnDeepLinkSuccessCallback onSuccessCallback,
|
||||
OnDeepLinkFailureCallback? onFailureCallback,
|
||||
}) async {
|
||||
log('DeepLinksManager::handleDeepLinksWhenAppOnForegroundNotSignedIn:DeepLinkData = $deepLinkData');
|
||||
if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) {
|
||||
_handleOpenApp(
|
||||
deepLinkData: deepLinkData,
|
||||
onFailureCallback: onFailureCallback,
|
||||
onSuccessCallback: onSuccessCallback,
|
||||
);
|
||||
} else {
|
||||
onFailureCallback?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleDeepLinksWhenAppTerminatedNotSignedIn({
|
||||
Future<void> handleDeepLinksWhenAppTerminated({
|
||||
required OnDeepLinkSuccessCallback onSuccessCallback,
|
||||
required OnDeepLinkFailureCallback onFailureCallback,
|
||||
}) async {
|
||||
final deepLinkData = await getDeepLinkData();
|
||||
log('DeepLinksManager::handleDeepLinksWhenAppTerminatedNotSignedIn:DeepLinkData = $deepLinkData');
|
||||
|
||||
if (deepLinkData == null) {
|
||||
onFailureCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) {
|
||||
_handleOpenApp(
|
||||
deepLinkData: deepLinkData,
|
||||
onFailureCallback: onFailureCallback,
|
||||
onSuccessCallback: onSuccessCallback,
|
||||
);
|
||||
} else {
|
||||
onFailureCallback();
|
||||
}
|
||||
onSuccessCallback(deepLinkData);
|
||||
}
|
||||
|
||||
Future<void> handleDeepLinksWhenAppTerminatedSignedIn({
|
||||
required String? username,
|
||||
required OnDeepLinkConfirmLogoutCallback onConfirmCallback,
|
||||
required OnDeepLinkFailureCallback onFailureCallback,
|
||||
Future<void> handleDeepLinksWhenAppRunning({
|
||||
required DeepLinkData? deepLinkData,
|
||||
required OnDeepLinkSuccessCallback onSuccessCallback,
|
||||
OnDeepLinkFailureCallback? onFailureCallback,
|
||||
}) async {
|
||||
final deepLinkData = await getDeepLinkData();
|
||||
log('DeepLinksManager::handleDeepLinksWhenAppTerminatedSignedIn:DeepLinkData = $deepLinkData');
|
||||
if (deepLinkData == null) {
|
||||
onFailureCallback();
|
||||
onFailureCallback?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deepLinkData.action.toLowerCase() == AppConfig.openAppHostDeepLink.toLowerCase()) {
|
||||
if (deepLinkData.username?.isNotEmpty != true || username?.isNotEmpty != true) {
|
||||
onFailureCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deepLinkData.username == username || currentContext == null) {
|
||||
onFailureCallback();
|
||||
} else {
|
||||
_showConfirmDialogSwitchAccount(
|
||||
context: currentContext!,
|
||||
username: username!,
|
||||
onConfirmAction: () => onConfirmCallback(deepLinkData),
|
||||
onCancelAction: onFailureCallback,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onFailureCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleOpenApp({
|
||||
required DeepLinkData deepLinkData,
|
||||
required OnDeepLinkSuccessCallback onSuccessCallback,
|
||||
OnDeepLinkFailureCallback? onFailureCallback,
|
||||
}) {
|
||||
autoSignInViaDeepLink(
|
||||
deepLinkData: deepLinkData,
|
||||
onFailureCallback: onFailureCallback,
|
||||
onSuccessCallback: onSuccessCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<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,
|
||||
);
|
||||
onSuccessCallback(deepLinkData);
|
||||
}
|
||||
|
||||
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 {
|
||||
return Intl.message(
|
||||
'Get help or report a bug',
|
||||
|
||||
@@ -19,7 +19,6 @@ class AppConfig {
|
||||
static const String linagoraPrivacyUrl = 'https://www.linagora.com/en/legal/privacy';
|
||||
static const String saasRegistrationUrl = 'https://sign-up.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 domainRedirectUrl => dotenv.get('DOMAIN_REDIRECT_URL', fallback: '');
|
||||
|
||||
@@ -82,7 +82,7 @@ class IOSSharingManager {
|
||||
userName: personalAccount.userName!
|
||||
);
|
||||
|
||||
final tokenRecords = await _getTokenEndpointAndScopes();
|
||||
final tokenRecords = await _getTokenEndpointScopesAndIsTWP();
|
||||
|
||||
final mailboxIdsBlockNotification = await _getMailboxIdsBlockNotification(
|
||||
accountId: personalAccount.accountId!,
|
||||
@@ -99,7 +99,9 @@ class IOSSharingManager {
|
||||
basicAuth: credentialInfo,
|
||||
tokenEndpoint: tokenRecords?.tokenEndpoint,
|
||||
oidcScopes: tokenRecords?.scopes,
|
||||
mailboxIdsBlockNotification: mailboxIdsBlockNotification);
|
||||
mailboxIdsBlockNotification: mailboxIdsBlockNotification,
|
||||
isTWP: tokenRecords?.isTWP ?? false,
|
||||
);
|
||||
|
||||
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 {
|
||||
final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration();
|
||||
final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig);
|
||||
return (
|
||||
tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint,
|
||||
scopes: oidcConfig.scopes
|
||||
scopes: oidcConfig.scopes,
|
||||
isTWP: oidcConfig.isTWP,
|
||||
);
|
||||
} catch (e) {
|
||||
logError('IOSSharingManager::_getTokenEndpointAndScopes:Exception: $e');
|
||||
|
||||
@@ -2,26 +2,25 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class OIDCConfiguration with EquatableMixin {
|
||||
final redirectOidcMobile = 'teammail.mobile://oauthredirect';
|
||||
final wellKnownOpenId = '.well-known/openid-configuration';
|
||||
final loginRedirectOidcWeb = 'login-callback.html';
|
||||
final logoutRedirectOidcWeb = 'logout-callback.html';
|
||||
static const String _wellKnownOpenId = '.well-known/openid-configuration';
|
||||
|
||||
final String authority;
|
||||
final String clientId;
|
||||
final List<String> scopes;
|
||||
final bool isTWP;
|
||||
|
||||
OIDCConfiguration({
|
||||
required this.authority,
|
||||
required this.clientId,
|
||||
required this.scopes
|
||||
required this.scopes,
|
||||
this.isTWP = false,
|
||||
});
|
||||
|
||||
String get discoveryUrl {
|
||||
if (authority.endsWith('/')) {
|
||||
return authority + wellKnownOpenId;
|
||||
return authority + _wellKnownOpenId;
|
||||
} else {
|
||||
return '$authority/$wellKnownOpenId';
|
||||
return '$authority/$_wellKnownOpenId';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +28,7 @@ class OIDCConfiguration with EquatableMixin {
|
||||
List<Object?> get props => [
|
||||
authority,
|
||||
clientId,
|
||||
scopes
|
||||
scopes,
|
||||
isTWP,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ void main() {
|
||||
apiUrl: 'https://jmap.domain.com/oidc/jmap',
|
||||
tokenEndpoint: 'https://jmap.domain.com/oidc/jmap',
|
||||
oidcScopes: ['email'],
|
||||
isTWP: true,
|
||||
emailState: 'ae08b34da40b48f30ec0b94',
|
||||
tokenOIDC: TokenOIDC(
|
||||
'ae08b34da40b48f30ec0b94',
|
||||
@@ -88,6 +89,7 @@ void main() {
|
||||
expect(keychainSession?.emailState, equals('ae08b34da40b48f30ec0b94'));
|
||||
expect(keychainSession?.tokenEndpoint, equals('https://jmap.domain.com/oidc/jmap',));
|
||||
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=='
|
||||
'®istrationUrl=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'
|
||||
'®istrationUrl=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'
|
||||
'®istrationUrl=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'
|
||||
'®istrationUrl=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=='
|
||||
'®istrationUrl=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=='
|
||||
'®istrationUrl=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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user