diff --git a/android/app/build.gradle b/android/app/build.gradle index 1eb259f16..0b4826176 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,6 +22,7 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,8 +41,8 @@ android { defaultConfig { applicationId "com.linagora.android.teammail" - minSdkVersion 16 - targetSdkVersion 31 + minSdkVersion 19 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName // The number of method references in a .dex file cannot exceed 64K @@ -51,6 +52,12 @@ android { ] } + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + // Use key information file signingConfigs { release { @@ -74,7 +81,9 @@ flutter { } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:multidex:1.0.3' implementation 'androidx.work:work-runtime-ktx:2.7.0' + implementation 'com.android.support:multidex:1.0.3' + implementation 'androidx.window:window:1.0.0' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9f003d3e9..a8ddaba77 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,12 +32,15 @@ - diff --git a/android/build.gradle b/android/build.gradle index 7603b1627..0b398e3f1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,8 +6,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.google.gms:google-services:4.3.10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.android.tools.build:gradle:4.2.2' } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afd..1b5208f24 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip diff --git a/lib/features/caching/caching_manager.dart b/lib/features/caching/caching_manager.dart index b2076cb8c..24cacf1ff 100644 --- a/lib/features/caching/caching_manager.dart +++ b/lib/features/caching/caching_manager.dart @@ -1,19 +1,19 @@ - import 'package:flutter/foundation.dart'; import 'package:tmail_ui_user/features/caching/account_cache_client.dart'; import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/firebase_config_cache_client.dart'; import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; class CachingManager { - final MailboxCacheClient _mailboxCacheClient; final StateCacheClient _stateCacheClient; final EmailCacheClient _emailCacheClient; final RecentSearchCacheClient _recentSearchCacheClient; final AccountCacheClient _accountCacheClient; + final FirebaseCacheClient _firebaseCacheClient; CachingManager( this._mailboxCacheClient, @@ -21,6 +21,7 @@ class CachingManager { this._emailCacheClient, this._recentSearchCacheClient, this._accountCacheClient, + this._firebaseCacheClient, ); Future clearAll() async { @@ -39,6 +40,7 @@ class CachingManager { _emailCacheClient.deleteBox(), _recentSearchCacheClient.deleteBox(), _accountCacheClient.deleteBox(), + _firebaseCacheClient.deleteBox(), ]); } } @@ -49,4 +51,4 @@ class CachingManager { _emailCacheClient.clearAllData(), ]); } -} \ No newline at end of file +} diff --git a/lib/features/caching/firebase_config_cache_client.dart b/lib/features/caching/firebase_config_cache_client.dart new file mode 100644 index 000000000..e45c9b060 --- /dev/null +++ b/lib/features/caching/firebase_config_cache_client.dart @@ -0,0 +1,8 @@ +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/firebase_cache.dart'; + +class FirebaseCacheClient extends HiveCacheClient { + + @override + String get tableName => 'FirebaseCache'; +} \ No newline at end of file diff --git a/lib/features/caching/utils/caching_constants.dart b/lib/features/caching/utils/caching_constants.dart index ed2f0008e..24450136b 100644 --- a/lib/features/caching/utils/caching_constants.dart +++ b/lib/features/caching/utils/caching_constants.dart @@ -13,4 +13,5 @@ class CachingConstants { static const int AUTHENTICATION_INFO_HIVE_CACHE_IDENTIFY = 11; static const int RECENT_LOGIN_URL_HIVE_CACHE_IDENTITY = 12; static const int RECENT_LOGIN_USERNAME_HIVE_CACHE_IDENTITY = 13; + static const int FIREBASE_CONFIG_CACHE_IDENTITY = 14; } \ 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 b506cd109..9e81282e3 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -124,7 +124,6 @@ class MailboxDashBoardController extends ReloadableController { final viewStateMarkAsReadMailbox = Rx>(Right(UIState.idle)); final vacationResponse = Rxn(); final routerParameters = Rxn>(); - Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; Map mapMailboxById = {}; diff --git a/lib/features/push_notification/data/datasource/firebase_datasource.dart b/lib/features/push_notification/data/datasource/firebase_datasource.dart new file mode 100644 index 000000000..a487128a6 --- /dev/null +++ b/lib/features/push_notification/data/datasource/firebase_datasource.dart @@ -0,0 +1,9 @@ +import 'package:model/firebase/firebase_dto.dart'; + +abstract class FirebaseDatasource { + Future getCurrentFirebase(); + + Future setCurrentFirebase(FirebaseDto newCurrentFirebase); + + Future deleteCurrentFirebase(String token); +} \ No newline at end of file diff --git a/lib/features/push_notification/data/datasource_impl/hive_account_datasource_impl.dart b/lib/features/push_notification/data/datasource_impl/hive_account_datasource_impl.dart new file mode 100644 index 000000000..6f5a19c35 --- /dev/null +++ b/lib/features/push_notification/data/datasource_impl/hive_account_datasource_impl.dart @@ -0,0 +1,39 @@ +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/firebase_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/firebase_cache_manager.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class HiveFirebaseDatasourceImpl extends FirebaseDatasource { + + final FirebaseCacheManager _firebaseCacheManager; + final ExceptionThrower _exceptionThrower; + + HiveFirebaseDatasourceImpl(this._firebaseCacheManager, this._exceptionThrower); + + @override + Future getCurrentFirebase() { + return Future.sync(() async { + return await _firebaseCacheManager.getFirebase(); + }).catchError((error) { + _exceptionThrower.throwException(error); + }); + } + + @override + Future setCurrentFirebase(FirebaseDto newCurrentFirebase) { + return Future.sync(() async { + return await _firebaseCacheManager.setFirebase(newCurrentFirebase); + }).catchError((error) { + _exceptionThrower.throwException(error); + }); + } + + @override + Future deleteCurrentFirebase(String token) { + return Future.sync(() async { + return await _firebaseCacheManager.deleteFirebase(token); + }).catchError((error) { + _exceptionThrower.throwException(error); + }); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/extensions/firebase_cache_extensions.dart b/lib/features/push_notification/data/extensions/firebase_cache_extensions.dart new file mode 100644 index 000000000..dd0a956b6 --- /dev/null +++ b/lib/features/push_notification/data/extensions/firebase_cache_extensions.dart @@ -0,0 +1,10 @@ + +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/firebase_cache.dart'; + +extension FirebaseCacheExtension on FirebaseCache { + + FirebaseDto toFirebaseDto() { + return FirebaseDto(token); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/extensions/firebase_extensions.dart b/lib/features/push_notification/data/extensions/firebase_extensions.dart new file mode 100644 index 000000000..93ea404b9 --- /dev/null +++ b/lib/features/push_notification/data/extensions/firebase_extensions.dart @@ -0,0 +1,8 @@ +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/firebase_cache.dart'; + +extension FirebaseExtensions on FirebaseDto { + FirebaseCache toCache() { + return FirebaseCache(token); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/local/firebase_cache_manager.dart b/lib/features/push_notification/data/local/firebase_cache_manager.dart new file mode 100644 index 000000000..227f1dd14 --- /dev/null +++ b/lib/features/push_notification/data/local/firebase_cache_manager.dart @@ -0,0 +1,37 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/caching/firebase_config_cache_client.dart'; +import 'package:tmail_ui_user/features/push_notification/data/extensions/firebase_extensions.dart'; +import 'package:tmail_ui_user/features/push_notification/data/model/firebase_cache.dart'; +import 'package:tmail_ui_user/features/push_notification/data/extensions/firebase_cache_extensions.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/firebase_exception.dart'; + +class FirebaseCacheManager { + final FirebaseCacheClient _firebaseCacheClient; + + FirebaseCacheManager(this._firebaseCacheClient); + + Future getFirebase() async { + try { + final firebase = await _firebaseCacheClient.getItem(FirebaseCache.keyCacheValue); + if(firebase != null ) { + return firebase.toFirebaseDto(); + } else { + throw NotFoundStoredFirebaseException(); + } + } catch (e) { + logError('FirebaseCacheManager::getFirebase(): $e'); + throw NotFoundStoredFirebaseException(); + } + } + + Future setFirebase(FirebaseDto firebaseDto) { + log('FirebaseCacheManager::setFirebase(): $_firebaseCacheClient'); + return _firebaseCacheClient.insertItem(firebaseDto.token, firebaseDto.toCache()); + } + + Future deleteFirebase(String token) { + log('FirebaseCacheManager::deleteSelectedFirebase(): $token'); + return _firebaseCacheClient.deleteItem(token); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/model/firebase_cache.dart b/lib/features/push_notification/data/model/firebase_cache.dart new file mode 100644 index 000000000..2a5a8260b --- /dev/null +++ b/lib/features/push_notification/data/model/firebase_cache.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +part 'firebase_cache.g.dart'; + +@HiveType(typeId: CachingConstants.FIREBASE_CONFIG_CACHE_IDENTITY) +class FirebaseCache extends HiveObject with EquatableMixin { + + static const String keyCacheValue = 'firebaseCache'; + + @HiveField(0) + final String token; + + FirebaseCache(this.token); + + @override + List get props => [token]; +} \ No newline at end of file diff --git a/lib/features/push_notification/data/repository/firebase_repository_impl.dart b/lib/features/push_notification/data/repository/firebase_repository_impl.dart new file mode 100644 index 000000000..737acc1c5 --- /dev/null +++ b/lib/features/push_notification/data/repository/firebase_repository_impl.dart @@ -0,0 +1,27 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/firebase_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/firebase_repository.dart'; + +class FirebaseRepositoryImpl extends FirebaseRepository { + + final FirebaseDatasource _firebaseDatasource; + + FirebaseRepositoryImpl(this._firebaseDatasource); + + @override + Future getCurrentFirebase() { + return _firebaseDatasource.getCurrentFirebase(); + } + + @override + Future setCurrentFirebase(FirebaseDto newCurrentFirebase) { + log('FirebaseRepositoryImpl::setCurrentFirebase(): $newCurrentFirebase'); + return _firebaseDatasource.setCurrentFirebase(newCurrentFirebase); + } + + @override + Future deleteCurrentFirebase(String token) { + return _firebaseDatasource.deleteCurrentFirebase(token); + } +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/exceptions/firebase_exception.dart b/lib/features/push_notification/domain/exceptions/firebase_exception.dart new file mode 100644 index 000000000..8ad5393f9 --- /dev/null +++ b/lib/features/push_notification/domain/exceptions/firebase_exception.dart @@ -0,0 +1,2 @@ +class NotFoundStoredFirebaseException implements Exception {} + diff --git a/lib/features/push_notification/domain/repository/firebase_repository.dart b/lib/features/push_notification/domain/repository/firebase_repository.dart new file mode 100644 index 000000000..52e4a1dac --- /dev/null +++ b/lib/features/push_notification/domain/repository/firebase_repository.dart @@ -0,0 +1,9 @@ +import 'package:model/firebase/firebase_dto.dart'; + +abstract class FirebaseRepository { + Future getCurrentFirebase(); + + Future setCurrentFirebase(FirebaseDto newCurrentFirebase); + + Future deleteCurrentFirebase(String token); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/delete_firebase_state.dart b/lib/features/push_notification/domain/state/delete_firebase_state.dart new file mode 100644 index 000000000..6c1c6d1d9 --- /dev/null +++ b/lib/features/push_notification/domain/state/delete_firebase_state.dart @@ -0,0 +1,19 @@ +import 'package:core/core.dart'; + +class DeleteFirebaseSuccess extends UIState { + + + DeleteFirebaseSuccess(); + + @override + List get props => []; +} + +class DeleteFirebaseFailure extends FeatureFailure { + final dynamic exception; + + DeleteFirebaseFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_firebase_state.dart b/lib/features/push_notification/domain/state/get_firebase_state.dart new file mode 100644 index 000000000..9978729bc --- /dev/null +++ b/lib/features/push_notification/domain/state/get_firebase_state.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:model/firebase/firebase_dto.dart'; + +class GetFirebaseSuccess extends UIState { + + final FirebaseDto firebaseDto; + + GetFirebaseSuccess(this.firebaseDto); + + @override + List get props => [firebaseDto]; +} + +class GetFirebaseFailure extends FeatureFailure { + final dynamic exception; + + GetFirebaseFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/save_firebase_state.dart b/lib/features/push_notification/domain/state/save_firebase_state.dart new file mode 100644 index 000000000..740395e8f --- /dev/null +++ b/lib/features/push_notification/domain/state/save_firebase_state.dart @@ -0,0 +1,19 @@ +import 'package:core/core.dart'; + +class SaveFirebaseSuccess extends UIState { + + + SaveFirebaseSuccess(); + + @override + List get props => []; +} + +class SaveFirebaseFailure extends FeatureFailure { + final dynamic exception; + + SaveFirebaseFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/delete_firebase_cache_interactor.dart b/lib/features/push_notification/domain/usecases/delete_firebase_cache_interactor.dart new file mode 100644 index 000000000..447e29cf4 --- /dev/null +++ b/lib/features/push_notification/domain/usecases/delete_firebase_cache_interactor.dart @@ -0,0 +1,20 @@ +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/firebase_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/delete_firebase_state.dart'; + +class DeleteFirebaseCacheInteractor { + final FirebaseRepository _firebaseRepository; + + DeleteFirebaseCacheInteractor(this._firebaseRepository); + + Future> execute(String token) async { + try { + await _firebaseRepository.deleteCurrentFirebase(token); + return Right(DeleteFirebaseSuccess()); + } catch (e) { + logError('DeleteFirebaseCacheInteractor::execute(): $e'); + return Left(DeleteFirebaseFailure(e)); + } + } +} diff --git a/lib/features/push_notification/domain/usecases/get_firebase_cache_interactor.dart b/lib/features/push_notification/domain/usecases/get_firebase_cache_interactor.dart new file mode 100644 index 000000000..bed918af0 --- /dev/null +++ b/lib/features/push_notification/domain/usecases/get_firebase_cache_interactor.dart @@ -0,0 +1,25 @@ + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/firebase_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_firebase_state.dart'; + +class GetFirebaseCacheInteractor { + + final FirebaseRepository _firebaseRepository; + + GetFirebaseCacheInteractor(this._firebaseRepository); + + Future> execute( + OIDCConfiguration config, + String refreshToken) async { + try { + final firebase = await _firebaseRepository.getCurrentFirebase(); + return Right(GetFirebaseSuccess(firebase)); + } catch (e) { + logError('GetFirebaseCacheInteractor::execute(): $e'); + return Left(GetFirebaseFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart b/lib/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart new file mode 100644 index 000000000..619b78106 --- /dev/null +++ b/lib/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/firebase/firebase_dto.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/firebase_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/save_firebase_state.dart'; + +class SaveFirebaseCacheInteractor { + final FirebaseRepository _firebaseRepository; + + SaveFirebaseCacheInteractor(this._firebaseRepository); + + Future> execute(FirebaseDto firebaseDto) async { + try { + await _firebaseRepository.setCurrentFirebase(firebaseDto); + return Right(SaveFirebaseSuccess()); + } catch (e) { + logError('SaveFirebaseCacheInteractor::execute(): $e'); + return Left(SaveFirebaseFailure(e)); + } + } +} diff --git a/lib/features/push_notification/presentation/firebase_bindings.dart b/lib/features/push_notification/presentation/firebase_bindings.dart new file mode 100644 index 000000000..e01b83345 --- /dev/null +++ b/lib/features/push_notification/presentation/firebase_bindings.dart @@ -0,0 +1,46 @@ +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource/firebase_datasource.dart'; +import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/hive_account_datasource_impl.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/firebase_cache_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/data/repository/firebase_repository_impl.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/firebase_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_firebase_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_firebase_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; + +class FireBaseBindings extends BaseBindings { + @override + void bindingsController() {} + + @override + void bindingsDataSource() { + Get.put(Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.put(HiveFirebaseDatasourceImpl( + Get.find(), + Get.find(), + )); + } + + @override + void bindingsInteractor() { + Get.put(DeleteFirebaseCacheInteractor(Get.find())); + Get.put(SaveFirebaseCacheInteractor(Get.find())); + Get.put(GetFirebaseCacheInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.put(Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.put(FirebaseRepositoryImpl(Get.find())); + } +} diff --git a/lib/features/push_notification/presentation/notification_service.dart b/lib/features/push_notification/presentation/notification_service.dart new file mode 100644 index 000000000..c7696df13 --- /dev/null +++ b/lib/features/push_notification/presentation/notification_service.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:get/get.dart'; +import 'package:model/firebase/firebase_dto.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart'; +import 'package:tmail_ui_user/firebase_options.dart'; +import 'notification_strings.dart'; + +final StreamController selectNotificationStream = +StreamController.broadcast(); + +@pragma('vm:entry-point') +Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await NotificationService.displayPushNotification(message); +} + +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + //handle action +} + +void onDidReceiveNotificationResponse( + NotificationResponse details, + ) { + selectNotificationStream.add(details); +} + +class NotificationService { + factory NotificationService() => _instance; + + NotificationService._internal(); + + static final NotificationService _instance = NotificationService._internal(); + + static get _firebaseMessaging => FirebaseMessaging.instance; + + static get _flutterLocalNotificationsPlugin => + FlutterLocalNotificationsPlugin(); + + static final BehaviorSubject + _notificationActionStream = + BehaviorSubject.seeded(null); + + static Stream get notificationActionStream => + _notificationActionStream.stream; + + static Stream get onTokenRefresh => _firebaseMessaging.onTokenRefresh; + static bool _isFlutterLocalNotificationsInitialized = false; + + static Future initializeNotificationService( + Function(NotificationResponse)? onDidReceiveNotificationResponse, + ) async { + if (_isFlutterLocalNotificationsInitialized) return; + final _saveFirebaseCacheInteractor = Get.find(); + final token = await _firebaseMessaging.getToken(); + _saveFirebaseCacheInteractor.execute(FirebaseDto(token)); + await _initFirebaseMessaging(); + await _flutterLocalNotificationsPlugin.initialize( + InitializationSettings( + android: androidInitializationSettings, + iOS: iosInitializationSettings, + ), + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + await _requestPermissions(); + await _createNotificationChannels(); + _listenNotificationActionStream(); + _isFlutterLocalNotificationsInitialized = true; + } + + static Future displayPushNotification( + RemoteMessage notification, + ) async { + try { + await _flutterLocalNotificationsPlugin.show( + createUniqueId, + channelName, + notification.data.toString(), + pushNotificationDetails, + ); + } catch (e) { + debugPrint(e.toString()); + } + } + + static Future cancelAllNotifications() async { + await _flutterLocalNotificationsPlugin.cancelAll(); + } + + static Future _initFirebaseMessaging() async { + FirebaseMessaging.onMessage.listen(_handleIncomingForegroundNotification); + FirebaseMessaging.onMessageOpenedApp.listen(_handleOpenedAppNotification); + } + + static Future _handleIncomingForegroundNotification( + RemoteMessage remoteMessage, + ) async { + await displayPushNotification(remoteMessage); + } + + static void _handleOpenedAppNotification(RemoteMessage remoteMessage) {} + + static Future _requestPermissions() async { + if (Platform.isAndroid) { + await _flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestPermission(); + } else { + await _flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } + } + + static Future _listenNotificationActionStream() async { + selectNotificationStream.stream + .listen((event) => event) + .onData((data) async { + _notificationActionStream.add(data); + switch (data?.notificationResponseType) { + case NotificationResponseType.selectedNotification: + break; + case NotificationResponseType.selectedNotificationAction: + break; + default: + } + }); + } + + static Future _createNotificationChannels() async { + await _flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(androidChannel); + } + + static Future deleteToken() async => _firebaseMessaging.deleteToken(); +} + +int get createUniqueId => + DateTime.now().millisecondsSinceEpoch.remainder(100000); diff --git a/lib/features/push_notification/presentation/notification_strings.dart b/lib/features/push_notification/presentation/notification_strings.dart new file mode 100644 index 000000000..c952e22d0 --- /dev/null +++ b/lib/features/push_notification/presentation/notification_strings.dart @@ -0,0 +1,54 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +const channelId = 'team_mail_channel_id'; +const channelKey = 'team_mail_channel_key'; +const channelName = 'Team Mail notifications'; +const channelDescription = 'Team Mail notifications'; + +final iosInitializationSettings = DarwinInitializationSettings( + notificationCategories: [ + DarwinNotificationCategory( + channelId, + actions: [ + DarwinNotificationAction.plain( + channelKey, + channelName, + options: { + DarwinNotificationActionOption.foreground, + }, + ), + ], + ), + ], + onDidReceiveLocalNotification: + (int id, String? title, String? body, String? payload) {}, +); + +const androidInitializationSettings = +AndroidInitializationSettings('background'); + +const androidChannel = AndroidNotificationChannel( + channelId, + channelName, + description: channelDescription, + importance: Importance.max, + enableLights: true, +); + +const pushNotificationDetails = NotificationDetails( + android: AndroidNotificationDetails( + channelId, + channelName, + channelDescription: channelDescription, + visibility: NotificationVisibility.public, + importance: Importance.max, + ledOnMs: 100, + ledOffMs: 1000, + category: AndroidNotificationCategory.message, + ), + iOS: DarwinNotificationDetails( + presentSound: true, + presentAlert: true, + presentBadge: true, + ), +); \ No newline at end of file diff --git a/lib/features/session/presentation/session_controller.dart b/lib/features/session/presentation/session_controller.dart index a5d295aa3..e8663eba4 100644 --- a/lib/features/session/presentation/session_controller.dart +++ b/lib/features/session/presentation/session_controller.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/firebase/firebase_dto.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; @@ -11,6 +12,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oi 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/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/notification_service.dart'; import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; @@ -26,6 +29,7 @@ class SessionController extends ReloadableController { final CachingManager _cachingManager; final DeleteAuthorityOidcInteractor _deleteAuthorityOidcInteractor; final AuthorizationInterceptors _authorizationInterceptors; + final SaveFirebaseCacheInteractor _saveFirebaseCacheInteractor; final AppToast _appToast; final DynamicUrlInterceptors _dynamicUrlInterceptors; @@ -40,13 +44,19 @@ class SessionController extends ReloadableController { this._authorizationInterceptors, this._appToast, this._dynamicUrlInterceptors, + this._saveFirebaseCacheInteractor, ) : super(logoutOidcInteractor, deleteAuthorityOidcInteractor, getAuthenticatedAccountInteractor); @override - void onReady() { + Future onReady() async { super.onReady(); + NotificationService.initializeNotificationService( + onDidReceiveNotificationResponse); + NotificationService.onTokenRefresh.listen((token) { + _saveFirebaseCacheInteractor.execute(FirebaseDto(token)); + }); final arguments = Get.arguments; if (arguments != null && arguments is String) { _getSession(); diff --git a/lib/features/session/presentation/session_page_bindings.dart b/lib/features/session/presentation/session_page_bindings.dart index 7b58cbea5..1c0ea2b3f 100644 --- a/lib/features/session/presentation/session_page_bindings.dart +++ b/lib/features/session/presentation/session_page_bindings.dart @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_a 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/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/save_firebase_cache_interactor.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/features/session/presentation/session_controller.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -44,6 +45,7 @@ class SessionPageBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/main.dart b/lib/main.dart index 6eeb9b775..dc3c97057 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,14 @@ import 'package:core/core.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/notification_service.dart'; +import 'package:tmail_ui_user/firebase_options.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; @@ -22,6 +26,13 @@ void main() async { statusBarColor: Colors.black, statusBarIconBrightness: Brightness.light, )); + + try { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); + } on FirebaseException catch (e) { + debugPrint(e.toString()); + } await MainBindings().dependencies(); await HiveCacheConfig().setUp(); await HiveCacheConfig.initializeEncryptionKey(); diff --git a/lib/main/bindings/local/local_bindings.dart b/lib/main/bindings/local/local_bindings.dart index ae54925c8..4dba52bae 100644 --- a/lib/main/bindings/local/local_bindings.dart +++ b/lib/main/bindings/local/local_bindings.dart @@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/caching/authentication_info_cache_client. import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; import 'package:tmail_ui_user/features/caching/encryption_key_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/firebase_config_cache_client.dart'; import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/caching/recent_login_username_cache_client.dart'; @@ -22,6 +23,7 @@ import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/data/local/firebase_cache_manager.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -55,12 +57,15 @@ class LocalBindings extends Bindings { Get.put(RecentLoginUrlCacheManager((Get.find()))); Get.put(RecentLoginUsernameCacheClient()); Get.put(RecentLoginUsernameCacheManager(Get.find())); + Get.put(FirebaseCacheClient()); + Get.put(FirebaseCacheManager(Get.find())); Get.put(CachingManager( Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/main/bindings/main_bindings.dart b/lib/main/bindings/main_bindings.dart index 2035dc015..57f768c94 100644 --- a/lib/main/bindings/main_bindings.dart +++ b/lib/main/bindings/main_bindings.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/firebase_bindings.dart'; import 'package:tmail_ui_user/main/bindings/core/core_bindings.dart'; import 'package:tmail_ui_user/main/bindings/credential/credential_bindings.dart'; import 'package:tmail_ui_user/main/bindings/local/local_bindings.dart'; @@ -17,5 +18,6 @@ class MainBindings extends Bindings { CredentialBindings().dependencies(); NetWorkConnectionBindings().dependencies(); SessionBindings().dependencies(); + FireBaseBindings().dependencies(); } } \ No newline at end of file diff --git a/model/lib/firebase/firebase_dto.dart b/model/lib/firebase/firebase_dto.dart new file mode 100644 index 000000000..539543cf6 --- /dev/null +++ b/model/lib/firebase/firebase_dto.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; + +class FirebaseDto with EquatableMixin { + final String token; + + FirebaseDto(this.token); + + @override + List get props => [token]; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index bf779f69a..4e75c6bbe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "47.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.8" analyzer: dependency: transitive description: @@ -141,6 +148,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.8.5" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" code_builder: dependency: transitive description: @@ -364,9 +385,9 @@ packages: dependency: transitive description: path: "." - ref: email_supported - resolved-ref: "241d7ba620137d6534cc9bbb5fd10e182b93b94a" - url: "https://github.com/linagora/enough_html_editor.git" + ref: "feature/update_inapp_webview" + resolved-ref: a805acd03aae48997b4e9625412a02aff975747e + url: "https://github.com/ManhNTX/enough_html_editor.git" source: git version: "0.0.5" enough_platform_widgets: @@ -432,6 +453,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "14.0.4" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.6" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.6" fixnum: dependency: transitive description: @@ -524,7 +587,7 @@ packages: name: flutter_inappwebview url: "https://pub.dartlang.org" source: hosted - version: "5.4.3+8" + version: "5.7.1" flutter_keyboard_visibility: dependency: transitive description: @@ -560,6 +623,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "12.0.3+1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -998,14 +1082,14 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "9.2.0" + version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android url: "https://pub.dartlang.org" source: hosted - version: "9.0.2+1" + version: "10.2.0" permission_handler_apple: dependency: transitive description: @@ -1110,8 +1194,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: d6c954206ac885a6f269f8be06dfd914a6393e89 + ref: demo + resolved-ref: "70cea1832b0e93bce515d4641f96cd1b12366725" url: "https://github.com/linagora/rich-text-composer.git" source: git version: "0.0.1" @@ -1323,6 +1407,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.2" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d6c5746e..483d69f6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: rich_text_composer: git: url: https://github.com/linagora/rich-text-composer.git - ref: master + ref: demo # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 @@ -117,7 +117,7 @@ dependencies: device_info_plus: 4.0.2 # permission_handler - permission_handler: 9.2.0 + permission_handler: 10.2.0 # share share: 2.0.4 @@ -196,6 +196,9 @@ dependencies: ref: master url_launcher: 6.1.5 + firebase_core: ^2.2.0 + firebase_messaging: 14.0.4 + flutter_local_notifications: ^12.0.3 dev_dependencies: flutter_test: