diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index 828e3ac15..63a212074 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 bba139562..bd7202c69 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 bf1d15d48..ccb0f1f88 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 0c13269e0..faa9d613d 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 9ee091784..305cbde93 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 1efc2d464..000000000
--- 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 4619113ec..1b7ecb780 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 f0d1bde46..254d08cda 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 51c4c58b5..0953d0dbc 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 e44350eed..9ca39a37e 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 71ecafff7..95f35d6a1 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);
- void _handleGetCredentialSuccess(GetCredentialViewState credentialViewState) {
- _setUpInterceptors(credentialViewState);
- getSessionAction();
+ if (userName != null && password != null) {
+ authorizationInterceptors.setBasicAuthorization(userName, password);
+ authorizationIsolateInterceptors.setBasicAuthorization(userName, password);
+ }
+
+ 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 c7a40a7d9..4f6758cdd 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 34d21b5db..581b1c1aa 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 c5d1931ed..f4a2f5608 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