From b2aa472eb38bd92b8d8b76a72731e09cd787a30a Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 21 May 2024 01:04:23 +0700 Subject: [PATCH 01/11] TF-2871 Fix missing `tokenEndpoint` & `scopes` passing to keychain --- .../bindings/network/network_bindings.dart | 3 ++ lib/main/utils/ios_sharing_manager.dart | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 208c47854d..d5823d2bed 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/email/data/network/mdn_api.dart'; import 'package:tmail_ui_user/features/home/data/network/session_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; @@ -77,6 +78,8 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), )); } diff --git a/lib/main/utils/ios_sharing_manager.dart b/lib/main/utils/ios_sharing_manager.dart index bb261bea1f..5c40f21b24 100644 --- a/lib/main/utils/ios_sharing_manager.dart +++ b/lib/main/utils/ios_sharing_manager.dart @@ -8,7 +8,9 @@ import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/personal_account.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; import 'package:tmail_ui_user/features/push_notification/data/extensions/keychain_sharing_session_extension.dart'; @@ -20,12 +22,16 @@ class IOSSharingManager { final StateCacheManager _stateCacheManager; final TokenOidcCacheManager _tokenOidcCacheManager; final AuthenticationInfoCacheManager _authenticationInfoCacheManager; + final OidcConfigurationCacheManager _oidcConfigurationCacheManager; + final OIDCHttpClient _oidcHttpClient; IOSSharingManager( this._keychainSharingManager, this._stateCacheManager, this._tokenOidcCacheManager, - this._authenticationInfoCacheManager + this._authenticationInfoCacheManager, + this._oidcConfigurationCacheManager, + this._oidcHttpClient, ); bool _validateToSaveKeychain(PersonalAccount personalAccount) { @@ -69,6 +75,8 @@ class IOSSharingManager { userName: personalAccount.userName! ); + final tokenRecords = await _getTokenEndpointAndScopes(); + final keychainSharingSession = KeychainSharingSession( accountId: personalAccount.accountId!, userName: personalAccount.userName!, @@ -77,7 +85,9 @@ class IOSSharingManager { emailState: emailState, emailDeliveryState: emailDeliveryState, tokenOIDC: authenticationInfo.value1, - basicAuth: authenticationInfo.value2 + basicAuth: authenticationInfo.value2, + tokenEndpoint: tokenRecords?.tokenEndpoint, + oidcScopes: tokenRecords?.scopes, ); log('IOSSharingManager::_saveKeyChainSharingSession: $keychainSharingSession'); await _keychainSharingManager.save(keychainSharingSession); @@ -153,6 +163,21 @@ class IOSSharingManager { } } + Future<({String? tokenEndpoint, List? scopes})?> _getTokenEndpointAndScopes() async { + try { + final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration(); + final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig); + log('IOSSharingManager::_getTokenEndpointAndScopes:oidcDiscoveryResponse = $oidcDiscoveryResponse | oidcConfig = $oidcConfig'); + return ( + tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint, + scopes: oidcConfig.scopes + ); + } catch (e) { + logError('IOSSharingManager::_getTokenEndpointAndScopes:Exception: $e'); + return null; + } + } + Future updateEmailStateInKeyChain(AccountId accountId, String newEmailState) async { final keychainSharingStored = await getKeychainSharingSession(accountId); log('IOSSharingManager::updateEmailStateInKeyChain:keychainSharingStored: $keychainSharingStored | newEmailState: $newEmailState'); From bb1205fad62fd3240c870e56d9186df247e09c2a Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 21 May 2024 01:14:31 +0700 Subject: [PATCH 02/11] TF-2871 Fix incorrect ISO8601 date string to date conversion --- ios/Runner.xcodeproj/project.pbxproj | 191 ++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 11 + .../xcschemes/TwakeMailNSE.xcscheme | 13 ++ .../Extensions/StringExtensions.swift | 10 +- ios/TwakeMailTests/DateConversionTests.swift | 25 +++ 5 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 ios/TwakeMailTests/DateConversionTests.swift diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 0da45ca480..fe8d93bb08 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ F53D1E862B2E401B00051FD0 /* JmapRequestGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D1E852B2E401B00051FD0 /* JmapRequestGenerator.swift */; }; F53D1E882B2E4B3B00051FD0 /* AuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */; }; F53D1E8A2B2E4BB700051FD0 /* TokenOidc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D1E892B2E4BB700051FD0 /* TokenOidc.swift */; }; + F55114CF2BFBACFA00E41E93 /* DateConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F55114CE2BFBACFA00E41E93 /* DateConversionTests.swift */; }; F5550D292B4E90A7003DD2AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F5550D2C2B4E90A7003DD2AE /* Localizable.strings */; }; F5550D2A2B4E90A7003DD2AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F5550D2C2B4E90A7003DD2AE /* Localizable.strings */; }; F5630C762B2CDFDA003CC0FD /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5630C752B2CDFDA003CC0FD /* KeychainController.swift */; }; @@ -67,6 +68,13 @@ remoteGlobalIDString = F52F992C27FD6EB900346091; remoteInfo = TeamMailShareExtension; }; + F55114D02BFBACFA00E41E93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; F5D4EA052B2ABF090090DDFC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -136,6 +144,8 @@ F53D1E852B2E401B00051FD0 /* JmapRequestGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JmapRequestGenerator.swift; sourceTree = ""; }; F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationType.swift; sourceTree = ""; }; F53D1E892B2E4BB700051FD0 /* TokenOidc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOidc.swift; sourceTree = ""; }; + F55114CC2BFBACFA00E41E93 /* TwakeMailTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TwakeMailTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F55114CE2BFBACFA00E41E93 /* DateConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConversionTests.swift; sourceTree = ""; }; F5550D2B2B4E90A7003DD2AE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; F5550D2D2B4E90C6003DD2AE /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; F5550D2E2B4E90D5003DD2AE /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -181,6 +191,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F55114C92BFBACFA00E41E93 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F5D4E9FD2B2ABF090090DDFC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -220,6 +237,7 @@ 97C146F01CF9000F007C117D /* Runner */, F52F992E27FD6EB900346091 /* TeamMailShareExtension */, F5D4EA012B2ABF090090DDFC /* TwakeMailNSE */, + F55114CD2BFBACFA00E41E93 /* TwakeMailTests */, 97C146EF1CF9000F007C117D /* Products */, F62B48DC0DFBD6D5D355E5FE /* Pods */, 601CDD7C97AAAE9C3FB007DB /* Frameworks */, @@ -232,6 +250,7 @@ 97C146EE1CF9000F007C117D /* Runner.app */, F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */, F5D4EA002B2ABF090090DDFC /* TwakeMailNSE.appex */, + F55114CC2BFBACFA00E41E93 /* TwakeMailTests.xctest */, ); name = Products; sourceTree = ""; @@ -330,6 +349,14 @@ path = Utils; sourceTree = ""; }; + F55114CD2BFBACFA00E41E93 /* TwakeMailTests */ = { + isa = PBXGroup; + children = ( + F55114CE2BFBACFA00E41E93 /* DateConversionTests.swift */, + ); + path = TwakeMailTests; + sourceTree = ""; + }; F5630C742B2CDFC5003CC0FD /* Keychain */ = { isa = PBXGroup; children = ( @@ -487,6 +514,24 @@ productReference = F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + F55114CB2BFBACFA00E41E93 /* TwakeMailTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F55114D52BFBACFA00E41E93 /* Build configuration list for PBXNativeTarget "TwakeMailTests" */; + buildPhases = ( + F55114C82BFBACFA00E41E93 /* Sources */, + F55114C92BFBACFA00E41E93 /* Frameworks */, + F55114CA2BFBACFA00E41E93 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F55114D12BFBACFA00E41E93 /* PBXTargetDependency */, + ); + name = TwakeMailTests; + productName = TwakeMailTests; + productReference = F55114CC2BFBACFA00E41E93 /* TwakeMailTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; F5D4E9FF2B2ABF090090DDFC /* TwakeMailNSE */ = { isa = PBXNativeTarget; buildConfigurationList = F5D4EA0B2B2ABF090090DDFC /* Build configuration list for PBXNativeTarget "TwakeMailNSE" */; @@ -525,6 +570,10 @@ F52F992C27FD6EB900346091 = { CreatedOnToolsVersion = 13.2.1; }; + F55114CB2BFBACFA00E41E93 = { + CreatedOnToolsVersion = 15.1; + TestTargetID = 97C146ED1CF9000F007C117D; + }; F5D4E9FF2B2ABF090090DDFC = { CreatedOnToolsVersion = 15.1; }; @@ -553,6 +602,7 @@ 97C146ED1CF9000F007C117D /* Runner */, F52F992C27FD6EB900346091 /* TeamMailShareExtension */, F5D4E9FF2B2ABF090090DDFC /* TwakeMailNSE */, + F55114CB2BFBACFA00E41E93 /* TwakeMailTests */, ); }; /* End PBXProject section */ @@ -578,6 +628,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F55114CA2BFBACFA00E41E93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F5D4E9FE2B2ABF090090DDFC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -684,6 +741,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F55114C82BFBACFA00E41E93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F55114CF2BFBACFA00E41E93 /* DateConversionTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F5D4E9FC2B2ABF090090DDFC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -728,6 +793,11 @@ target = F52F992C27FD6EB900346091 /* TeamMailShareExtension */; targetProxy = F52F993527FD6EB900346091 /* PBXContainerItemProxy */; }; + F55114D12BFBACFA00E41E93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F55114D02BFBACFA00E41E93 /* PBXContainerItemProxy */; + }; F5D4EA062B2ABF090090DDFC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F5D4E9FF2B2ABF090090DDFC /* TwakeMailNSE */; @@ -1136,6 +1206,117 @@ }; name = Profile; }; + F55114D22BFBACFA00E41E93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=macosx*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TwakeMailTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + F55114D32BFBACFA00E41E93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=macosx*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TwakeMailTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + F55114D42BFBACFA00E41E93 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=macosx*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TwakeMailTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; F5D4EA082B2ABF090090DDFC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1298,6 +1479,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F55114D52BFBACFA00E41E93 /* Build configuration list for PBXNativeTarget "TwakeMailTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F55114D22BFBACFA00E41E93 /* Debug */, + F55114D32BFBACFA00E41E93 /* Release */, + F55114D42BFBACFA00E41E93 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F5D4EA0B2B2ABF090090DDFC /* Build configuration list for PBXNativeTarget "TwakeMailNSE" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db27..1726f018ea 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,17 @@ + + + + + + + + + + Date? { - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - if let date = dateFormatter.date(from: self) { - return date - } - return nil + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.date(from: self) } func convertUTCDateToLocalDate() -> Date? { diff --git a/ios/TwakeMailTests/DateConversionTests.swift b/ios/TwakeMailTests/DateConversionTests.swift new file mode 100644 index 0000000000..d51f0f8ced --- /dev/null +++ b/ios/TwakeMailTests/DateConversionTests.swift @@ -0,0 +1,25 @@ +import XCTest + +@testable import Runner + +class DateConversionTests: XCTestCase { + + func testConvertValidISO8601StringToDate() { + let validDateString = "2024-05-20T22:54:57.958" + + let date = validDateString.convertISO8601StringToDate() + XCTAssertNotNil(date, "Date should not be nil") + + let calendar = Calendar.current + let expectedComponents = DateComponents(year: 2024, month: 5, day: 20, hour: 22, minute: 54, second: 57, nanosecond: 958000000) + let expectedDate = calendar.date(from: expectedComponents) + XCTAssertEqual(date, expectedDate, "Converted date does not match expected date") + } + + func testInvalidISO8601String() { + let invalidDateString = "Invalid Date String" + + let date = invalidDateString.convertISO8601StringToDate() + XCTAssertNil(date, "The conversion should return nil for an invalid date string.") + } +} From e89b0a2dae0fc02f5a2d2105df6e7cdeec9d7fc0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 21 May 2024 02:11:13 +0700 Subject: [PATCH 03/11] TF-2871 Fix notification loss error when sending multiple notifications simultaneously. --- ios/TwakeMailNSE/NotificationService.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/TwakeMailNSE/NotificationService.swift b/ios/TwakeMailNSE/NotificationService.swift index d4e48eb96c..7f4f360d1d 100644 --- a/ios/TwakeMailNSE/NotificationService.swift +++ b/ios/TwakeMailNSE/NotificationService.swift @@ -94,13 +94,14 @@ class NotificationService: UNNotificationServiceExtension { badgeCount: emails.count, userInfo: [JmapConstants.EMAIL_ID : email.id]) return self.notify() + } else { + self.showNewNotification(title: email.getSenderName(), + subtitle: email.subject, + body: email.preview, + badgeCount: emails.count, + notificationId: email.id, + userInfo: [JmapConstants.EMAIL_ID : email.id]) } - self.showNewNotification(title: email.getSenderName(), - subtitle: email.subject, - body: email.preview, - badgeCount: emails.count, - notificationId: email.id, - userInfo: [JmapConstants.EMAIL_ID : email.id]) } } else { self.showModifiedNotification(title: emails.first!.getSenderName(), @@ -157,8 +158,7 @@ class NotificationService: UNNotificationServiceExtension { content.userInfo = userInfo // Create a notification trigger - let triggerDateTime = Calendar.current.dateComponents([.hour, .minute, .second], from: Date()) - let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDateTime, repeats: false) + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false) // Create a notification request let request = UNNotificationRequest(identifier: notificationId, content: content, trigger: trigger) From 8df1b9373a53a349bd93b4c181a9772335df6a52 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 4 Jun 2024 16:57:05 +0700 Subject: [PATCH 04/11] TF-2871 Fix validate expire time in swift code --- ios/Runner.xcodeproj/project.pbxproj | 32 +++++++++++++ ios/TwakeCore/Extensions/DateExtensions.swift | 7 +++ .../Extensions/IntegerExtensions.swift | 11 +++++ .../Extensions/StringExtensions.swift | 4 +- .../AuthenticationInterceptor.swift | 2 +- .../Network/Model/AuthenticationSSO.swift | 18 ++----- ios/TwakeCore/Utils/CoreUtils.swift | 17 +++++++ .../AuthenticationSSOTests.swift | 47 +++++++++++++++++++ ios/TwakeMailTests/DateConversionTests.swift | 20 ++++++++ 9 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 ios/TwakeCore/Extensions/IntegerExtensions.swift create mode 100644 ios/TwakeCore/Utils/CoreUtils.swift create mode 100644 ios/TwakeMailTests/AuthenticationSSOTests.swift diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fe8d93bb08..bf1d15d48c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,15 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; CDFECA8C54311B749F044831 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */; }; + F522E87F2C0EE23400DDA35B /* AuthenticationSSOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */; }; + F522E8812C0EE3F200DDA35B /* AuthenticationSSO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */; }; + F522E8822C0EE48700DDA35B /* AuthenticationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53D1E872B2E4B3B00051FD0 /* AuthenticationType.swift */; }; + F522E8832C0EE48C00DDA35B /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D87F2B3876DE0009BB8A /* Authentication.swift */; }; + F522E8862C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; + F522E8872C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; + F522E8882C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; + F522E88A2C0F117900DDA35B /* IntegerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8892C0F117900DDA35B /* IntegerExtensions.swift */; }; + F522E88B2C0F117900DDA35B /* IntegerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8892C0F117900DDA35B /* IntegerExtensions.swift */; }; F52F993027FD6EB900346091 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52F992F27FD6EB900346091 /* ShareViewController.swift */; }; F52F993327FD6EB900346091 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F52F993127FD6EB900346091 /* MainInterface.storyboard */; }; F52F993727FD6EB900346091 /* TeamMailShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -128,6 +137,9 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSOTests.swift; sourceTree = ""; }; + F522E8852C0EE8B600DDA35B /* CoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUtils.swift; sourceTree = ""; }; + F522E8892C0F117900DDA35B /* IntegerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensions.swift; sourceTree = ""; }; F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TeamMailShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F52F992F27FD6EB900346091 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; F52F993227FD6EB900346091 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; @@ -273,6 +285,14 @@ path = Runner; sourceTree = ""; }; + F522E8842C0EE89D00DDA35B /* Utils */ = { + isa = PBXGroup; + children = ( + F522E8852C0EE8B600DDA35B /* CoreUtils.swift */, + ); + path = Utils; + sourceTree = ""; + }; F52F992E27FD6EB900346091 /* TeamMailShareExtension */ = { isa = PBXGroup; children = ( @@ -353,6 +373,7 @@ isa = PBXGroup; children = ( F55114CE2BFBACFA00E41E93 /* DateConversionTests.swift */, + F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */, ); path = TwakeMailTests; sourceTree = ""; @@ -425,6 +446,7 @@ children = ( F5E7D8782B38763B0009BB8A /* DateExtensions.swift */, F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */, + F522E8892C0F117900DDA35B /* IntegerExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -448,6 +470,7 @@ F5EFC0802B32965100829056 /* TwakeCore */ = { isa = PBXGroup; children = ( + F522E8842C0EE89D00DDA35B /* Utils */, F5E7D8772B38761B0009BB8A /* Extensions */, F5BBBF4F2B2EEC1B007930E1 /* Exceptions */, F53D1E642B2DE84E00051FD0 /* Network */, @@ -725,10 +748,15 @@ files = ( F5E7D87C2B38764F0009BB8A /* StringExtensions.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + F522E88A2C0F117900DDA35B /* IntegerExtensions.swift in Sources */, F5E7D8792B38763B0009BB8A /* DateExtensions.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + F522E8862C0EE8B600DDA35B /* CoreUtils.swift in Sources */, + F522E8812C0EE3F200DDA35B /* AuthenticationSSO.swift in Sources */, + F522E8822C0EE48700DDA35B /* AuthenticationType.swift in Sources */, F5EFC07D2B328B9F00829056 /* TwakeLogger.swift in Sources */, F5E7D8742B3578F90009BB8A /* JmapConstants.swift in Sources */, + F522E8832C0EE48C00DDA35B /* Authentication.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -746,6 +774,8 @@ buildActionMask = 2147483647; files = ( F55114CF2BFBACFA00E41E93 /* DateConversionTests.swift in Sources */, + F522E8882C0EE8B600DDA35B /* CoreUtils.swift in Sources */, + F522E87F2C0EE23400DDA35B /* AuthenticationSSOTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -757,6 +787,8 @@ F53D1E7F2B2E3C2600051FD0 /* JmapConstants.swift in Sources */, F5E7D8822B3876F60009BB8A /* AuthenticationCredential.swift in Sources */, F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */, + F522E88B2C0F117900DDA35B /* IntegerExtensions.swift in Sources */, + F522E8872C0EE8B600DDA35B /* CoreUtils.swift in Sources */, F53D1E8A2B2E4BB700051FD0 /* TokenOidc.swift in Sources */, F53D1E862B2E401B00051FD0 /* JmapRequestGenerator.swift in Sources */, F5E7D87A2B38763B0009BB8A /* DateExtensions.swift in Sources */, diff --git a/ios/TwakeCore/Extensions/DateExtensions.swift b/ios/TwakeCore/Extensions/DateExtensions.swift index b610c1bcda..9ee0917843 100644 --- a/ios/TwakeCore/Extensions/DateExtensions.swift +++ b/ios/TwakeCore/Extensions/DateExtensions.swift @@ -4,4 +4,11 @@ extension Date { func isBefore(_ otherDate: Date) -> Bool { return self < otherDate } + + func convertDateToISO8601String() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = CoreUtils.ISO8601_DATE_FORMAT + dateFormatter.locale = Locale(identifier: CoreUtils.EN_US_POSIX_LOCALE) + return dateFormatter.string(from: self) + } } diff --git a/ios/TwakeCore/Extensions/IntegerExtensions.swift b/ios/TwakeCore/Extensions/IntegerExtensions.swift new file mode 100644 index 0000000000..1efc2d4648 --- /dev/null +++ b/ios/TwakeCore/Extensions/IntegerExtensions.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Int { + func convertMillisecondsToDate() -> Date { + return Date(timeIntervalSince1970: TimeInterval(self) / 1000) + } + + func convertMillisecondsToISO8601String() -> String { + return convertMillisecondsToDate().convertDateToISO8601String() + } +} diff --git a/ios/TwakeCore/Extensions/StringExtensions.swift b/ios/TwakeCore/Extensions/StringExtensions.swift index 73f9607180..4f1fff70f4 100644 --- a/ios/TwakeCore/Extensions/StringExtensions.swift +++ b/ios/TwakeCore/Extensions/StringExtensions.swift @@ -3,8 +3,8 @@ import Foundation extension String { func convertISO8601StringToDate() -> Date? { let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" - dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = CoreUtils.ISO8601_DATE_FORMAT + dateFormatter.locale = Locale(identifier: CoreUtils.EN_US_POSIX_LOCALE) return dateFormatter.date(from: self) } diff --git a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift index 5e17bfef45..c6cebaeae9 100644 --- a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift +++ b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift @@ -69,7 +69,7 @@ class AuthenticationInterceptor: RequestInterceptor { private func validateToRefreshToken(response: HTTPURLResponse, authenticationSSO: AuthenticationSSO) -> Bool { return response.statusCode == 401 && !authenticationSSO.refreshToken.isEmpty && - authenticationSSO.isExpiredTime() + authenticationSSO.isExpiredTime(currentDate: CoreUtils.shared.getCurrentDate()) } // MARK: - Handle refresh token to get new token diff --git a/ios/TwakeCore/Network/Model/AuthenticationSSO.swift b/ios/TwakeCore/Network/Model/AuthenticationSSO.swift index d965d01888..a41794043f 100644 --- a/ios/TwakeCore/Network/Model/AuthenticationSSO.swift +++ b/ios/TwakeCore/Network/Model/AuthenticationSSO.swift @@ -18,23 +18,15 @@ struct AuthenticationSSO: Authentication { return "Bearer \(accessToken)" } - func isExpiredTime() -> Bool { + func isExpiredTime(currentDate: Date) -> Bool { guard let expireTime else { return false } - - if #available(iOSApplicationExtension 15, *) { - guard let expireDate = expireTime.convertISO8601StringToDate(), - expireDate.isBefore(Date.now) else { - return true - } + + if let expireDate = expireTime.convertISO8601StringToDate() { + return expireDate.isBefore(currentDate) } else { - guard let expireDate = expireTime.convertISO8601StringToDate(), - expireDate.isBefore(Date()) else { - return true - } + return false } - - return false } } diff --git a/ios/TwakeCore/Utils/CoreUtils.swift b/ios/TwakeCore/Utils/CoreUtils.swift new file mode 100644 index 0000000000..f0d1bde463 --- /dev/null +++ b/ios/TwakeCore/Utils/CoreUtils.swift @@ -0,0 +1,17 @@ +import Foundation + +class CoreUtils { + static let shared: CoreUtils = CoreUtils() + + static let ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS" + static let EN_US_POSIX_LOCALE = "en_US_POSIX" + + func getCurrentDate() -> Date { + if #available(iOS 15, *) { + return Date.now + } else { + return Date() + } + } +} + diff --git a/ios/TwakeMailTests/AuthenticationSSOTests.swift b/ios/TwakeMailTests/AuthenticationSSOTests.swift new file mode 100644 index 0000000000..1c0127cd7c --- /dev/null +++ b/ios/TwakeMailTests/AuthenticationSSOTests.swift @@ -0,0 +1,47 @@ +import XCTest + +@testable import Runner + +final class AuthenticationSSOTests: XCTestCase { + + func testIsExpiredTimeMethodWithValidExpireTime() { + let currentDate = CoreUtils.shared.getCurrentDate() + let expireDate = currentDate.addingTimeInterval(3600) + + let authenticationSSO = AuthenticationSSO( + type: AuthenticationType.oidc, + accessToken: "abcxyz", + refreshToken: "abcxyz", + expireTime: expireDate.convertDateToISO8601String() + ) + + XCTAssertFalse(authenticationSSO.isExpiredTime(currentDate: currentDate)) + } + + func testIsExpiredTimeMethodWithExpireTime() { + let currentDate = CoreUtils.shared.getCurrentDate() + let expireDate = currentDate.addingTimeInterval(-3600) + + let authenticationSSO = AuthenticationSSO( + type: AuthenticationType.oidc, + accessToken: "abcxyz", + refreshToken: "abcxyz", + expireTime: expireDate.convertDateToISO8601String() + ) + + XCTAssertTrue(authenticationSSO.isExpiredTime(currentDate: currentDate)) + } + + func testIsExpiredTimeMethodNilExpireTime() { + let currentDate = CoreUtils.shared.getCurrentDate() + + let authenticationSSO = AuthenticationSSO( + type: AuthenticationType.oidc, + accessToken: "abcxyz", + refreshToken: "abcxyz", + expireTime: nil + ) + + XCTAssertFalse(authenticationSSO.isExpiredTime(currentDate: currentDate)) + } +} diff --git a/ios/TwakeMailTests/DateConversionTests.swift b/ios/TwakeMailTests/DateConversionTests.swift index d51f0f8ced..51c4c58b55 100644 --- a/ios/TwakeMailTests/DateConversionTests.swift +++ b/ios/TwakeMailTests/DateConversionTests.swift @@ -22,4 +22,24 @@ class DateConversionTests: XCTestCase { let date = invalidDateString.convertISO8601StringToDate() XCTAssertNil(date, "The conversion should return nil for an invalid date string.") } + + func testConvertValidDateToISO8601String() { + let validDate = Calendar.current.date( + from: DateComponents( + year: 2024, + month: 5, + day: 20, + hour: 22, + minute: 54, + second: 57, + nanosecond: 958000000 + ) + ) + + + let expectedDateString = "2024-05-20T22:54:57.958" + let validDateString = validDate!.convertDateToISO8601String() + + XCTAssertEqual(validDateString, expectedDateString, "Converted date string does not match expected date string") + } } From e0cb43f09825986eeeb265e55416f06263376630 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 4 Jun 2024 16:58:37 +0700 Subject: [PATCH 05/11] TF-2871 Use `refreshToken` again when the new refreshToken is null --- .../Network/Interceptor/AuthenticationInterceptor.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift index c6cebaeae9..ac43f2a6ad 100644 --- a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift +++ b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift @@ -36,15 +36,16 @@ class AuthenticationInterceptor: RequestInterceptor { } handleRefreshToken(tokenRefreshManager: tokenRefreshManager) { tokenResponse in - guard let accessToken = tokenResponse.accessToken, - let refreshToken = tokenResponse.refreshToken else { + guard let accessToken = tokenResponse.accessToken else { return completion(.doNotRetryWithError(error)) } + let newRefreshToken = tokenResponse.refreshToken ?? authenticationSSO.refreshToken + self.authentication = AuthenticationSSO( type: AuthenticationType.oidc, accessToken: accessToken, - refreshToken: refreshToken, + refreshToken: newRefreshToken, expireTime: "\(tokenResponse.expiresTime ?? 0)" ) @@ -54,7 +55,7 @@ class AuthenticationInterceptor: RequestInterceptor { token: accessToken, tokenId: tokenResponse.tokenId, expiredTime: "\(tokenResponse.expiresTime ?? 0)", - refreshToken: refreshToken + refreshToken: newRefreshToken ) ) From 6087d60dc7329d8e3193164ae5aaebb8f50e641b Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 4 Jun 2024 17:00:44 +0700 Subject: [PATCH 06/11] TF-2871 Convert milliseconds to date for expire time --- .../Network/Interceptor/AuthenticationInterceptor.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift index ac43f2a6ad..4619113ec9 100644 --- a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift +++ b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift @@ -41,12 +41,15 @@ class AuthenticationInterceptor: RequestInterceptor { } let newRefreshToken = tokenResponse.refreshToken ?? authenticationSSO.refreshToken + let expireTime = tokenResponse.expiresTime != nil + ? tokenResponse.expiresTime!.convertMillisecondsToISO8601String() + : nil self.authentication = AuthenticationSSO( type: AuthenticationType.oidc, accessToken: accessToken, refreshToken: newRefreshToken, - expireTime: "\(tokenResponse.expiresTime ?? 0)" + expireTime: expireTime ) self.keychainController.updateTokenOidc( @@ -54,7 +57,7 @@ class AuthenticationInterceptor: RequestInterceptor { newTokenOidc: TokenOidc( token: accessToken, tokenId: tokenResponse.tokenId, - expiredTime: "\(tokenResponse.expiresTime ?? 0)", + expiredTime: expireTime, refreshToken: newRefreshToken ) ) From 872a0780358c1cfe823f2efad0c121149bbb4893 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 2 Jul 2024 14:31:17 +0700 Subject: [PATCH 07/11] TF-2871 Add `adr` for push notification click logic on iOS --- ...y-one-pdf-in-dart-side-in-only-web-app.md} | 2 +- ...-android-notification-permission-check.md} | 2 +- ...51-push-notification-click-logic-on-ios.md | 79 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) rename docs/adr/{0048-view-one-by-one-pdf-in-dart-side-in-only-web-app.md => 0049-view-one-by-one-pdf-in-dart-side-in-only-web-app.md} (94%) rename docs/adr/{0049-android-notification-permission-check.md => 0050-android-notification-permission-check.md} (94%) create mode 100644 docs/adr/0051-push-notification-click-logic-on-ios.md diff --git a/docs/adr/0048-view-one-by-one-pdf-in-dart-side-in-only-web-app.md b/docs/adr/0049-view-one-by-one-pdf-in-dart-side-in-only-web-app.md similarity index 94% rename from docs/adr/0048-view-one-by-one-pdf-in-dart-side-in-only-web-app.md rename to docs/adr/0049-view-one-by-one-pdf-in-dart-side-in-only-web-app.md index f1315ded26..ecaa7d2864 100644 --- a/docs/adr/0048-view-one-by-one-pdf-in-dart-side-in-only-web-app.md +++ b/docs/adr/0049-view-one-by-one-pdf-in-dart-side-in-only-web-app.md @@ -1,4 +1,4 @@ -# 48. View one-by-one PDF in Dart side in only Web app +# 49. View one-by-one PDF in Dart side in only Web app Date: 2024-05-17 diff --git a/docs/adr/0049-android-notification-permission-check.md b/docs/adr/0050-android-notification-permission-check.md similarity index 94% rename from docs/adr/0049-android-notification-permission-check.md rename to docs/adr/0050-android-notification-permission-check.md index dc7e475f43..1c22bf97c7 100644 --- a/docs/adr/0049-android-notification-permission-check.md +++ b/docs/adr/0050-android-notification-permission-check.md @@ -1,4 +1,4 @@ -# 48. Notification setting +# 50. Android notification permission check Date: 2024-06-07 diff --git a/docs/adr/0051-push-notification-click-logic-on-ios.md b/docs/adr/0051-push-notification-click-logic-on-ios.md new file mode 100644 index 0000000000..bba1395622 --- /dev/null +++ b/docs/adr/0051-push-notification-click-logic-on-ios.md @@ -0,0 +1,79 @@ +# 51. Push notification click logic on iOS + +Date: 2024-06-07 + +## Status + +Accepted + +## Context + +On iOS, we use `Notification Service Extension` [NSE](https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications) to modify email content first when displaying push notifications to users. +- In NSE we have implemented getting `new email list` based on `EmailDeliveryState` sent from FCM. +- NSE only modifies and displays 1 notification, so we will display the notification for the last email in the list via NSE (is called `RemoteNotification`). +For the remaining emails we will use [UNUserNotificationCenter](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter) to automatically display notifications (is called `LocalNotification`). + +## Decision + +Brief the logic flows when clicking notifications: + +1. Foreground/Background state + +- When clicking on notifications (both LocalNotification and RemoteNotification) the `didReceive` function of `UNUserNotificationCenter` in `AppDelegate` will be called. + +```swift + override func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) {} +``` + +`response.notification.request.content.userInfo` is the contents of the push payload notification. +We use `FlutterMethodChannel` to pass `UserInfo` from iOS native code to Flutter dart code + +```swift + self.notificationInteractionChannel?.invokeMethod("current_email_id_in_notification_click_on_foreground", arguments: response.notification.request.content.userInfo) +``` + +2. Terminated state + +- With `RemoteNotification`, we will get the push notification payload through `launchOptions?[.remoteNotification]` in the `didFinishLaunchingWithOptions` function of `AppDelegate` + +```swift +override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +) -> Bool {} +``` + +Save it in a `remoteNotificationPayload` variable in memory and use `FlutterMethodChannel` to retrieve it every time you open the app. + +```swift +self.notificationInteractionChannel?.setMethodCallHandler { (call, result) in + switch call.method { + case "current_email_id_in_notification_click": + result(self.remoteNotificationPayload) + self.remoteNotificationPayload = nil + default: + break + } +} +``` + +- As for `LocalNotification`, we will receive the push notification payload at the `didReceive` function of `UNUserNotificationCenter` (similar to `Foreground/Background state`) + +```swift + override func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void +) { + self.remoteNotificationPayload = response.notification.request.content.userInfo + } +``` + +## Consequences + +- The application is correctly adjusted to the detailed screen email when clicking on the notification. +- Any changes to the click push notification logic must be updated in this ADR. From ce95da4409de651389472c4bf9a74ad80f1317d4 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 11 Jul 2024 08:46:28 +0700 Subject: [PATCH 08/11] TF-2871 Fix keychain not being accessible when application is terminated --- ios/TwakeMailNSE/Keychain/KeychainController.swift | 2 +- lib/main/bindings/core/core_bindings.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/TwakeMailNSE/Keychain/KeychainController.swift b/ios/TwakeMailNSE/Keychain/KeychainController.swift index b54321c0b1..068d3159d7 100644 --- a/ios/TwakeMailNSE/Keychain/KeychainController.swift +++ b/ios/TwakeMailNSE/Keychain/KeychainController.swift @@ -15,7 +15,7 @@ class KeychainController: KeychainControllerDelegate { init(service: KeychainControllerService, accessGroup: String) { keychain = Keychain(service: service.identifier, - accessGroup: accessGroup) + accessGroup: accessGroup).accessibility(.afterFirstUnlock) } func retrieveSharingSessionFromKeychain(accountId: String) -> KeychainSharingSession? { diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 5feaf5f03c..4c337813cb 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -79,6 +79,7 @@ class CoreBindings extends Bindings { iOptions: IOSOptions( groupId: AppConfig.iOSKeychainSharingGroupId, accountName: AppConfig.iOSKeychainSharingService, + accessibility: KeychainAccessibility.first_unlock_this_device ) )); } From ab44b93cc42e3f3c0adcf66c396af11389bfe7a7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 11 Jul 2024 08:49:29 +0700 Subject: [PATCH 09/11] TF-2871 Write unit test for KeychainSharingManager --- .../keychain/keychain_sharing_manager.dart | 4 +- .../keychain_sharing_manager_test.dart | 93 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart diff --git a/lib/features/push_notification/data/keychain/keychain_sharing_manager.dart b/lib/features/push_notification/data/keychain/keychain_sharing_manager.dart index 4b267e8ca9..b6d384fdbb 100644 --- a/lib/features/push_notification/data/keychain/keychain_sharing_manager.dart +++ b/lib/features/push_notification/data/keychain/keychain_sharing_manager.dart @@ -11,7 +11,7 @@ class KeychainSharingManager { KeychainSharingManager(this._secureStorage); - Future save(KeychainSharingSession keychainSharingSession) => _secureStorage.write( + Future save(KeychainSharingSession keychainSharingSession) => _secureStorage.write( key: keychainSharingSession.accountId.asString, value: jsonEncode(keychainSharingSession.toJson()), ); @@ -28,7 +28,7 @@ class KeychainSharingManager { } } - Future delete({String? accountId}) { + Future delete({String? accountId}) { if (accountId != null) { return _secureStorage.delete(key: accountId); } else { diff --git a/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart b/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart new file mode 100644 index 0000000000..152bc73d5e --- /dev/null +++ b/test/features/push_notification/data/keychain/keychain_sharing_manager_test.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/push_notification/data/keychain/keychain_sharing_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/data/keychain/keychain_sharing_session.dart'; + +void main() { + late KeychainSharingManager keychainSharingManager; + late FlutterSecureStorage flutterSecureStorage; + + group('KeychainSharingManager:save for the same accountId', () { + setUp(() { + flutterSecureStorage = const FlutterSecureStorage(); + keychainSharingManager = KeychainSharingManager(flutterSecureStorage); + }); + + test('WHEN keychain have no session \n' + 'AND another session saved to keychain \n' + 'THEN keychain will have only new session', () async { + // arrange + final keychainSharingSession = KeychainSharingSession( + accountId: AccountId(Id( + 'ae08b34da40b48f30ec0b94db05675894262fbc5c2e278644f9517aaf25e8246')), + userName: UserName('username@domain.com'), + authenticationType: AuthenticationType.oidc, + apiUrl: 'https://jmap.domain.com/oidc/jmap', + ); + + FlutterSecureStorage.setMockInitialValues({ + keychainSharingSession.accountId.asString: jsonEncode(keychainSharingSession.toJson()) + }); + + // act + await keychainSharingManager.save(keychainSharingSession); + + // assert + final keychainSession = await keychainSharingManager.getSharingSession(AccountId(Id( + 'ae08b34da40b48f30ec0b94db05675894262fbc5c2e278644f9517aaf25e8246'))); + expect(keychainSession, isNotNull); + }); + + test('WHEN keychain have session \n' + 'AND another session saved to keychain \n' + 'THEN keychain will have only new session', () async { + // arrange + final keychainSharingSession1 = KeychainSharingSession( + accountId: AccountId(Id( + 'ae08b34da40b48f30ec0b94db05675894262fbc5c2e278644f9517aaf25e8246')), + userName: UserName('username@domain.com'), + authenticationType: AuthenticationType.oidc, + apiUrl: 'https://jmap.domain.com/oidc/jmap', + ); + + FlutterSecureStorage.setMockInitialValues({ + keychainSharingSession1.accountId.asString: jsonEncode(keychainSharingSession1.toJson()) + }); + + final keychainSharingSession2 = KeychainSharingSession( + accountId: AccountId(Id( + 'ae08b34da40b48f30ec0b94db05675894262fbc5c2e278644f9517aaf25e8246')), + userName: UserName('username@domain.com'), + authenticationType: AuthenticationType.oidc, + apiUrl: 'https://jmap.domain.com/oidc/jmap', + tokenEndpoint: 'https://jmap.domain.com/oidc/jmap', + oidcScopes: ['email'], + emailState: 'ae08b34da40b48f30ec0b94', + tokenOIDC: TokenOIDC( + 'ae08b34da40b48f30ec0b94', + TokenId('ae08b34da40b48f30ec0b94'), + 'ae08b34da40b48f30ec0b94', + expiredTime: DateTime.now() + ) + ); + + // act + await keychainSharingManager.save(keychainSharingSession2); + + // assert + final keychainSession = await keychainSharingManager.getSharingSession(AccountId(Id( + 'ae08b34da40b48f30ec0b94db05675894262fbc5c2e278644f9517aaf25e8246'))); + expect(keychainSession, isNotNull); + expect(keychainSession?.tokenOIDC, isNotNull); + expect(keychainSession?.emailState, equals('ae08b34da40b48f30ec0b94')); + expect(keychainSession?.tokenEndpoint, equals('https://jmap.domain.com/oidc/jmap',)); + expect(keychainSession?.oidcScopes, equals(['email'])); + }); + }); +} From 6b764e8b41d8c3ddd5f41b01f201e4242b2a9cfd Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 11 Jul 2024 09:46:23 +0700 Subject: [PATCH 10/11] TF-2871 Handle click notification to open detailed email on iOS --- core/lib/utils/html/html_utils.dart | 3 +- ...51-push-notification-click-logic-on-ios.md | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 - ios/Runner/AppDelegate.swift | 45 ++++--- ios/TwakeCore/Extensions/DateExtensions.swift | 4 + .../Extensions/IntegerExtensions.swift | 11 -- .../AuthenticationInterceptor.swift | 9 +- ios/TwakeCore/Utils/CoreUtils.swift | 3 + ios/TwakeMailTests/DateConversionTests.swift | 45 ++++++- lib/features/base/base_controller.dart | 28 ++--- .../reloadable/reloadable_controller.dart | 110 ++++++++---------- .../home/presentation/home_controller.dart | 49 ++------ .../authorization_interceptors.dart | 60 +++++----- .../update_authentication_account_state.dart | 19 ++- .../update_account_cache_interactor.dart | 52 +++++++++ ...ate_authentication_account_interactor.dart | 31 ----- .../mailbox_dashboard_controller.dart | 64 +++++----- .../model/preview_email_arguments.dart | 15 --- .../manage_account_dashboard_controller.dart | 2 +- .../work_manager/sending_email_worker.dart | 2 +- .../controller/fcm_message_controller.dart | 2 +- .../presentation/services/fcm_receiver.dart | 30 ----- .../presentation/services/fcm_service.dart | 13 --- lib/main/bindings/core/core_bindings.dart | 4 + .../credential/credential_bindings.dart | 6 +- lib/main/utils/ios_notification_manager.dart | 79 +++++++++++++ lib/main/utils/ios_sharing_manager.dart | 41 +++---- model/lib/extensions/session_extension.dart | 2 + model/lib/oidc/token_oidc.dart | 3 - .../presentation/home_controller_test.dart | 10 +- .../presentation/login_controller_test.dart | 10 +- .../mailbox_dashboard_controller_test.dart | 8 +- 32 files changed, 416 insertions(+), 354 deletions(-) delete mode 100644 ios/TwakeCore/Extensions/IntegerExtensions.swift create mode 100644 lib/features/login/domain/usecases/update_account_cache_interactor.dart delete mode 100644 lib/features/login/domain/usecases/update_authentication_account_interactor.dart delete mode 100644 lib/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart create mode 100644 lib/main/utils/ios_notification_manager.dart diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 828e3ac15c..63a212074f 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -30,7 +30,6 @@ class HtmlUtils { static const unregisterDropListener = ( script: ''' - console.log("unregisterDropListener"); const editor = document.querySelector(".note-editable"); const newEditor = editor.cloneNode(true); editor.parentNode.replaceChild(newEditor, editor);''', @@ -76,12 +75,12 @@ class HtmlUtils { required String base64Data, required String mimeType }) { + log('HtmlUtils::convertBase64ToImageResourceData:'); mimeType = validateHtmlImageResourceMimeType(mimeType); if (!base64Data.endsWith('==')) { base64Data.append('=='); } final imageResource = 'data:$mimeType;base64,$base64Data'; - log('HtmlUtils::convertBase64ToImageResourceData:imageResource: $imageResource'); return imageResource; } diff --git a/docs/adr/0051-push-notification-click-logic-on-ios.md b/docs/adr/0051-push-notification-click-logic-on-ios.md index bba1395622..bd7202c690 100644 --- a/docs/adr/0051-push-notification-click-logic-on-ios.md +++ b/docs/adr/0051-push-notification-click-logic-on-ios.md @@ -33,7 +33,7 @@ Brief the logic flows when clicking notifications: We use `FlutterMethodChannel` to pass `UserInfo` from iOS native code to Flutter dart code ```swift - self.notificationInteractionChannel?.invokeMethod("current_email_id_in_notification_click_on_foreground", arguments: response.notification.request.content.userInfo) + self.notificationInteractionChannel?.invokeMethod("current_email_id_in_notification_click_when_app_foreground_or_background", arguments: response.notification.request.content.userInfo) ``` 2. Terminated state @@ -52,7 +52,7 @@ Save it in a `remoteNotificationPayload` variable in memory and use `FlutterMeth ```swift self.notificationInteractionChannel?.setMethodCallHandler { (call, result) in switch call.method { - case "current_email_id_in_notification_click": + case "current_email_id_in_notification_click_when_app_terminated": result(self.remoteNotificationPayload) self.remoteNotificationPayload = nil default: diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bf1d15d48c..ccb0f1f88a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -21,8 +21,6 @@ F522E8862C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; F522E8872C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; F522E8882C0EE8B600DDA35B /* CoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8852C0EE8B600DDA35B /* CoreUtils.swift */; }; - F522E88A2C0F117900DDA35B /* IntegerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8892C0F117900DDA35B /* IntegerExtensions.swift */; }; - F522E88B2C0F117900DDA35B /* IntegerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E8892C0F117900DDA35B /* IntegerExtensions.swift */; }; F52F993027FD6EB900346091 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52F992F27FD6EB900346091 /* ShareViewController.swift */; }; F52F993327FD6EB900346091 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F52F993127FD6EB900346091 /* MainInterface.storyboard */; }; F52F993727FD6EB900346091 /* TeamMailShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -139,7 +137,6 @@ B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSOTests.swift; sourceTree = ""; }; F522E8852C0EE8B600DDA35B /* CoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUtils.swift; sourceTree = ""; }; - F522E8892C0F117900DDA35B /* IntegerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensions.swift; sourceTree = ""; }; F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TeamMailShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F52F992F27FD6EB900346091 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; F52F993227FD6EB900346091 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; @@ -446,7 +443,6 @@ children = ( F5E7D8782B38763B0009BB8A /* DateExtensions.swift */, F5E7D87B2B38764F0009BB8A /* StringExtensions.swift */, - F522E8892C0F117900DDA35B /* IntegerExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -748,7 +744,6 @@ files = ( F5E7D87C2B38764F0009BB8A /* StringExtensions.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - F522E88A2C0F117900DDA35B /* IntegerExtensions.swift in Sources */, F5E7D8792B38763B0009BB8A /* DateExtensions.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, F522E8862C0EE8B600DDA35B /* CoreUtils.swift in Sources */, @@ -787,7 +782,6 @@ F53D1E7F2B2E3C2600051FD0 /* JmapConstants.swift in Sources */, F5E7D8822B3876F60009BB8A /* AuthenticationCredential.swift in Sources */, F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */, - F522E88B2C0F117900DDA35B /* IntegerExtensions.swift in Sources */, F522E8872C0EE8B600DDA35B /* CoreUtils.swift in Sources */, F53D1E8A2B2E4BB700051FD0 /* TokenOidc.swift in Sources */, F53D1E862B2E401B00051FD0 /* JmapRequestGenerator.swift in Sources */, diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0c13269e00..faa9d613d6 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -8,7 +8,7 @@ import flutter_local_notifications @objc class AppDelegate: FlutterAppDelegate { var notificationInteractionChannel: FlutterMethodChannel? - var initialNotificationInfo: Any? + var currentEmailId: String? override func application( _ application: UIApplication, @@ -17,7 +17,12 @@ import flutter_local_notifications GeneratedPluginRegistrant.register(with: self) createNotificationInteractionChannel() - initialNotificationInfo = launchOptions?[.remoteNotification] + + if let payload = launchOptions?[.remoteNotification] as? [AnyHashable : Any], + let emailId = payload[JmapConstants.EMAIL_ID] as? String, + !emailId.isEmpty { + currentEmailId = emailId + } if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate @@ -90,9 +95,7 @@ import flutter_local_notifications TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent:newBadgeCount: \(newBadgeCount)") updateAppBadger(newBadgeCount: newBadgeCount) } - if let emailId = notification.request.content.userInfo[JmapConstants.EMAIL_ID] as? String, - !emailId.isEmpty, - !isAppForegroundActive() { + if validateDisplayPushNotification(userInfo: notification.request.content.userInfo) { completionHandler([.alert, .badge, .sound]) } else { completionHandler([]) @@ -105,12 +108,24 @@ import flutter_local_notifications let newBadgeCount = currentBadgeCount > 0 ? currentBadgeCount - 1 : 0 updateAppBadger(newBadgeCount: newBadgeCount) - if let emailId = response.notification.request.content.userInfo[JmapConstants.EMAIL_ID] as? String { - self.notificationInteractionChannel?.invokeMethod("openEmail", arguments: emailId) + + let userInfo = response.notification.request.content.userInfo + + if let emailId = userInfo[JmapConstants.EMAIL_ID] as? String, !emailId.isEmpty { + self.notificationInteractionChannel?.invokeMethod( + CoreUtils.CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_FOREGROUND_OR_BACKGROUND, + arguments: emailId) } completionHandler() } + + private func validateDisplayPushNotification(userInfo: [AnyHashable : Any]) -> Bool { + if let emailId = userInfo[JmapConstants.EMAIL_ID] as? String, !emailId.isEmpty, UIApplication.shared.applicationState != .active { + return true + } + return false + } } extension AppDelegate { @@ -134,25 +149,21 @@ extension AppDelegate { } } - private func isAppForegroundActive() -> Bool { - return UIApplication.shared.applicationState == .active - } - private func createNotificationInteractionChannel() { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController self.notificationInteractionChannel = FlutterMethodChannel( - name: "notification_interaction_channel", + name: CoreUtils.NOTIFICATION_INTERACTION_CHANNEL_NAME, binaryMessenger: controller.binaryMessenger ) self.notificationInteractionChannel?.setMethodCallHandler { (call, result) in switch call.method { - case "getInitialNotificationInfo": - result(self.initialNotificationInfo) - self.initialNotificationInfo = nil - default: - break + case CoreUtils.CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_TERMINATED: + result(self.currentEmailId) + self.currentEmailId = nil + default: + break } } } diff --git a/ios/TwakeCore/Extensions/DateExtensions.swift b/ios/TwakeCore/Extensions/DateExtensions.swift index 9ee0917843..305cbde939 100644 --- a/ios/TwakeCore/Extensions/DateExtensions.swift +++ b/ios/TwakeCore/Extensions/DateExtensions.swift @@ -11,4 +11,8 @@ extension Date { dateFormatter.locale = Locale(identifier: CoreUtils.EN_US_POSIX_LOCALE) return dateFormatter.string(from: self) } + + func adding(seconds: Int) -> Date { + return self.addingTimeInterval(TimeInterval(seconds)) + } } diff --git a/ios/TwakeCore/Extensions/IntegerExtensions.swift b/ios/TwakeCore/Extensions/IntegerExtensions.swift deleted file mode 100644 index 1efc2d4648..0000000000 --- a/ios/TwakeCore/Extensions/IntegerExtensions.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -extension Int { - func convertMillisecondsToDate() -> Date { - return Date(timeIntervalSince1970: TimeInterval(self) / 1000) - } - - func convertMillisecondsToISO8601String() -> String { - return convertMillisecondsToDate().convertDateToISO8601String() - } -} diff --git a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift index 4619113ec9..1b7ecb780c 100644 --- a/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift +++ b/ios/TwakeCore/Network/Interceptor/AuthenticationInterceptor.swift @@ -41,9 +41,12 @@ class AuthenticationInterceptor: RequestInterceptor { } let newRefreshToken = tokenResponse.refreshToken ?? authenticationSSO.refreshToken - let expireTime = tokenResponse.expiresTime != nil - ? tokenResponse.expiresTime!.convertMillisecondsToISO8601String() - : nil + + var expireTime: String? = nil + + if (tokenResponse.expiresTime != nil) { + expireTime = CoreUtils.shared.getCurrentDate().adding(seconds: tokenResponse.expiresTime!).convertDateToISO8601String() + } self.authentication = AuthenticationSSO( type: AuthenticationType.oidc, diff --git a/ios/TwakeCore/Utils/CoreUtils.swift b/ios/TwakeCore/Utils/CoreUtils.swift index f0d1bde463..254d08cda7 100644 --- a/ios/TwakeCore/Utils/CoreUtils.swift +++ b/ios/TwakeCore/Utils/CoreUtils.swift @@ -5,6 +5,9 @@ class CoreUtils { static let ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS" static let EN_US_POSIX_LOCALE = "en_US_POSIX" + static let NOTIFICATION_INTERACTION_CHANNEL_NAME = "notification_interaction_channel" + static let CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_FOREGROUND_OR_BACKGROUND = "current_email_id_in_notification_click_when_app_foreground_or_background" + static let CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_TERMINATED = "current_email_id_in_notification_click_when_app_terminated" func getCurrentDate() -> Date { if #available(iOS 15, *) { diff --git a/ios/TwakeMailTests/DateConversionTests.swift b/ios/TwakeMailTests/DateConversionTests.swift index 51c4c58b55..0953d0dbc6 100644 --- a/ios/TwakeMailTests/DateConversionTests.swift +++ b/ios/TwakeMailTests/DateConversionTests.swift @@ -5,25 +5,39 @@ import XCTest class DateConversionTests: XCTestCase { func testConvertValidISO8601StringToDate() { + // Arrange let validDateString = "2024-05-20T22:54:57.958" + // Act let date = validDateString.convertISO8601StringToDate() + + // Assert XCTAssertNotNil(date, "Date should not be nil") + // Arrange let calendar = Calendar.current let expectedComponents = DateComponents(year: 2024, month: 5, day: 20, hour: 22, minute: 54, second: 57, nanosecond: 958000000) + + // Act let expectedDate = calendar.date(from: expectedComponents) + + // Assert XCTAssertEqual(date, expectedDate, "Converted date does not match expected date") } func testInvalidISO8601String() { + // Arrange let invalidDateString = "Invalid Date String" + // Act let date = invalidDateString.convertISO8601StringToDate() + + // Assert XCTAssertNil(date, "The conversion should return nil for an invalid date string.") } func testConvertValidDateToISO8601String() { + // Arrange let validDate = Calendar.current.date( from: DateComponents( year: 2024, @@ -35,11 +49,38 @@ class DateConversionTests: XCTestCase { nanosecond: 958000000 ) ) - - let expectedDateString = "2024-05-20T22:54:57.958" + + // Act let validDateString = validDate!.convertDateToISO8601String() + // Assert XCTAssertEqual(validDateString, expectedDateString, "Converted date string does not match expected date string") } + + func testAddingSeconds() { + // Arrange + let initialDate = Date() + let secondsToAdd = 60 + let expectedDate = initialDate.addingTimeInterval(TimeInterval(secondsToAdd)) + + // Act + let resultDate = initialDate.adding(seconds: secondsToAdd) + + // Assert + XCTAssertEqual(resultDate, expectedDate, "The date after adding seconds should be correct.") + } + + func testAddingNegativeSeconds() { + // Arrange + let initialDate = Date() + let secondsToSubtract = -60 + let expectedDate = initialDate.addingTimeInterval(TimeInterval(secondsToSubtract)) + + // Act + let resultDate = initialDate.adding(seconds: secondsToSubtract) + + // Assert + XCTAssertEqual(resultDate, expectedDate, "The date after subtracting seconds should be correct.") + } } diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index e44350eedf..9ca39a37ea 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -106,7 +106,7 @@ abstract class BaseController extends GetxController (failure) { if (failure is FeatureFailure) { final exception = _performFilterExceptionInError(failure.exception); - logError('BaseController::onData:exception: $exception'); + logError('$runtimeType::onData:exception: $exception'); if (exception != null) { handleExceptionAction(failure: failure, exception: exception); } else { @@ -120,7 +120,7 @@ abstract class BaseController extends GetxController } void onError(Object error, StackTrace stackTrace) { - logError('BaseController::onError():error: $error | stackTrace: $stackTrace'); + logError('$runtimeType::onError():error: $error | stackTrace: $stackTrace'); final exception = _performFilterExceptionInError(error); if (exception != null) { handleExceptionAction(exception: exception); @@ -132,7 +132,7 @@ abstract class BaseController extends GetxController void onDone() {} Exception? _performFilterExceptionInError(dynamic error) { - logError('BaseController::_performFilterExceptionInError(): $error'); + logError('$runtimeType::_performFilterExceptionInError(): $error'); if (error is NoNetworkError || error is ConnectionTimeout || error is InternalServerError) { if (PlatformInfo.isWeb && currentOverlayContext != null && currentContext != null) { appToast.showToastMessage( @@ -157,7 +157,7 @@ abstract class BaseController extends GetxController void handleErrorViewState(Object error, StackTrace stackTrace) {} void handleExceptionAction({Failure? failure, Exception? exception}) { - logError('BaseController::handleExceptionAction():failure: $failure | exception: $exception'); + logError('$runtimeType::handleExceptionAction():failure: $failure | exception: $exception'); if (exception is ConnectionError) { if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( @@ -183,7 +183,7 @@ abstract class BaseController extends GetxController } void handleFailureViewState(Failure failure) async { - logError('BaseController::handleFailureViewState(): ${failure.runtimeType}'); + logError('$runtimeType::handleFailureViewState():Failure = $failure'); if (failure is LogoutOidcFailure) { if (_isFcmEnabled) { _getStoredFirebaseRegistrationFromCache(); @@ -197,7 +197,7 @@ abstract class BaseController extends GetxController } void handleSuccessViewState(Success success) async { - log('BaseController::handleSuccessViewState(): ${success.runtimeType}'); + log('$runtimeType::handleSuccessViewState():Success = ${success.runtimeType}'); if (success is LogoutOidcSuccess) { if (_isFcmEnabled) { _getStoredFirebaseRegistrationFromCache(); @@ -214,7 +214,7 @@ abstract class BaseController extends GetxController void startFpsMeter() { FpsManager().start(); fpsCallback = (fpsInfo) { - log('BaseController::startFpsMeter(): $fpsInfo'); + log('$runtimeType::startFpsMeter(): $fpsInfo'); }; if (fpsCallback != null) { FpsManager().addFpsCallback(fpsCallback!); @@ -234,7 +234,7 @@ abstract class BaseController extends GetxController requireCapability(session!, accountId!, [tmailContactCapabilityIdentifier]); TMailAutoCompleteBindings().dependencies(); } catch (e) { - logError('BaseController::injectAutoCompleteBindings(): exception: $e'); + logError('$runtimeType::injectAutoCompleteBindings(): exception: $e'); } } @@ -243,7 +243,7 @@ abstract class BaseController extends GetxController requireCapability(session!, accountId!, [CapabilityIdentifier.jmapMdn]); MdnInteractorBindings().dependencies(); } catch(e) { - logError('BaseController::injectMdnBindings(): exception: $e'); + logError('$runtimeType::injectMdnBindings(): exception: $e'); } } @@ -252,7 +252,7 @@ abstract class BaseController extends GetxController requireCapability(session!, accountId!, [capabilityForward]); ForwardingInteractorsBindings().dependencies(); } catch(e) { - logError('BaseController::injectForwardBindings(): exception: $e'); + logError('$runtimeType::injectForwardBindings(): exception: $e'); } } @@ -261,14 +261,14 @@ abstract class BaseController extends GetxController requireCapability(session!, accountId!, [capabilityRuleFilter]); EmailRulesInteractorBindings().dependencies(); } catch(e) { - logError('BaseController::injectRuleFilterBindings(): exception: $e'); + logError('$runtimeType::injectRuleFilterBindings(): exception: $e'); } } Future injectFCMBindings(Session? session, AccountId? accountId) async { try { requireCapability(session!, accountId!, [FirebaseCapability.fcmIdentifier]); - log('BaseController::injectFCMBindings: fcmAvailable = ${AppConfig.fcmAvailable}'); + log('$runtimeType::injectFCMBindings: fcmAvailable = ${AppConfig.fcmAvailable}'); if (AppConfig.fcmAvailable) { final mapEnvData = Map.from(dotenv.env); await AppUtils.loadFcmConfigFileToEnv(currentMapEnvData: mapEnvData); @@ -285,7 +285,7 @@ abstract class BaseController extends GetxController throw NotSupportFCMException(); } } catch(e) { - logError('BaseController::injectFCMBindings(): exception: $e'); + logError('$runtimeType::injectFCMBindings(): exception: $e'); } } @@ -348,7 +348,7 @@ abstract class BaseController extends GetxController } Future clearDataAndGoToLoginPage() async { - log('BaseController::clearDataAndGoToLoginPage:'); + log('$runtimeType::clearDataAndGoToLoginPage:'); await clearAllData(); goToLogin(); } diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 71ecafff7d..95f35d6a1f 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -6,16 +6,18 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:model/extensions/session_extension.dart'; +import 'package:model/account/password.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_interactors_bindings.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; @@ -26,16 +28,18 @@ import 'package:tmail_ui_user/main/utils/message_toast_utils.dart'; abstract class ReloadableController extends BaseController { final GetSessionInteractor _getSessionInteractor = Get.find(); final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor = Get.find(); - final UpdateAuthenticationAccountInteractor _updateAuthenticationAccountInteractor = Get.find(); + final UpdateAccountCacheInteractor _updateAccountCacheInteractor = Get.find(); @override void handleFailureViewState(Failure failure) { if (failure is GetCredentialFailure || failure is GetStoredTokenOidcFailure || - failure is GetAuthenticatedAccountFailure) { - log('ReloadableController::handleFailureViewState(): failure: $failure'); + failure is GetAuthenticatedAccountFailure || + failure is UpdateAccountCacheFailure) { + logError('$runtimeType::handleFailureViewState():Failure = $failure'); goToLogin(); } else if (failure is GetSessionFailure) { + logError('$runtimeType::handleFailureViewState():Failure = $failure'); _handleGetSessionFailure(failure.exception); } else { super.handleFailureViewState(failure); @@ -45,11 +49,26 @@ abstract class ReloadableController extends BaseController { @override void handleSuccessViewState(Success success) { if (success is GetCredentialViewState) { - _handleGetCredentialSuccess(success); - } else if (success is GetSessionSuccess) { - _handleGetSessionSuccess(success); + log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); + _setDataToInterceptors( + baseUrl: success.baseUrl.origin, + userName: success.userName, + password: success.password); + getSessionAction(); } else if (success is GetStoredTokenOidcSuccess) { - _handleGetStoredTokenOIDCSuccess(success); + log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); + _setDataToInterceptors( + baseUrl: success.baseUrl.toString(), + tokenOIDC: success.tokenOidc, + oidcConfiguration: success.oidcConfiguration); + getSessionAction(); + } else if (success is GetSessionSuccess) { + log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); + updateAccountCache(success.session); + } else if (success is UpdateAccountCacheSuccess) { + log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); + dynamicUrlInterceptors.changeBaseUrl(success.apiUrl); + handleReloaded(success.session); } else { super.handleSuccessViewState(success); } @@ -66,22 +85,25 @@ abstract class ReloadableController extends BaseController { consumeState(_getAuthenticatedAccountInteractor.execute()); } - void _setUpInterceptors(GetCredentialViewState credentialViewState) { - dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); - dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); - authorizationInterceptors.setBasicAuthorization( - credentialViewState.userName, - credentialViewState.password, - ); - authorizationIsolateInterceptors.setBasicAuthorization( - credentialViewState.userName, - credentialViewState.password, - ); - } + void _setDataToInterceptors({ + required String baseUrl, + UserName? userName, + Password? password, + TokenOIDC? tokenOIDC, + OIDCConfiguration? oidcConfiguration + }) { + dynamicUrlInterceptors.setJmapUrl(baseUrl); + dynamicUrlInterceptors.changeBaseUrl(baseUrl); + + if (userName != null && password != null) { + authorizationInterceptors.setBasicAuthorization(userName, password); + authorizationIsolateInterceptors.setBasicAuthorization(userName, password); + } - void _handleGetCredentialSuccess(GetCredentialViewState credentialViewState) { - _setUpInterceptors(credentialViewState); - getSessionAction(); + if (tokenOIDC != null && oidcConfiguration != null) { + authorizationInterceptors.setTokenAndAuthorityOidc(newToken: tokenOIDC, newConfig: oidcConfiguration); + authorizationIsolateInterceptors.setTokenAndAuthorityOidc(newToken: tokenOIDC, newConfig: oidcConfiguration); + } } void getSessionAction() { @@ -98,50 +120,18 @@ abstract class ReloadableController extends BaseController { clearDataAndGoToLoginPage(); } - void _handleGetSessionSuccess(GetSessionSuccess success) { - final session = success.session; - final personalAccount = session.personalAccount; - final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); - if (apiUrl.isNotEmpty) { - dynamicUrlInterceptors.changeBaseUrl(apiUrl); - updateAuthenticationAccount(session, personalAccount.accountId, session.username); - handleReloaded(session); - } else { - clearDataAndGoToLoginPage(); - } - } - void handleReloaded(Session session) {} - void _handleGetStoredTokenOIDCSuccess(GetStoredTokenOidcSuccess tokenOidcSuccess) { - _setUpInterceptorsOidc(tokenOidcSuccess); - getSessionAction(); - } - - void _setUpInterceptorsOidc(GetStoredTokenOidcSuccess tokenOidcSuccess) { - dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString()); - dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); - authorizationInterceptors.setTokenAndAuthorityOidc( - newToken: tokenOidcSuccess.tokenOidc, - newConfig: tokenOidcSuccess.oidcConfiguration); - authorizationIsolateInterceptors.setTokenAndAuthorityOidc( - newToken: tokenOidcSuccess.tokenOidc, - newConfig: tokenOidcSuccess.oidcConfiguration); - } - void injectVacationBindings(Session? session, AccountId? accountId) { try { requireCapability(session!, accountId!, [CapabilityIdentifier.jmapVacationResponse]); VacationInteractorsBindings().dependencies(); } catch(e) { - logError('ReloadableController::injectVacationBindings(): exception: $e'); + logError('$runtimeType::injectVacationBindings(): exception: $e'); } } - void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { - final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); - if (apiUrl.isNotEmpty) { - consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); - } + void updateAccountCache(Session session) { + consumeState(_updateAccountCacheInteractor.execute(session)); } } \ No newline at end of file diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index c7a40a7d93..4f6758cddd 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -2,11 +2,8 @@ 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/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/account/personal_account.dart'; import 'package:model/email/email_content.dart'; import 'package:model/email/email_content_type.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; @@ -21,12 +18,11 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cac import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_url_cache_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_username_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; +import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; class HomeController extends ReloadableController { final CleanupEmailCacheInteractor _cleanupEmailCacheInteractor; @@ -35,6 +31,8 @@ class HomeController extends ReloadableController { final CleanupRecentLoginUrlCacheInteractor _cleanupRecentLoginUrlCacheInteractor; final CleanupRecentLoginUsernameCacheInteractor _cleanupRecentLoginUsernameCacheInteractor; + IOSNotificationManager? _iosNotificationManager; + HomeController( this._cleanupEmailCacheInteractor, this._emailReceiveManager, @@ -43,9 +41,6 @@ class HomeController extends ReloadableController { this._cleanupRecentLoginUsernameCacheInteractor, ); - PersonalAccount? currentAccount; - EmailId? _emailIdPreview; - @override void onInit() { if (PlatformInfo.isMobile) { @@ -53,7 +48,7 @@ class HomeController extends ReloadableController { _registerReceivingSharingIntent(); } if (PlatformInfo.isIOS) { - _handleIOSDataMessage(); + _registerNotificationClickOnIOS(); } super.onInit(); } @@ -66,20 +61,9 @@ class HomeController extends ReloadableController { @override void handleReloaded(Session session) { - if (_emailIdPreview != null) { - popAndPush( - RouteUtils.generateNavigationRoute(AppRoutes.dashboard), - arguments: PreviewEmailArguments( - session: session, - emailId: _emailIdPreview! - ) - ); - } else { - popAndPush( - RouteUtils.generateNavigationRoute(AppRoutes.dashboard), - arguments: session - ); - } + pushAndPopAll( + RouteUtils.generateNavigationRoute(AppRoutes.dashboard), + arguments: session); } void _initFlutterDownloader() { @@ -90,7 +74,7 @@ class HomeController extends ReloadableController { static void downloadCallback(String id, DownloadTaskStatus status, int progress) {} - void _cleanupCache() async { + Future _cleanupCache() async { await HiveCacheConfig.instance.onUpgradeDatabase(cachingManager); await Future.wait([ @@ -98,7 +82,7 @@ class HomeController extends ReloadableController { _cleanupRecentSearchCacheInteractor.execute(RecentSearchCleanupRule()), _cleanupRecentLoginUrlCacheInteractor.execute(RecentLoginUrlCleanupRule()), _cleanupRecentLoginUsernameCacheInteractor.execute(RecentLoginUsernameCleanupRule()), - ]).then((value) => getAuthenticatedAccountAction()); + ], eagerError: true).then((_) => getAuthenticatedAccountAction()); } void _registerReceivingSharingIntent() { @@ -117,17 +101,8 @@ class HomeController extends ReloadableController { _emailReceiveManager.receivingFileSharingStream.listen(_emailReceiveManager.setPendingFileInfo); } - Future _handleIOSDataMessage() async { - if (Get.arguments is EmailId) { - _emailIdPreview = Get.arguments; - } else { - final notificationInfo = await FcmReceiver.instance.getIOSInitialNotificationInfo(); - if (notificationInfo != null && notificationInfo.containsKey('email_id')) { - final emailId = notificationInfo['email_id'] as String?; - if (emailId?.isNotEmpty == true) { - _emailIdPreview = EmailId(Id(emailId!)); - } - } - } + void _registerNotificationClickOnIOS() { + _iosNotificationManager = getBinding(); + _iosNotificationManager?.listenClickNotification(); } } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 34d21b5db6..581b1c1aae 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -48,11 +48,11 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { _token = newToken; _configOIDC = newConfig; _authenticationType = AuthenticationType.oidc; - log('AuthorizationInterceptors::setTokenAndAuthorityOidc: INITIAL_TOKEN = ${newToken?.token} | EXPIRED_TIME = ${newToken?.expiredTime}'); + log('AuthorizationInterceptors::setTokenAndAuthorityOidc: TokenId = ${newToken?.tokenIdHash}'); } void _updateNewToken(TokenOIDC newToken) { - log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = ${newToken.token} | EXPIRED_TIME = ${newToken.expiredTime}'); + log('AuthorizationInterceptors::_updateNewToken: TokenId = ${newToken.tokenIdHash}'); _token = newToken; } @@ -76,13 +76,13 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { case AuthenticationType.none: break; } - log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | HEADER = ${options.headers} | DATA = ${options.data} | METHOD = ${options.method}'); + log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | DATA = ${options.data}'); super.onRequest(options, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) async { - logError('AuthorizationInterceptors::onError(): TOKEN = ${_token?.expiredTime} | DIO_ERROR = $err | METHOD = ${err.requestOptions.method}'); + logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err'); try { final requestOptions = err.requestOptions; final extraInRequest = requestOptions.extra; @@ -92,24 +92,29 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { responseStatusCode: err.response?.statusCode, tokenOIDC: _token )) { - log('AuthorizationInterceptors::onError:_validateToRefreshToken'); + log('AuthorizationInterceptors::onError: Perform get New Token'); final newTokenOidc = PlatformInfo.isIOS - ? await _handleRefreshTokenOnIOSPlatform() - : await _handleRefreshTokenOnOtherPlatform(); + ? await _getNewTokenForIOSPlatform() + : await _getNewTokenForOtherPlatform(); if (newTokenOidc.token == _token?.token) { - log('AuthorizationInterceptors::onError: TokenOIDC duplicated'); + log('AuthorizationInterceptors::onError: Token duplicated'); return super.onError(err, handler); } - _updateNewToken(newTokenOidc); + final personalAccount = await _updateCurrentAccount(tokenOIDC: newTokenOidc); + + if (PlatformInfo.isIOS) { + await _iosSharingManager.saveKeyChainSharingSession(personalAccount); + } + isRetryRequest = true; } else if (validateToRetryTheRequestWithNewToken( authHeader: requestOptions.headers[HttpHeaders.authorizationHeader], tokenOIDC: _token )) { - log('AuthorizationInterceptors::onError:validateToRetryTheRequestWithNewToken'); + log('AuthorizationInterceptors::onError: Request using old token'); isRetryRequest = true; } else { return super.onError(err, handler); @@ -117,7 +122,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { if (isRetryRequest) { if (extraInRequest.containsKey(FileUploader.uploadAttachmentExtraKey)) { - log('AuthorizationInterceptors::onError: Perform upload attachment request'); + log('AuthorizationInterceptors::onError: Retry upload request with TokenId = ${_token?.tokenIdHash}'); final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey]; requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); @@ -136,7 +141,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { return handler.resolve(response); } else { - log('AuthorizationInterceptors::onError: Perform normal request'); + log('AuthorizationInterceptors::onError: Retry request with TokenId = ${_token?.tokenIdHash}'); requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); final response = await _dio.fetch(requestOptions); @@ -231,13 +236,11 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { Future _getTokenInKeychain(TokenOIDC currentTokenOidc) async { final currentAccount = await _accountCacheManager.getCurrentAccount(); - log('AuthorizationInterceptors::_getTokenInKeychain:currentAccount: $currentAccount'); if (currentAccount.accountId == null) { return null; } final keychainSharingSession = await _iosSharingManager.getKeychainSharingSession(currentAccount.accountId!); - log('AuthorizationInterceptors::_getTokenInKeychain:keychainSharingSession: $keychainSharingSession'); if (keychainSharingSession == null) { return null; } @@ -250,36 +253,29 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { return null; } - Future _invokeRefreshTokenFromServer() async { - final newToken = await _authenticationClient.refreshingTokensOIDC( + Future _invokeRefreshTokenFromServer() { + log('AuthorizationInterceptors::_invokeRefreshTokenFromServer:'); + return _authenticationClient.refreshingTokensOIDC( _configOIDC!.clientId, _configOIDC!.redirectUrl, _configOIDC!.discoveryUrl, _configOIDC!.scopes, _token!.refreshToken ); - log('AuthorizationInterceptors::_invokeRefreshTokenFromServer:newToken: $newToken'); - return newToken; } - Future _handleRefreshTokenOnIOSPlatform() async { - final keychainToken = await _getTokenInKeychain(_token!); - - if (keychainToken == null) { - final newToken = await _invokeRefreshTokenFromServer(); - final newAccount = await _updateCurrentAccount(tokenOIDC: newToken); - await _iosSharingManager.saveKeyChainSharingSession(newAccount); - return newToken; + Future _getNewTokenForIOSPlatform() async { + final tokenInKeychain = await _getTokenInKeychain(_token!); + log('AuthorizationInterceptors::_handleRefreshTokenOnIOSPlatform: KeychainTokenId = ${tokenInKeychain?.tokenIdHash} | isTokenExpired = ${_isTokenExpired(tokenInKeychain)}'); + if (tokenInKeychain == null || _isTokenExpired(tokenInKeychain)) { + return _invokeRefreshTokenFromServer(); } else { - await _updateCurrentAccount(tokenOIDC: keychainToken); - return keychainToken; + return tokenInKeychain; } } - Future _handleRefreshTokenOnOtherPlatform() async { - final newToken = await _invokeRefreshTokenFromServer(); - await _updateCurrentAccount(tokenOIDC: newToken); - return newToken; + Future _getNewTokenForOtherPlatform() { + return _invokeRefreshTokenFromServer(); } void clear() { diff --git a/lib/features/login/domain/state/update_authentication_account_state.dart b/lib/features/login/domain/state/update_authentication_account_state.dart index c5d1931edf..f4a2f56080 100644 --- a/lib/features/login/domain/state/update_authentication_account_state.dart +++ b/lib/features/login/domain/state/update_authentication_account_state.dart @@ -1,11 +1,22 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; -class UpdateAuthenticationAccountLoading extends LoadingState {} +class UpdatingAccountCache extends LoadingState {} -class UpdateAuthenticationAccountSuccess extends UIState {} +class UpdateAccountCacheSuccess extends UIState { + final Session session; + final String apiUrl; -class UpdateAuthenticationAccountFailure extends FeatureFailure { + UpdateAccountCacheSuccess({ + required this.session, + required this.apiUrl}); - UpdateAuthenticationAccountFailure(dynamic exception) : super(exception: exception); + @override + List get props => [session, apiUrl]; +} + +class UpdateAccountCacheFailure extends FeatureFailure { + + UpdateAccountCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/update_account_cache_interactor.dart b/lib/features/login/domain/usecases/update_account_cache_interactor.dart new file mode 100644 index 0000000000..b92dcb7fb7 --- /dev/null +++ b/lib/features/login/domain/usecases/update_account_cache_interactor.dart @@ -0,0 +1,52 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:model/extensions/personal_account_extension.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; + +class UpdateAccountCacheInteractor { + final AccountRepository _accountRepository; + final CredentialRepository _credentialRepository; + + UpdateAccountCacheInteractor( + this._accountRepository, + this._credentialRepository); + + Stream> execute(Session session) async* { + try{ + yield Right(UpdatingAccountCache()); + + final futureValue = await Future.wait([ + _credentialRepository.getBaseUrl(), + _accountRepository.getCurrentAccount(), + ], eagerError: true); + + final baseUrl = futureValue[0] as Uri; + final currentAccount = futureValue[1] as PersonalAccount; + final apiUrl = session.getQualifiedApiUrl( + baseUrl: currentAccount.authenticationType == AuthenticationType.basic + ? baseUrl.origin + : baseUrl.toString()); + + await _accountRepository.setCurrentAccount( + currentAccount.fromAccount( + accountId: session.accountId, + apiUrl: apiUrl, + userName: session.username + )); + + yield Right(UpdateAccountCacheSuccess( + session: session, + apiUrl: apiUrl)); + } catch(e) { + yield Left(UpdateAccountCacheFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart b/lib/features/login/domain/usecases/update_authentication_account_interactor.dart deleted file mode 100644 index d12cfd696a..0000000000 --- a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:model/extensions/personal_account_extension.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; - -class UpdateAuthenticationAccountInteractor { - final AccountRepository _accountRepository; - - UpdateAuthenticationAccountInteractor(this._accountRepository); - - Stream> execute(AccountId accountId, String apiUrl, UserName userName) async* { - try{ - yield Right(UpdateAuthenticationAccountLoading()); - final currentAccount = await _accountRepository.getCurrentAccount(); - await _accountRepository.setCurrentAccount( - currentAccount.fromAccount( - accountId: accountId, - apiUrl: apiUrl, - userName: userName - ) - ); - yield Right(UpdateAuthenticationAccountSuccess()); - } catch(e) { - yield Left(UpdateAuthenticationAccountFailure(e)); - } - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 1c7103e456..b7cbe88902 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -94,7 +94,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/comp import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/refresh_action_view_event.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -153,6 +152,7 @@ import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; +import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; import 'package:uuid/uuid.dart'; class MailboxDashBoardController extends ReloadableController { @@ -197,6 +197,7 @@ class MailboxDashBoardController extends ReloadableController { GetMailboxStateToRefreshInteractor? _getMailboxStateToRefreshInteractor; DeleteMailboxStateToRefreshInteractor? _deleteMailboxStateToRefreshInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; + IOSNotificationManager? _iosNotificationManager; final scaffoldKey = GlobalKey(); final selectedMailbox = Rxn(); @@ -234,6 +235,8 @@ class MailboxDashBoardController extends ReloadableController { late StreamSubscription _emailContentStreamSubscription; late StreamSubscription _fileReceiveManagerStreamSubscription; + StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; + final StreamController> _progressStateController = StreamController>.broadcast(); Stream> get progressState => _progressStateController.stream; @@ -272,7 +275,7 @@ class MailboxDashBoardController extends ReloadableController { ); @override - void onInit() async { + void onInit() { _registerStreamListener(); BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard); WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -286,6 +289,9 @@ class MailboxDashBoardController extends ReloadableController { _registerPendingEmailAddress(); _registerPendingEmailContents(); _registerPendingFileInfo(); + if (PlatformInfo.isIOS) { + _registerPendingCurrentEmailIdInNotification(); + } _handleArguments(); super.onReady(); } @@ -349,7 +355,7 @@ class MailboxDashBoardController extends ReloadableController { } else if (success is GetAppDashboardConfigurationSuccess) { appGridDashboardController.handleShowAppDashboard(success.linagoraApplications); } else if(success is GetEmailByIdSuccess) { - _moveToEmailDetailedView(success); + openEmailDetailedView(success.email); } else if (success is StoreSendingEmailSuccess) { _handleStoreSendingEmailSuccess(success); } else if (success is GetAllSendingEmailSuccess) { @@ -392,7 +398,7 @@ class MailboxDashBoardController extends ReloadableController { } else if (failure is MarkAsMailboxReadFailure) { _markAsReadMailboxFailure(failure); } else if (failure is GetEmailByIdFailure) { - _handleGetEmailDetailedFailed(failure); + _handleGetEmailByIdFailure(failure); } else if (failure is RestoreDeletedMessageFailure) { _handleRestoreDeletedMessageFailed(); } else if (failure is GetRestoredDeletedMessageFailure) { @@ -439,6 +445,18 @@ class MailboxDashBoardController extends ReloadableController { }); } + void _registerPendingCurrentEmailIdInNotification() { + _iosNotificationManager = getBinding(); + _currentEmailIdInNotificationIOSStreamSubscription = _iosNotificationManager + ?.pendingCurrentEmailIdInNotification.stream + .listen((emailId) { + if (emailId != null) { + _iosNotificationManager?.clearPendingCurrentEmailId(); + _handleNotificationMessageFromEmailId(emailId); + } + }); + } + void _registerStreamListener() { progressState.listen((state) { viewStateMarkAsReadMailbox.value = state; @@ -455,7 +473,7 @@ class MailboxDashBoardController extends ReloadableController { _notificationManager.localNotificationStream.listen(_handleClickLocalNotificationOnForeground); } - void _handleClickLocalNotificationOnTerminated() async { + Future _handleClickNotificationOnAndroidInTerminated() async { _notificationManager.activatedNotificationClickedOnTerminate = true; final notificationResponse = await _notificationManager.getCurrentNotificationResponse(); log('MailboxDashBoardController::_handleClickLocalNotificationOnTerminated():payload: ${notificationResponse?.payload}'); @@ -464,13 +482,11 @@ class MailboxDashBoardController extends ReloadableController { void _handleArguments() { final arguments = Get.arguments; - log('MailboxDashBoardController::_getSessionCurrent(): arguments = $arguments'); + log('MailboxDashBoardController::_handleArguments():Arguments = ${arguments.runtimeType}'); if (arguments is Session) { _handleSessionFromArguments(arguments); } else if (arguments is MailtoArguments) { _handleMailtoURL(arguments); - } else if (arguments is PreviewEmailArguments) { - _handleOpenEmailAction(arguments); } else { dispatchRoute(DashboardRoutes.thread); reload(); @@ -501,11 +517,7 @@ class MailboxDashBoardController extends ReloadableController { void _handleSessionFromArguments(Session session) { log('MailboxDashBoardController::_handleSession:'); - updateAuthenticationAccount( - session, - session.personalAccount.accountId, - session.username - ); + updateAccountCache(session); _setUpComponentsFromSession(session); @@ -513,15 +525,15 @@ class MailboxDashBoardController extends ReloadableController { _handleComposerCache(); } - if (PlatformInfo.isMobile && !_notificationManager.isNotificationClickedOnTerminate) { - _handleClickLocalNotificationOnTerminated(); + if (PlatformInfo.isAndroid && !_notificationManager.isNotificationClickedOnTerminate) { + _handleClickNotificationOnAndroidInTerminated(); } else { dispatchRoute(DashboardRoutes.thread); } } void _setUpComponentsFromSession(Session session) { - final currentAccountId = session.personalAccount.accountId; + final currentAccountId = session.accountId; sessionCurrent = session; accountId.value = currentAccountId; @@ -546,13 +558,6 @@ class MailboxDashBoardController extends ReloadableController { _handleSessionFromArguments(arguments.session); } - void _handleOpenEmailAction(PreviewEmailArguments arguments) { - log('MailboxDashBoardController::_handleOpenEmailAction:arguments: $arguments'); - dispatchRoute(DashboardRoutes.waiting); - _handleSessionFromArguments(arguments.session); - _handleNotificationMessageFromEmailId(arguments.emailId); - } - void _getVacationResponse() { if (accountId.value != null && _getAllVacationInteractor != null) { consumeState(_getAllVacationInteractor!.execute(accountId.value!)); @@ -1794,14 +1799,7 @@ class MailboxDashBoardController extends ReloadableController { )); } - void _moveToEmailDetailedView(GetEmailByIdSuccess success) { - log('MailboxDashBoardController::_moveToEmailDetailedView(): ${success.email}'); - setSelectedEmail(success.email); - dispatchRoute(DashboardRoutes.emailDetailed); - } - - void _handleGetEmailDetailedFailed(GetEmailByIdFailure failure) { - logError('MailboxDashBoardController::_handleGetEmailDetailedFailed(): $failure'); + void _handleGetEmailByIdFailure(GetEmailByIdFailure failure) { dispatchRoute(DashboardRoutes.thread); } @@ -2552,6 +2550,10 @@ class MailboxDashBoardController extends ReloadableController { @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); + if (PlatformInfo.isIOS) { + _iosNotificationManager?.dispose(); + _currentEmailIdInNotificationIOSStreamSubscription?.cancel(); + } _emailAddressStreamSubscription.cancel(); _emailContentStreamSubscription.cancel(); _fileReceiveManagerStreamSubscription.cancel(); diff --git a/lib/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart b/lib/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart deleted file mode 100644 index 4ac7555eff..0000000000 --- a/lib/features/mailbox_dashboard/presentation/model/preview_email_arguments.dart +++ /dev/null @@ -1,15 +0,0 @@ - -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/main/routes/router_arguments.dart'; - -class PreviewEmailArguments extends RouterArguments { - - final Session session; - final EmailId emailId; - - PreviewEmailArguments({required this.session, required this.emailId}); - - @override - List get props => [session, emailId]; -} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index acff137cbe..40106ecbf0 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -85,7 +85,7 @@ class ManageAccountDashBoardController extends ReloadableController { void handleReloaded(Session session) { log('ManageAccountDashBoardController::handleReloaded:'); sessionCurrent = session; - accountId.value = session.personalAccount.accountId; + accountId.value = session.accountId; _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); _getParametersRouter(); diff --git a/lib/features/offline_mode/work_manager/sending_email_worker.dart b/lib/features/offline_mode/work_manager/sending_email_worker.dart index cc8064914c..670aac94d4 100644 --- a/lib/features/offline_mode/work_manager/sending_email_worker.dart +++ b/lib/features/offline_mode/work_manager/sending_email_worker.dart @@ -161,7 +161,7 @@ class SendingEmailWorker extends Worker { void _handleGetSessionSuccess(GetSessionSuccess success) async { _currentSession = success.session; - _currentAccountId = success.session.personalAccount.accountId; + _currentAccountId = success.session.accountId; final apiUrl = success.session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors?.jmapUrl); if (apiUrl.isNotEmpty && _currentSession != null && _currentAccountId != null) { _dynamicUrlInterceptors?.changeBaseUrl(apiUrl); diff --git a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart index 9c69a7153d..c573fea736 100644 --- a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart @@ -270,7 +270,7 @@ class FcmMessageController extends FcmBaseController { _dynamicUrlInterceptors?.changeBaseUrl(apiUrl); _pushActionFromRemoteMessageBackground( - accountId: success.session.personalAccount.accountId, + accountId: success.session.accountId, userName: success.session.username, stateChange: stateChange, session: success.session); diff --git a/lib/features/push_notification/presentation/services/fcm_receiver.dart b/lib/features/push_notification/presentation/services/fcm_receiver.dart index 66ff6fdabc..51fa43e66f 100644 --- a/lib/features/push_notification/presentation/services/fcm_receiver.dart +++ b/lib/features/push_notification/presentation/services/fcm_receiver.dart @@ -1,9 +1,7 @@ - import 'package:core/utils/app_logger.dart'; import 'package:core/utils/broadcast_channel/broadcast_channel.dart'; import 'package:core/utils/platform_info.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/services.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; @@ -23,7 +21,6 @@ class FcmReceiver { static FcmReceiver get instance => _instance; - static const notificationInteractionChannel = MethodChannel('notification_interaction_channel'); static const int MAX_COUNT_RETRY_TO_GET_FCM_TOKEN = 3; int _countRetryToGetFcmToken = 0; @@ -36,24 +33,11 @@ class FcmReceiver { if (PlatformInfo.isWeb) { _onMessageBroadcastChannel(); await _requestNotificationPermissionOnWeb(); - } else if (PlatformInfo.isIOS) { - _setUpIOSNotificationInteraction(); - await _onHandleFcmToken(); } else { await _onHandleFcmToken(); } } - void _setUpIOSNotificationInteraction() { - notificationInteractionChannel.setMethodCallHandler((call) async { - log('FcmReceiver::_setUpIOSNotificationInteraction:notificationInteractionChannel: $call'); - if (call.method == 'openEmail' && call.arguments is String) { - log('FcmReceiver::_setUpIOSNotificationInteraction:openEmail with id = ${call.arguments}'); - FcmService.instance.handleOpenEmailFromNotification(call.arguments); - } - }); - } - Future _requestNotificationPermissionOnWeb() async { NotificationSettings notificationSetting = await FirebaseMessaging.instance.getNotificationSettings(); log('FcmReceiver::_requestNotificationPermissionOnWeb: authorizationStatus = ${notificationSetting.authorizationStatus}'); @@ -118,18 +102,4 @@ class FcmReceiver { } }); } - - Future?> getIOSInitialNotificationInfo() async { - try { - final notificationInfo = await notificationInteractionChannel.invokeMethod('getInitialNotificationInfo'); - log('FcmReceiver::getIOSInitialNotificationInfo:notificationInfo: $notificationInfo'); - if (notificationInfo != null && notificationInfo is Map) { - return notificationInfo; - } - return null; - } catch (e) { - logError('FcmReceiver::getIOSInitialNotificationInfo: Exception: $e'); - return null; - } - } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/services/fcm_service.dart b/lib/features/push_notification/presentation/services/fcm_service.dart index 031b43d82b..147bded91b 100644 --- a/lib/features/push_notification/presentation/services/fcm_service.dart +++ b/lib/features/push_notification/presentation/services/fcm_service.dart @@ -4,12 +4,7 @@ import 'dart:convert'; import 'package:core/utils/app_logger.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/model/broadcast_message_event_data.dart'; -import 'package:tmail_ui_user/main/routes/app_routes.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:universal_html/html.dart' as html; class FcmService { @@ -63,14 +58,6 @@ class FcmService { fcmTokenStreamController = StreamController.broadcast(); } - void handleOpenEmailFromNotification(String emailId) { - log('FcmService::handleOpenEmailFromNotification:emailId: $emailId'); - popAndPush( - RouteUtils.generateNavigationRoute(AppRoutes.home), - arguments: EmailId(Id(emailId)) - ); - } - void closeStream() { foregroundMessageStreamController?.close(); backgroundMessageStreamController?.close(); diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 4c337813cb..069808895b 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -16,6 +16,7 @@ import 'package:tmail_ui_user/features/base/before_unload_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; +import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; import 'package:uuid/uuid.dart'; class CoreBindings extends Bindings { @@ -66,6 +67,9 @@ class CoreBindings extends Bindings { Get.put(PrintUtils()); Get.put(ApplicationManager(Get.find())); Get.put(BeforeUnloadManager()); + if (PlatformInfo.isIOS) { + Get.put(IOSNotificationManager()); + } } void _bindingIsolate() { diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index 82abf014d2..35cf854748 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -27,7 +27,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_i import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; @@ -61,7 +61,9 @@ class CredentialBindings extends InteractorsBindings { Get.find(), Get.find() )); - Get.put(UpdateAuthenticationAccountInteractor(Get.find())); + Get.put(UpdateAccountCacheInteractor( + Get.find(), + Get.find())); } @override diff --git a/lib/main/utils/ios_notification_manager.dart b/lib/main/utils/ios_notification_manager.dart new file mode 100644 index 0000000000..d8dc83259d --- /dev/null +++ b/lib/main/utils/ios_notification_manager.dart @@ -0,0 +1,79 @@ + +import 'dart:async'; + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/services.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/model.dart'; +import 'package:rxdart/rxdart.dart'; + +class IOSNotificationManager { + + static const _notificationInteractionChannel = MethodChannel('notification_interaction_channel'); + + static const String CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_FOREGROUND_OR_BACKGROUND = 'current_email_id_in_notification_click_when_app_foreground_or_background'; + static const String CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_TERMINATED = 'current_email_id_in_notification_click_when_app_terminated'; + + BehaviorSubject _pendingCurrentEmailIdInNotification = BehaviorSubject.seeded(null); + BehaviorSubject get pendingCurrentEmailIdInNotification => _pendingCurrentEmailIdInNotification; + + StreamSubscription? _getCurrentEmailIdStreamSubscription; + + void listenClickNotification() { + try { + _notificationInteractionChannel.setMethodCallHandler((methodCall) async { + log('IOSNotificationManager::listenClickNotification: $methodCall'); + if (methodCall.method == CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_FOREGROUND_OR_BACKGROUND + && methodCall.arguments != null) { + final emailId = EmailId(Id(methodCall.arguments)); + setPendingCurrentEmailId(emailId); + } + }); + + _getCurrentEmailIdStreamSubscription = Stream.fromFuture(_getCurrentEmailIdInNotificationClick()).listen((emailId) { + log('IOSNotificationManager::listenClickNotification:_getCurrentEmailIdInNotificationClick:EmailId = ${emailId?.asString}'); + if (emailId != null) { + setPendingCurrentEmailId(emailId); + } + }); + } catch (e) { + logError('IOSNotificationManager::listenClickNotification:Exception = $e'); + } + } + + Future _getCurrentEmailIdInNotificationClick() async { + try { + log('IOSNotificationManager::_getCurrentEmailIdInNotificationClick: START'); + final emailId = await _notificationInteractionChannel.invokeMethod(CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_TERMINATED); + log('IOSNotificationManager::_getCurrentEmailIdInNotificationClick: END'); + if (emailId?.isNotEmpty == true) { + return EmailId(Id(emailId!)); + } else { + return null; + } + } catch (e) { + logError('IOSNotificationManager::getCurrentEmailIdInNotificationClick:Exception = $e'); + return null; + } + } + + void setPendingCurrentEmailId(EmailId emailId) async { + clearPendingCurrentEmailId(); + _pendingCurrentEmailIdInNotification.add(emailId); + } + + void clearPendingCurrentEmailId() { + if(_pendingCurrentEmailIdInNotification.isClosed) { + _pendingCurrentEmailIdInNotification = BehaviorSubject.seeded(null); + } else { + _pendingCurrentEmailIdInNotification.add(null); + } + } + + void dispose() { + _pendingCurrentEmailIdInNotification.close(); + _getCurrentEmailIdStreamSubscription?.cancel(); + _getCurrentEmailIdStreamSubscription = null; + } +} \ No newline at end of file diff --git a/lib/main/utils/ios_sharing_manager.dart b/lib/main/utils/ios_sharing_manager.dart index 5c40f21b24..62367fb43e 100644 --- a/lib/main/utils/ios_sharing_manager.dart +++ b/lib/main/utils/ios_sharing_manager.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/authentication_type.dart'; import 'package:model/account/personal_account.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; @@ -50,20 +50,22 @@ class IOSSharingManager { } } - Future saveKeyChainSharingSession(PersonalAccount personalAccount) async { + Future saveKeyChainSharingSession(PersonalAccount personalAccount) async { + log('IOSSharingManager::saveKeyChainSharingSession: START'); try { if (!_validateToSaveKeychain(personalAccount)) { - logError('IOSSharingManager::saveKeyChainSharingSession: account is null'); + logError('IOSSharingManager::saveKeyChainSharingSession: AccountId | Username | ApiUrl is NULL'); return Future.value(null); } - Tuple2 authenticationInfo = await Future.wait( - [ - _getTokenOidc(tokeHashId: personalAccount.id), - _getCredentialAuthentication() - ], - eagerError: true - ).then((listValue) => Tuple2(listValue[0] as TokenOIDC?, listValue[1] as String?)); + TokenOIDC? tokenOIDC; + String? credentialInfo; + + if (personalAccount.authenticationType == AuthenticationType.oidc) { + tokenOIDC = await _getTokenOidc(tokeHashId: personalAccount.id); + } else { + credentialInfo = await _getCredentialAuthentication(); + } final emailDeliveryState = await _getEmailDeliveryState( accountId: personalAccount.accountId!, @@ -84,13 +86,15 @@ class IOSSharingManager { apiUrl: personalAccount.apiUrl!, emailState: emailState, emailDeliveryState: emailDeliveryState, - tokenOIDC: authenticationInfo.value1, - basicAuth: authenticationInfo.value2, + tokenOIDC: tokenOIDC, + basicAuth: credentialInfo, tokenEndpoint: tokenRecords?.tokenEndpoint, oidcScopes: tokenRecords?.scopes, ); - log('IOSSharingManager::_saveKeyChainSharingSession: $keychainSharingSession'); + await _keychainSharingManager.save(keychainSharingSession); + + log('IOSSharingManager::_saveKeyChainSharingSession: COMPLETED'); } catch (e) { logError('IOSSharingManager::_saveKeyChainSharingSession: Exception: $e'); } @@ -111,9 +115,7 @@ class IOSSharingManager { Future _getTokenOidc({required String tokeHashId}) async { try { - final tokenOidc = await _tokenOidcCacheManager.getTokenOidc(tokeHashId); - log('IOSSharingManager::_getTokenOidc:tokenOidc: $tokenOidc'); - return tokenOidc; + return await _tokenOidcCacheManager.getTokenOidc(tokeHashId); } catch (e) { logError('IOSSharingManager::_getTokenOidc:Exception: $e'); return null; @@ -123,7 +125,6 @@ class IOSSharingManager { Future _getCredentialAuthentication() async { try { final credentialInfo = await _authenticationInfoCacheManager.getAuthenticationInfoStored(); - log('IOSSharingManager::_getCredentialAuthentication:credentialInfo: $credentialInfo'); return base64Encode(utf8.encode('${credentialInfo.username}:${credentialInfo.password}')); } catch (e) { logError('IOSSharingManager::_getCredentialAuthentication:Exception: $e'); @@ -136,9 +137,7 @@ class IOSSharingManager { required UserName userName }) async { try { - final emailDeliveryState = await getEmailDeliveryStateFromKeychain(accountId); - log('IOSSharingManager::_getEmailState:emailDeliveryState: $emailDeliveryState'); - return emailDeliveryState; + return await getEmailDeliveryStateFromKeychain(accountId); } catch (e) { logError('IOSSharingManager::_getEmailDeliveryState:Exception: $e'); return null; @@ -167,7 +166,6 @@ class IOSSharingManager { try { final oidcConfig = await _oidcConfigurationCacheManager.getOidcConfiguration(); final oidcDiscoveryResponse = await _oidcHttpClient.discoverOIDC(oidcConfig); - log('IOSSharingManager::_getTokenEndpointAndScopes:oidcDiscoveryResponse = $oidcDiscoveryResponse | oidcConfig = $oidcConfig'); return ( tokenEndpoint: oidcDiscoveryResponse.tokenEndpoint, scopes: oidcConfig.scopes @@ -180,7 +178,6 @@ class IOSSharingManager { Future updateEmailStateInKeyChain(AccountId accountId, String newEmailState) async { final keychainSharingStored = await getKeychainSharingSession(accountId); - log('IOSSharingManager::updateEmailStateInKeyChain:keychainSharingStored: $keychainSharingStored | newEmailState: $newEmailState'); if (keychainSharingStored == null) { return; } diff --git a/model/lib/extensions/session_extension.dart b/model/lib/extensions/session_extension.dart index 48126265c0..f3661ca9b1 100644 --- a/model/lib/extensions/session_extension.dart +++ b/model/lib/extensions/session_extension.dart @@ -67,6 +67,8 @@ extension SessionExtension on Session { throw NotFoundPersonalAccountException(); } + AccountId get accountId => personalAccount.accountId; + ({ bool isAvailable, CalendarEventCapability? calendarEventCapability diff --git a/model/lib/oidc/token_oidc.dart b/model/lib/oidc/token_oidc.dart index 05afa3448e..50fd2798fb 100644 --- a/model/lib/oidc/token_oidc.dart +++ b/model/lib/oidc/token_oidc.dart @@ -1,5 +1,4 @@ -import 'package:core/utils/app_logger.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:model/oidc/converter/token_id_converter.dart'; @@ -40,8 +39,6 @@ extension TokenOIDCExtension on TokenOIDC { bool get isExpired { if (expiredTime != null) { final now = DateTime.now(); - log('TokenOIDC::isExpired(): TIME_NOW: $now'); - log('TokenOIDC::isExpired(): EXPIRED_DATE: $expiredTime'); return expiredTime!.isBefore(now); } return false; diff --git a/test/features/home/presentation/home_controller_test.dart b/test/features/home/presentation/home_controller_test.dart index fff4bf85f7..eea721ebfb 100644 --- a/test/features/home/presentation/home_controller_test.dart +++ b/test/features/home/presentation/home_controller_test.dart @@ -23,7 +23,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_ import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; @@ -49,7 +49,7 @@ import 'home_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -66,7 +66,7 @@ void main() { late MockGetSessionInteractor mockGetSessionInteractor; late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor; - late MockUpdateAuthenticationAccountInteractor mockUpdateAuthenticationAccountInteractor; + late MockUpdateAccountCacheInteractor mockUpdateAccountCacheInteractor; late MockCachingManager mockCachingManager; late MockLanguageCacheManager mockLanguageCacheManager; @@ -91,7 +91,7 @@ void main() { // mock reloadable controller mockGetSessionInteractor = MockGetSessionInteractor(); mockGetAuthenticatedAccountInteractor = MockGetAuthenticatedAccountInteractor(); - mockUpdateAuthenticationAccountInteractor = MockUpdateAuthenticationAccountInteractor(); + mockUpdateAccountCacheInteractor = MockUpdateAccountCacheInteractor(); //mock base controller mockCachingManager = MockCachingManager(); @@ -109,7 +109,7 @@ void main() { Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); - Get.put(mockUpdateAuthenticationAccountInteractor); + Get.put(mockUpdateAccountCacheInteractor); Get.put(mockCachingManager); Get.put(mockLanguageCacheManager); diff --git a/test/features/login/presentation/login_controller_test.dart b/test/features/login/presentation/login_controller_test.dart index 0c9750e576..a29d5aecb6 100644 --- a/test/features/login/presentation/login_controller_test.dart +++ b/test/features/login/presentation/login_controller_test.dart @@ -28,7 +28,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; @@ -63,7 +63,7 @@ import 'login_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -84,7 +84,7 @@ void main() { late MockDNSLookupToGetJmapUrlInteractor mockDNSLookupToGetJmapUrlInteractor; late MockGetSessionInteractor mockGetSessionInteractor; late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor; - late MockUpdateAuthenticationAccountInteractor mockUpdateAuthenticationAccountInteractor; + late MockUpdateAccountCacheInteractor mockUpdateAccountCacheInteractor; late CachingManager mockCachingManager; late LanguageCacheManager mockLanguageCacheManager; late MockAuthorizationInterceptors mockAuthorizationInterceptors; @@ -119,7 +119,7 @@ void main() { // mock reloadable controller mockGetSessionInteractor = MockGetSessionInteractor(); mockGetAuthenticatedAccountInteractor = MockGetAuthenticatedAccountInteractor(); - mockUpdateAuthenticationAccountInteractor = MockUpdateAuthenticationAccountInteractor(); + mockUpdateAccountCacheInteractor = MockUpdateAccountCacheInteractor(); //mock base controller mockCachingManager = MockCachingManager(); @@ -137,7 +137,7 @@ void main() { Get.put(mockGetSessionInteractor); Get.put(mockGetAuthenticatedAccountInteractor); - Get.put(mockUpdateAuthenticationAccountInteractor); + Get.put(mockUpdateAccountCacheInteractor); Get.put(mockCachingManager); Get.put(mockLanguageCacheManager); Get.put(mockAuthorizationInterceptors); diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index dd1cc34f96..a79febbd47 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -34,7 +34,7 @@ import 'package:tmail_ui_user/features/login/data/network/interceptors/authoriza import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; @@ -138,7 +138,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), - MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -234,7 +234,7 @@ void main() { // mock reloadable controller Get dependencies final getSessionInteractor = MockGetSessionInteractor(); final getAuthenticatedAccountInteractor = MockGetAuthenticatedAccountInteractor(); - final updateAuthenticationAccountInteractor = MockUpdateAuthenticationAccountInteractor(); + final updateAccountCacheInteractor = MockUpdateAccountCacheInteractor(); // mock mailbox controller direct dependencies final createNewMailboxInteractor = MockCreateNewMailboxInteractor(); @@ -299,7 +299,7 @@ void main() { Get.put(applicationManager); Get.put(getSessionInteractor); Get.put(getAuthenticatedAccountInteractor); - Get.put(updateAuthenticationAccountInteractor); + Get.put(updateAccountCacheInteractor); Get.put(getAllIdentitiesInteractor); Get.put(removeComposerCacheOnWebInteractor); From 20c1f1c295a8a9bf3a761bc07c876579aedee9f7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 16 Jul 2024 09:40:12 +0700 Subject: [PATCH 11/11] TF-2871 Block notification on folders `Sent`, `Outbox`, `Drafts`, `Spam` and `Trash` --- ios/Podfile.lock | 2 +- ios/TwakeCore/Jmap/JmapClient.swift | 18 ++++- ios/TwakeCore/Jmap/Model/Email/Email.swift | 1 + ios/TwakeCore/Jmap/Utils/JmapConstants.swift | 3 +- .../Model/KeychainSharingSession.swift | 7 +- ios/TwakeMailNSE/NotificationService.swift | 66 ++++++++++++------- .../reloadable/reloadable_controller.dart | 60 ++++++++++++----- .../authorization_interceptors.dart | 4 +- .../update_authentication_account_state.dart | 11 +++- .../update_account_cache_interactor.dart | 40 +++++------ .../presentation/mailbox_controller.dart | 30 ++++++++- .../mailbox_dashboard_controller.dart | 2 - .../keychain_sharing_session_extension.dart | 6 +- .../keychain/keychain_sharing_session.dart | 6 ++ .../credential/credential_bindings.dart | 4 +- .../bindings/network/network_bindings.dart | 2 + lib/main/utils/ios_sharing_manager.dart | 55 +++++++++++++++- model/lib/extensions/mailbox_extension.dart | 12 ++++ model/lib/mailbox/presentation_mailbox.dart | 2 +- model/lib/oidc/token_oidc.dart | 3 + 20 files changed, 253 insertions(+), 81 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fd2a6e5260..42d69184a5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -323,7 +323,7 @@ SPEC CHECKSUMS: FirebaseInstallations: 558b1da7d65afeb996fd5c814332f013234ece4e FirebaseMessaging: e345b219fd15d325f0cf2fef28cb8ce00d851b3f fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39 flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e diff --git a/ios/TwakeCore/Jmap/JmapClient.swift b/ios/TwakeCore/Jmap/JmapClient.swift index b7dd4edb86..b2c9173b9e 100644 --- a/ios/TwakeCore/Jmap/JmapClient.swift +++ b/ios/TwakeCore/Jmap/JmapClient.swift @@ -68,6 +68,11 @@ class JmapClient { accountId: String, onComplete: @escaping ([Email], [Error]) -> Void) { guard hasMoreChanges, let sinceState = currentSinceState else { + if !self.totalListEmails.isEmpty { + let sortedListEmails = self.sortListEmails(currentListEmails: self.totalListEmails) + self.totalListEmails = sortedListEmails + } + return onComplete(self.totalListEmails, self.listErrors) } @@ -85,8 +90,7 @@ class JmapClient { if let response = data.parsing(methodName: JmapConstants.EMAIL_GET_METHOD_NAME, methodCallId: "c1") { if let listEmail = response.list, !listEmail.isEmpty { - let sortedListEmails = self.sortListEmails(currentListEmails: listEmail) - self.totalListEmails.append(contentsOf: sortedListEmails) + self.totalListEmails.append(contentsOf: listEmail) } self.hasMoreChanges = response.hasMoreChanges ?? false self.currentSinceState = response.newState @@ -97,6 +101,11 @@ class JmapClient { self.hasMoreChanges = false self.currentSinceState = nil + if !self.totalListEmails.isEmpty { + let sortedListEmails = self.sortListEmails(currentListEmails: self.totalListEmails) + self.totalListEmails = sortedListEmails + } + onComplete(self.totalListEmails, self.listErrors) } }, @@ -105,6 +114,11 @@ class JmapClient { self.hasMoreChanges = false self.currentSinceState = nil + if !self.totalListEmails.isEmpty { + let sortedListEmails = self.sortListEmails(currentListEmails: self.totalListEmails) + self.totalListEmails = sortedListEmails + } + onComplete(self.totalListEmails, self.listErrors) } ) diff --git a/ios/TwakeCore/Jmap/Model/Email/Email.swift b/ios/TwakeCore/Jmap/Model/Email/Email.swift index b546c41873..36aa6cf683 100644 --- a/ios/TwakeCore/Jmap/Model/Email/Email.swift +++ b/ios/TwakeCore/Jmap/Model/Email/Email.swift @@ -6,6 +6,7 @@ struct Email: Codable { let preview: String? let from: [EmailAddress]? let receivedAt: String? + let mailboxIds: [String: Bool]? func getSenderName() -> String? { if (from == nil || from?.isEmpty == true) { diff --git a/ios/TwakeCore/Jmap/Utils/JmapConstants.swift b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift index 05d8677984..26f3a5b80a 100644 --- a/ios/TwakeCore/Jmap/Utils/JmapConstants.swift +++ b/ios/TwakeCore/Jmap/Utils/JmapConstants.swift @@ -14,7 +14,8 @@ class JmapConstants { "subject", "preview", "from", - "receivedAt" + "receivedAt", + "mailboxIds" ] static let EMAIL_ID = "email_id" diff --git a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift index 4567d9cc5d..4ac23ce332 100644 --- a/ios/TwakeMailNSE/Model/KeychainSharingSession.swift +++ b/ios/TwakeMailNSE/Model/KeychainSharingSession.swift @@ -11,6 +11,7 @@ struct KeychainSharingSession: Codable { let basicAuth: String? let tokenEndpoint: String? let oidcScopes: [String]? + let mailboxIdsBlockNotification: [String]? } extension KeychainSharingSession { @@ -25,7 +26,8 @@ extension KeychainSharingSession { tokenOIDC: self.tokenOIDC, basicAuth: self.basicAuth, tokenEndpoint: self.tokenEndpoint, - oidcScopes: self.oidcScopes + oidcScopes: self.oidcScopes, + mailboxIdsBlockNotification: self.mailboxIdsBlockNotification ) } @@ -45,7 +47,8 @@ extension KeychainSharingSession { ), basicAuth: self.basicAuth, tokenEndpoint: self.tokenEndpoint, - oidcScopes: self.oidcScopes + oidcScopes: self.oidcScopes, + mailboxIdsBlockNotification: self.mailboxIdsBlockNotification ) } diff --git a/ios/TwakeMailNSE/NotificationService.swift b/ios/TwakeMailNSE/NotificationService.swift index 7f4f360d1d..e61f93521a 100644 --- a/ios/TwakeMailNSE/NotificationService.swift +++ b/ios/TwakeMailNSE/NotificationService.swift @@ -85,31 +85,15 @@ class NotificationService: UNNotificationServiceExtension { newEmailDeliveryState: newEmailDeliveryState ) - if (emails.count > 1) { - for email in emails { - if (email.id == emails.last?.id) { - self.showModifiedNotification(title: email.getSenderName(), - subtitle: email.subject, - body: email.preview, - badgeCount: emails.count, - userInfo: [JmapConstants.EMAIL_ID : email.id]) - return self.notify() - } else { - self.showNewNotification(title: email.getSenderName(), - subtitle: email.subject, - body: email.preview, - badgeCount: emails.count, - notificationId: email.id, - userInfo: [JmapConstants.EMAIL_ID : email.id]) - } - } + let mailboxIdsBlockNotification = keychainSharingSession.mailboxIdsBlockNotification ?? [] + + if (mailboxIdsBlockNotification.isEmpty) { + return self.showListNotification(emails: emails) } else { - self.showModifiedNotification(title: emails.first!.getSenderName(), - subtitle: emails.first!.subject, - body: emails.first!.preview, - badgeCount: 1, - userInfo: [JmapConstants.EMAIL_ID : emails.first!.id]) - return self.notify() + let emailFiltered = self.filterEmailsToPushNotification( + emails: emails, + mailboxIdsBlockNotification: mailboxIdsBlockNotification) + return self.showListNotification(emails: emailFiltered) } } } catch { @@ -121,7 +105,39 @@ class NotificationService: UNNotificationServiceExtension { ) } } - + + private func filterEmailsToPushNotification(emails: [Email], mailboxIdsBlockNotification: [String]) -> [Email] { + return emails.filter { email in + guard let mailboxIds = email.mailboxIds else { return true } + for id in mailboxIds.keys { + if mailboxIdsBlockNotification.contains(id) { + return false + } + } + return true + } + } + + private func showListNotification(emails: [Email]) { + for email in emails { + if (email.id == emails.last?.id) { + self.showModifiedNotification(title: email.getSenderName(), + subtitle: email.subject, + body: email.preview, + badgeCount: emails.count, + userInfo: [JmapConstants.EMAIL_ID : email.id]) + return self.notify() + } else { + self.showNewNotification(title: email.getSenderName(), + subtitle: email.subject, + body: email.preview, + badgeCount: emails.count, + notificationId: email.id, + userInfo: [JmapConstants.EMAIL_ID : email.id]) + } + } + } + private func showDefaultNotification(message: String) { self.modifiedContent?.title = InfoPlistReader(bundle: .app).bundleDisplayName self.modifiedContent?.body = message diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 95f35d6a1f..040c8d3d6e 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -34,13 +34,17 @@ abstract class ReloadableController extends BaseController { void handleFailureViewState(Failure failure) { if (failure is GetCredentialFailure || failure is GetStoredTokenOidcFailure || - failure is GetAuthenticatedAccountFailure || - failure is UpdateAccountCacheFailure) { + failure is GetAuthenticatedAccountFailure) { logError('$runtimeType::handleFailureViewState():Failure = $failure'); goToLogin(); } else if (failure is GetSessionFailure) { logError('$runtimeType::handleFailureViewState():Failure = $failure'); _handleGetSessionFailure(failure.exception); + } else if (failure is UpdateAccountCacheFailure) { + logError('$runtimeType::handleFailureViewState():Failure = $failure'); + _handleUpdateAccountCacheCompleted( + session: failure.session, + apiUrl: failure.apiUrl); } else { super.handleFailureViewState(failure); } @@ -50,25 +54,20 @@ abstract class ReloadableController extends BaseController { void handleSuccessViewState(Success success) { if (success is GetCredentialViewState) { log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); - _setDataToInterceptors( - baseUrl: success.baseUrl.origin, - userName: success.userName, - password: success.password); - getSessionAction(); + _handleGetCredentialSuccess(success); } else if (success is GetStoredTokenOidcSuccess) { log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); - _setDataToInterceptors( - baseUrl: success.baseUrl.toString(), - tokenOIDC: success.tokenOidc, - oidcConfiguration: success.oidcConfiguration); - getSessionAction(); + _handleGetStoredTokenOidcSuccess(success); } else if (success is GetSessionSuccess) { log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); - updateAccountCache(success.session); + updateAccountCache( + session: success.session, + baseUrl: dynamicUrlInterceptors.baseUrl); } else if (success is UpdateAccountCacheSuccess) { log('$runtimeType::handleSuccessViewState:Success = ${success.runtimeType}'); - dynamicUrlInterceptors.changeBaseUrl(success.apiUrl); - handleReloaded(success.session); + _handleUpdateAccountCacheCompleted( + session: success.session, + apiUrl: success.apiUrl); } else { super.handleSuccessViewState(success); } @@ -85,6 +84,27 @@ abstract class ReloadableController extends BaseController { consumeState(_getAuthenticatedAccountInteractor.execute()); } + void _handleGetCredentialSuccess(GetCredentialViewState success) { + _setDataToInterceptors( + baseUrl: success.baseUrl.origin, + userName: success.userName, + password: success.password); + getSessionAction(); + } + + void _handleGetStoredTokenOidcSuccess(GetStoredTokenOidcSuccess success) { + _setDataToInterceptors( + baseUrl: success.baseUrl.toString(), + tokenOIDC: success.tokenOidc, + oidcConfiguration: success.oidcConfiguration); + getSessionAction(); + } + + void _handleUpdateAccountCacheCompleted({required Session session, String? apiUrl}) { + dynamicUrlInterceptors.changeBaseUrl(apiUrl); + handleReloaded(session); + } + void _setDataToInterceptors({ required String baseUrl, UserName? userName, @@ -131,7 +151,13 @@ abstract class ReloadableController extends BaseController { } } - void updateAccountCache(Session session) { - consumeState(_updateAccountCacheInteractor.execute(session)); + void updateAccountCache({ + required Session session, + String? baseUrl + }) { + consumeState(_updateAccountCacheInteractor.execute( + session: session, + baseUrl: baseUrl + )); } } \ No newline at end of file diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 581b1c1aae..85b2078c3b 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -48,11 +48,11 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { _token = newToken; _configOIDC = newConfig; _authenticationType = AuthenticationType.oidc; - log('AuthorizationInterceptors::setTokenAndAuthorityOidc: TokenId = ${newToken?.tokenIdHash}'); + log('AuthorizationInterceptors::setTokenAndAuthorityOidc: INITIAL_TOKEN = ${newToken?.token} | EXPIRED_TIME = ${newToken?.expiredTime}'); } void _updateNewToken(TokenOIDC newToken) { - log('AuthorizationInterceptors::_updateNewToken: TokenId = ${newToken.tokenIdHash}'); + log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = ${newToken.token} | EXPIRED_TIME = ${newToken.expiredTime}'); _token = newToken; } diff --git a/lib/features/login/domain/state/update_authentication_account_state.dart b/lib/features/login/domain/state/update_authentication_account_state.dart index f4a2f56080..b4de9636f6 100644 --- a/lib/features/login/domain/state/update_authentication_account_state.dart +++ b/lib/features/login/domain/state/update_authentication_account_state.dart @@ -17,6 +17,15 @@ class UpdateAccountCacheSuccess extends UIState { } class UpdateAccountCacheFailure extends FeatureFailure { + final Session session; + final String apiUrl; - UpdateAccountCacheFailure(dynamic exception) : super(exception: exception); + UpdateAccountCacheFailure({ + required this.session, + required this.apiUrl, + dynamic exception, + }) : super(exception: exception); + + @override + List get props => [session, apiUrl, ...super.props]; } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/update_account_cache_interactor.dart b/lib/features/login/domain/usecases/update_account_cache_interactor.dart index b92dcb7fb7..239fb72060 100644 --- a/lib/features/login/domain/usecases/update_account_cache_interactor.dart +++ b/lib/features/login/domain/usecases/update_account_cache_interactor.dart @@ -1,39 +1,26 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:model/account/authentication_type.dart'; -import 'package:model/account/personal_account.dart'; import 'package:model/extensions/personal_account_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; class UpdateAccountCacheInteractor { final AccountRepository _accountRepository; - final CredentialRepository _credentialRepository; - UpdateAccountCacheInteractor( - this._accountRepository, - this._credentialRepository); + UpdateAccountCacheInteractor(this._accountRepository); - Stream> execute(Session session) async* { + Stream> execute({required Session session, String? baseUrl}) async* { + final apiUrl = _getQualifiedApiUrl(session: session, baseUrl: baseUrl); + log('UpdateAccountCacheInteractor::execute: ApiUrl = $apiUrl'); try{ yield Right(UpdatingAccountCache()); - final futureValue = await Future.wait([ - _credentialRepository.getBaseUrl(), - _accountRepository.getCurrentAccount(), - ], eagerError: true); - - final baseUrl = futureValue[0] as Uri; - final currentAccount = futureValue[1] as PersonalAccount; - final apiUrl = session.getQualifiedApiUrl( - baseUrl: currentAccount.authenticationType == AuthenticationType.basic - ? baseUrl.origin - : baseUrl.toString()); + final currentAccount = await _accountRepository.getCurrentAccount(); await _accountRepository.setCurrentAccount( currentAccount.fromAccount( @@ -46,7 +33,20 @@ class UpdateAccountCacheInteractor { session: session, apiUrl: apiUrl)); } catch(e) { - yield Left(UpdateAccountCacheFailure(e)); + yield Left(UpdateAccountCacheFailure( + session: session, + apiUrl: apiUrl, + exception: e + )); + } + } + + String _getQualifiedApiUrl({required Session session, String? baseUrl}) { + try { + return session.getQualifiedApiUrl(baseUrl: baseUrl); + } catch (e) { + logError('UpdateAccountCacheInteractor::_getQualifiedApiUrl:Exception = $e'); + return ''; } } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 25247f7891..8438d62549 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -84,6 +84,7 @@ import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; +import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; class MailboxController extends BaseMailboxController with MailboxActionHandlerMixin { @@ -97,6 +98,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final SubscribeMultipleMailboxInteractor _subscribeMultipleMailboxInteractor; final CreateDefaultMailboxInteractor _createDefaultMailboxInteractor; + IOSSharingManager? _iosSharingManager; + final currentSelectMode = SelectMode.INACTIVE.obs; final _activeScrollTop = RxBool(false); final _activeScrollBottom = RxBool(true); @@ -206,6 +209,9 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _handleCreateDefaultFolderIfMissing(mailboxDashBoardController.mapDefaultMailboxIdByRole); _handleDataFromNavigationRouter(); mailboxDashBoardController.getSpamReportBanner(); + if (PlatformInfo.isIOS) { + _updateMailboxIdsBlockNotificationToKeychain(success.mailboxList); + } } else if (success is RefreshChangesAllMailboxSuccess) { _selectSelectedMailboxDefault(); mailboxDashBoardController.refreshSpamReportBanner(); @@ -1115,7 +1121,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM curve: Curves.fastOutSlowIn); } - void _handleGetAllMailboxSuccess(GetAllMailboxSuccess success) async { + Future _handleGetAllMailboxSuccess(GetAllMailboxSuccess success) async { currentMailboxState = success.currentMailboxState; log('MailboxController::_handleGetAllMailboxSuccess:currentMailboxState: $currentMailboxState'); final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; @@ -1127,6 +1133,28 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _setOutboxMailbox(); } + Future _updateMailboxIdsBlockNotificationToKeychain(List mailboxes) async { + _iosSharingManager = getBinding(); + final accountId = mailboxDashBoardController.accountId.value; + if (accountId == null || _iosSharingManager == null || mailboxes.isEmpty) { + logError('MailboxController::_updateMailboxIdsBlockNotificationToKeychain: AccountId = $accountId | IosSharingManager = $_iosSharingManager | Mailboxes = ${mailboxes.length}'); + return; + } + + if (await _iosSharingManager!.isExistMailboxIdsBlockNotificationInKeyChain(accountId)) { + return; + } + + final mailboxIdsBlockNotification = mailboxes + .where((presentationMailbox) => presentationMailbox.pushNotificationDeactivated && presentationMailbox.mailboxId != null) + .map((presentationMailbox) => presentationMailbox.mailboxId!) + .toList(); + log('MailboxController::_updateMailboxIdsBlockNotificationToKeychain:MailboxIdsBlockNotification = $mailboxIdsBlockNotification'); + _iosSharingManager!.updateMailboxIdsBlockNotificationInKeyChain( + accountId: accountId, + mailboxIds: mailboxIdsBlockNotification); + } + void _handleRefreshChangesAllMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { currentMailboxState = success.currentMailboxState; log('MailboxController::_handleRefreshChangesAllMailboxSuccess:currentMailboxState: $currentMailboxState'); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index b7cbe88902..ae3e63f8a9 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -517,8 +517,6 @@ class MailboxDashBoardController extends ReloadableController { void _handleSessionFromArguments(Session session) { log('MailboxDashBoardController::_handleSession:'); - updateAccountCache(session); - _setUpComponentsFromSession(session); if (PlatformInfo.isWeb) { diff --git a/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart b/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart index 16bf658522..d2eb1bd2ae 100644 --- a/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart +++ b/lib/features/push_notification/data/extensions/keychain_sharing_session_extension.dart @@ -1,20 +1,22 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/push_notification/data/keychain/keychain_sharing_session.dart'; extension KeychainSharingSessionExtension on KeychainSharingSession { - KeychainSharingSession updating({String? emailState}) { + KeychainSharingSession updating({String? emailState, List? mailboxIdsBlockNotification}) { return KeychainSharingSession( accountId: accountId, userName: userName, authenticationType: authenticationType, apiUrl: apiUrl, - emailState: emailState ?? emailState, + emailState: emailState ?? this.emailState, emailDeliveryState: emailDeliveryState, tokenOIDC: tokenOIDC, basicAuth: basicAuth, tokenEndpoint: tokenEndpoint, oidcScopes: oidcScopes, + mailboxIdsBlockNotification: mailboxIdsBlockNotification ?? this.mailboxIdsBlockNotification, ); } } \ No newline at end of file diff --git a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart index 2847726b06..a8d8391754 100644 --- a/lib/features/push_notification/data/keychain/keychain_sharing_session.dart +++ b/lib/features/push_notification/data/keychain/keychain_sharing_session.dart @@ -1,14 +1,17 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_id_converter.dart'; import 'package:jmap_dart_client/http/converter/user_name_converter.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/oidc/token_oidc.dart'; part 'keychain_sharing_session.g.dart'; +@MailboxIdConverter() @UserNameConverter() @AccountIdConverter() @JsonSerializable(includeIfNull: false, explicitToJson: true) @@ -23,6 +26,7 @@ class KeychainSharingSession with EquatableMixin { String? basicAuth; String? tokenEndpoint; List? oidcScopes; + List? mailboxIdsBlockNotification; KeychainSharingSession({ required this.accountId, @@ -35,6 +39,7 @@ class KeychainSharingSession with EquatableMixin { this.basicAuth, this.tokenEndpoint, this.oidcScopes, + this.mailboxIdsBlockNotification, }); factory KeychainSharingSession.fromJson(Map json) => _$KeychainSharingSessionFromJson(json); @@ -53,5 +58,6 @@ class KeychainSharingSession with EquatableMixin { basicAuth, tokenEndpoint, oidcScopes, + mailboxIdsBlockNotification, ]; } \ No newline at end of file diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index 35cf854748..10e5a7e938 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -61,9 +61,7 @@ class CredentialBindings extends InteractorsBindings { Get.find(), Get.find() )); - Get.put(UpdateAccountCacheInteractor( - Get.find(), - Get.find())); + Get.put(UpdateAccountCacheInteractor(Get.find())); } @override diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index d5823d2bed..343a6e8293 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -23,6 +23,7 @@ import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; @@ -80,6 +81,7 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/main/utils/ios_sharing_manager.dart b/lib/main/utils/ios_sharing_manager.dart index 62367fb43e..aad25f60bb 100644 --- a/lib/main/utils/ios_sharing_manager.dart +++ b/lib/main/utils/ios_sharing_manager.dart @@ -4,13 +4,16 @@ import 'dart:convert'; import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/personal_account.dart'; +import 'package:model/extensions/mailbox_extension.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; import 'package:tmail_ui_user/features/push_notification/data/extensions/keychain_sharing_session_extension.dart'; @@ -24,6 +27,7 @@ class IOSSharingManager { final AuthenticationInfoCacheManager _authenticationInfoCacheManager; final OidcConfigurationCacheManager _oidcConfigurationCacheManager; final OIDCHttpClient _oidcHttpClient; + final MailboxCacheManager _mailboxCacheManager; IOSSharingManager( this._keychainSharingManager, @@ -32,6 +36,7 @@ class IOSSharingManager { this._authenticationInfoCacheManager, this._oidcConfigurationCacheManager, this._oidcHttpClient, + this._mailboxCacheManager, ); bool _validateToSaveKeychain(PersonalAccount personalAccount) { @@ -79,6 +84,10 @@ class IOSSharingManager { final tokenRecords = await _getTokenEndpointAndScopes(); + final mailboxIdsBlockNotification = await _getMailboxIdsBlockNotification( + accountId: personalAccount.accountId!, + userName: personalAccount.userName!); + final keychainSharingSession = KeychainSharingSession( accountId: personalAccount.accountId!, userName: personalAccount.userName!, @@ -90,7 +99,7 @@ class IOSSharingManager { basicAuth: credentialInfo, tokenEndpoint: tokenRecords?.tokenEndpoint, oidcScopes: tokenRecords?.scopes, - ); + mailboxIdsBlockNotification: mailboxIdsBlockNotification); await _keychainSharingManager.save(keychainSharingSession); @@ -184,4 +193,48 @@ class IOSSharingManager { final newKeychain = keychainSharingStored.updating(emailState: newEmailState); await _keychainSharingManager.save(newKeychain); } + + Future isExistMailboxIdsBlockNotificationInKeyChain(AccountId accountId) async { + try { + final keychainSharingStored = await getKeychainSharingSession(accountId); + return keychainSharingStored?.mailboxIdsBlockNotification?.isNotEmpty == true; + } catch (e) { + logError('IOSSharingManager::getMailboxIdsBlockNotificationInKeyChain:Exception = $e'); + return false; + } + } + + Future updateMailboxIdsBlockNotificationInKeyChain({ + required AccountId accountId, + required List mailboxIds + }) async { + try { + final keychainSharingStored = await getKeychainSharingSession(accountId); + if (keychainSharingStored == null) { + return; + } + final newKeychain = keychainSharingStored.updating(mailboxIdsBlockNotification: mailboxIds); + await _keychainSharingManager.save(newKeychain); + } catch (e) { + logError('IOSSharingManager::updateMailboxIdsBlockNotificationInKeyChain: Exception = $e'); + } + } + + Future?> _getMailboxIdsBlockNotification({ + required AccountId accountId, + required UserName userName, + }) async { + try { + final mailboxesCache = await _mailboxCacheManager.getAllMailbox(accountId, userName); + final listMailboxIdBlockNotification = mailboxesCache + .where((mailbox) => mailbox.pushNotificationDeactivated && mailbox.id != null) + .map((mailbox) => mailbox.id!) + .toList(); + log('IOSSharingManager::_getMailboxIdsBlockNotification(): CACHE_MAILBOX_LIST = $listMailboxIdBlockNotification'); + return listMailboxIdBlockNotification; + } catch (e) { + logError('IOSSharingManager::_getMailboxIdsBlockNotification:Exception: $e'); + return null; + } + } } \ No newline at end of file diff --git a/model/lib/extensions/mailbox_extension.dart b/model/lib/extensions/mailbox_extension.dart index a39976b584..712f84701c 100644 --- a/model/lib/extensions/mailbox_extension.dart +++ b/model/lib/extensions/mailbox_extension.dart @@ -6,6 +6,18 @@ extension MailboxExtension on Mailbox { bool hasRole() => role != null && role!.value.isNotEmpty; + bool get isSpam => role == PresentationMailbox.roleSpam; + + bool get isTrash => role == PresentationMailbox.roleTrash; + + bool get isDrafts => role == PresentationMailbox.roleDrafts; + + bool get isSent => role == PresentationMailbox.roleSent; + + bool get isOutbox => name?.name == PresentationMailbox.outboxRole || role == PresentationMailbox.roleOutbox; + + bool get pushNotificationDeactivated => isOutbox || isSent || isDrafts || isTrash || isSpam; + PresentationMailbox toPresentationMailbox() { return PresentationMailbox( id!, diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart index 4d34e9a44b..52e24c4365 100644 --- a/model/lib/mailbox/presentation_mailbox.dart +++ b/model/lib/mailbox/presentation_mailbox.dart @@ -14,7 +14,7 @@ class PresentationMailbox with EquatableMixin { static const String templatesRole= 'templates'; static const String outboxRole = 'outbox'; static const String draftsRole = 'drafts'; - static const String spamRole = 'spam'; + static const String spamRole = 'junk'; static const String archiveRole = 'archive'; static const String recoveredRole = 'restored messages'; diff --git a/model/lib/oidc/token_oidc.dart b/model/lib/oidc/token_oidc.dart index 50fd2798fb..05afa3448e 100644 --- a/model/lib/oidc/token_oidc.dart +++ b/model/lib/oidc/token_oidc.dart @@ -1,4 +1,5 @@ +import 'package:core/utils/app_logger.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:model/oidc/converter/token_id_converter.dart'; @@ -39,6 +40,8 @@ extension TokenOIDCExtension on TokenOIDC { bool get isExpired { if (expiredTime != null) { final now = DateTime.now(); + log('TokenOIDC::isExpired(): TIME_NOW: $now'); + log('TokenOIDC::isExpired(): EXPIRED_DATE: $expiredTime'); return expiredTime!.isBefore(now); } return false;