Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions lib/core/dio/app_dio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,39 @@ import 'package:on_time_front/core/constants/environment_variable.dart';
import 'package:on_time_front/core/dio/adapters/shared.dart';
import 'package:on_time_front/core/dio/interceptors/logger_interceptor.dart';
import 'package:on_time_front/core/dio/interceptors/token_interceptor.dart';
import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart';
import 'package:on_time_front/core/dio/transformers/logging_transformer.dart';
import 'package:on_time_front/data/data_sources/token_local_data_source.dart';

@Injectable(as: Dio)
class AppDio with DioMixin implements Dio {
AppDio() {
AppDio(
TokenLocalDataSource tokenLocalDataSource,
TokenSessionInvalidator sessionInvalidator,
) {
httpClientAdapter = getAdapter();
transformer = LoggingTransformer(inner: BackgroundTransformer());
options = BaseOptions(
contentType: Headers.jsonContentType,
baseUrl: EnvironmentVariable.restApiUrl,
connectTimeout: const Duration(milliseconds: 30000),
receiveTimeout: const Duration(milliseconds: 30000),
sendTimeout: const Duration(milliseconds: 30000),
receiveDataWhenStatusError: true,
followRedirects: false,
headers: {
"Accept": "application/json",
"Authorization": EnvironmentVariable.restAuthToken
});
contentType: Headers.jsonContentType,
baseUrl: EnvironmentVariable.restApiUrl,
connectTimeout: const Duration(milliseconds: 30000),
receiveTimeout: const Duration(milliseconds: 30000),
sendTimeout: const Duration(milliseconds: 30000),
receiveDataWhenStatusError: true,
followRedirects: false,
headers: {
"Accept": "application/json",
"Authorization": EnvironmentVariable.restAuthToken,
},
);

interceptors.addAll([TokenInterceptor(this), LoggerInterceptor()]);
interceptors.addAll([
TokenInterceptor(
this,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
),
LoggerInterceptor(),
]);
}
}
20 changes: 10 additions & 10 deletions lib/core/dio/interceptors/token_interceptor.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import 'package:dio/dio.dart';
import 'package:on_time_front/core/constants/endpoint.dart';
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart';
import 'package:on_time_front/core/logging/app_logger.dart';
import 'package:on_time_front/data/data_sources/token_local_data_source.dart';
import 'package:on_time_front/domain/entities/token_entity.dart';
import 'package:on_time_front/domain/repositories/user_repository.dart';

class TokenInterceptor implements InterceptorsWrapper {
static const _refreshTokenPath = '/refresh-token';
Expand All @@ -13,9 +12,14 @@ class TokenInterceptor implements InterceptorsWrapper {
'Authentication token is unavailable for a protected request';

final Dio dio;
TokenInterceptor(this.dio);
final TokenLocalDataSource tokenLocalDataSource = getIt
.get<TokenLocalDataSource>();
final TokenLocalDataSource tokenLocalDataSource;
final TokenSessionInvalidator _sessionInvalidator;

TokenInterceptor(
this.dio, {
required this.tokenLocalDataSource,
required TokenSessionInvalidator sessionInvalidator,
}) : _sessionInvalidator = sessionInvalidator;

// when accessToken is expired & having multiple requests call
// this variable to lock others request to make sure only trigger call refresh token 01 times
Expand Down Expand Up @@ -186,11 +190,7 @@ class TokenInterceptor implements InterceptorsWrapper {
}

Future<void> _signOutLocally() async {
try {
await getIt.get<UserRepository>().signOut();
} catch (_) {
await tokenLocalDataSource.deleteToken();
}
await _sessionInvalidator.signOutLocally();
}

@override
Expand Down
3 changes: 3 additions & 0 deletions lib/core/dio/interceptors/token_session_invalidator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
abstract interface class TokenSessionInvalidator {
Future<void> signOutLocally();
}
55 changes: 28 additions & 27 deletions lib/core/services/notification_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ import 'package:firebase_core/firebase_core.dart';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/logging/app_logger.dart';
import 'package:on_time_front/core/services/js_interop_service.dart';
import 'package:on_time_front/core/services/navigation_service.dart';
import 'package:on_time_front/core/services/notification_content.dart';
import 'package:on_time_front/core/services/notification_routing.dart';
import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart';
import 'package:on_time_front/data/models/fcm_token_register_request_model.dart';
import 'package:on_time_front/core/services/notification_tap_router.dart';
import 'package:on_time_front/core/services/notification_token_registrar.dart';
import 'package:on_time_front/domain/entities/alarm_entities.dart';
import 'package:on_time_front/domain/repositories/alarm_repository.dart';
import 'package:permission_handler/permission_handler.dart'
as permission_handler;
import 'package:timezone/data/latest.dart' as tz_data;
Expand All @@ -43,13 +40,18 @@ class NotificationService {
FlutterLocalNotificationsPlugin? localNotifications,
String Function()? localeProvider,
bool? isIOSOverride,
FcmTokenRegistrar? fcmTokenRegistrar,
NotificationTapRouter? notificationTapRouter,
Stream<RemoteMessage>? onMessage,
Stream<RemoteMessage>? onMessageOpenedApp,
}) : _messaging = messaging ?? FirebaseMessaging.instance,
_localNotifications =
localNotifications ?? FlutterLocalNotificationsPlugin(),
_localeProvider = localeProvider,
_isIOSOverride = isIOSOverride,
_fcmTokenRegistrar = fcmTokenRegistrar ?? const NoopFcmTokenRegistrar(),
_notificationTapRouter =
notificationTapRouter ?? const NoopNotificationTapRouter(),
_onMessage = onMessage ?? FirebaseMessaging.onMessage,
_onMessageOpenedApp =
onMessageOpenedApp ?? FirebaseMessaging.onMessageOpenedApp;
Expand All @@ -62,12 +64,17 @@ class NotificationService {
bool isFlutterLocalNotificationsInitialized = false,
bool isTimezoneInitialized = false,
bool? isIOSOverride,
FcmTokenRegistrar? fcmTokenRegistrar,
NotificationTapRouter? notificationTapRouter,
Stream<RemoteMessage>? onMessage,
Stream<RemoteMessage>? onMessageOpenedApp,
}) : _messaging = messaging,
_localNotifications = localNotifications,
_localeProvider = localeProvider,
_isIOSOverride = isIOSOverride,
_fcmTokenRegistrar = fcmTokenRegistrar ?? const NoopFcmTokenRegistrar(),
_notificationTapRouter =
notificationTapRouter ?? const NoopNotificationTapRouter(),
_onMessage = onMessage ?? FirebaseMessaging.onMessage,
_onMessageOpenedApp =
onMessageOpenedApp ?? FirebaseMessaging.onMessageOpenedApp,
Expand All @@ -84,6 +91,8 @@ class NotificationService {
final FlutterLocalNotificationsPlugin _localNotifications;
final String Function()? _localeProvider;
final bool? _isIOSOverride;
FcmTokenRegistrar _fcmTokenRegistrar;
NotificationTapRouter _notificationTapRouter;
final Stream<RemoteMessage> _onMessage;
final Stream<RemoteMessage> _onMessageOpenedApp;
bool _isFlutterLocalNotificationsInitialized = false;
Expand All @@ -110,6 +119,14 @@ class NotificationService {
}
}

void configureDelegates({
required FcmTokenRegistrar fcmTokenRegistrar,
required NotificationTapRouter notificationTapRouter,
}) {
_fcmTokenRegistrar = fcmTokenRegistrar;
_notificationTapRouter = notificationTapRouter;
}

Future<void> initialize() {
if (_isInitialized) {
return Future.value();
Expand Down Expand Up @@ -255,13 +272,7 @@ class NotificationService {

if (token != null) {
try {
final deviceId = await getIt.get<AlarmRepository>().getDeviceId();
await getIt.get<NotificationRemoteDataSource>().fcmTokenRegister(
FcmTokenRegisterRequestModel(
firebaseToken: token,
deviceId: deviceId,
),
);
await _fcmTokenRegistrar.registerToken(token);
AppLogger.debug('[FCM] FCM Token 서버 등록 완료');
} catch (e) {
AppLogger.debug('[FCM] FCM Token 서버 등록 실패: $e');
Expand All @@ -274,12 +285,9 @@ class NotificationService {
AppLogger.debug(
'[FCM] token refreshed token=${AppLogger.redactToken(newToken)}',
);
getIt.get<AlarmRepository>().getDeviceId().then((deviceId) {
getIt.get<NotificationRemoteDataSource>().fcmTokenRegister(
FcmTokenRegisterRequestModel(
firebaseToken: newToken,
deviceId: deviceId,
),
_fcmTokenRegistrar.registerToken(newToken).catchError((e) {
AppLogger.debug(
'[FCM] refreshed token server registration failed: $e',
);
});
});
Expand Down Expand Up @@ -679,18 +687,11 @@ class NotificationService {

void _handleLocalNotificationTap(String? payload) {
AppLogger.debug('[FCM] 알림 탭');
final target = notificationRouteForPayloadString(payload);
if (target != null) {
getIt.get<NavigationService>().push(target.path, extra: target.extra);
}
_notificationTapRouter.routeLocalNotificationTap(payload);
}

Future<void> _handleBackgroundMessage(RemoteMessage message) async {
AppLogger.debug('[FCM] 백그라운드 메시지 처리');

final target = notificationRouteForData(message.data);
if (target != null) {
getIt.get<NavigationService>().push(target.path, extra: target.extra);
}
_notificationTapRouter.routeRemoteNotificationData(message.data);
}
}
43 changes: 43 additions & 0 deletions lib/core/services/notification_tap_router.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/core/services/navigation_service.dart';
import 'package:on_time_front/core/services/notification_routing.dart';

abstract interface class NotificationTapRouter {
void routeLocalNotificationTap(String? payload);

void routeRemoteNotificationData(Map<dynamic, dynamic> data);
}

class NoopNotificationTapRouter implements NotificationTapRouter {
const NoopNotificationTapRouter();

@override
void routeLocalNotificationTap(String? payload) {}

@override
void routeRemoteNotificationData(Map<dynamic, dynamic> data) {}
}

@Singleton(as: NotificationTapRouter)
class NavigationNotificationTapRouter implements NotificationTapRouter {
NavigationNotificationTapRouter(this._navigationService);

final NavigationService _navigationService;

@override
void routeLocalNotificationTap(String? payload) {
final target = notificationRouteForPayloadString(payload);
_pushTarget(target);
}

@override
void routeRemoteNotificationData(Map<dynamic, dynamic> data) {
final target = notificationRouteForData(data);
_pushTarget(target);
}

void _pushTarget(NotificationRouteTarget? target) {
if (target == null) return;
_navigationService.push(target.path, extra: target.extra);
}
}
10 changes: 10 additions & 0 deletions lib/core/services/notification_token_registrar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
abstract interface class FcmTokenRegistrar {
Future<void> registerToken(String firebaseToken);
}

class NoopFcmTokenRegistrar implements FcmTokenRegistrar {
const NoopFcmTokenRegistrar();

@override
Future<void> registerToken(String firebaseToken) async {}
}
27 changes: 27 additions & 0 deletions lib/data/services/device_fcm_token_registrar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/core/services/notification_token_registrar.dart';
import 'package:on_time_front/data/data_sources/notification_remote_data_source.dart';
import 'package:on_time_front/data/models/fcm_token_register_request_model.dart';
import 'package:on_time_front/domain/repositories/alarm_repository.dart';

@Singleton(as: FcmTokenRegistrar)
class DeviceFcmTokenRegistrar implements FcmTokenRegistrar {
DeviceFcmTokenRegistrar(
this._alarmRepository,
this._notificationRemoteDataSource,
);

final AlarmRepository _alarmRepository;
final NotificationRemoteDataSource _notificationRemoteDataSource;

@override
Future<void> registerToken(String firebaseToken) async {
final deviceId = await _alarmRepository.getDeviceId();
await _notificationRemoteDataSource.fcmTokenRegister(
FcmTokenRegisterRequestModel(
firebaseToken: firebaseToken,
deviceId: deviceId,
),
);
}
}
15 changes: 15 additions & 0 deletions lib/data/services/token_local_session_invalidator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:injectable/injectable.dart';
import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.dart';
import 'package:on_time_front/data/data_sources/token_local_data_source.dart';

@Singleton(as: TokenSessionInvalidator)
class TokenLocalSessionInvalidator implements TokenSessionInvalidator {
TokenLocalSessionInvalidator(this._tokenLocalDataSource);

final TokenLocalDataSource _tokenLocalDataSource;

@override
Future<void> signOutLocally() {
return _tokenLocalDataSource.deleteToken();
}
}
11 changes: 8 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:on_time_front/core/di/di_setup.dart';
import 'package:on_time_front/core/logging/app_logger.dart';
import 'package:on_time_front/core/services/device_info_service/shared.dart';
import 'package:on_time_front/core/services/notification_service.dart';
import 'package:on_time_front/core/services/notification_tap_router.dart';
import 'package:on_time_front/core/services/notification_token_registrar.dart';
import 'package:on_time_front/firebase_options.dart';
import 'package:on_time_front/presentation/app/screens/app.dart';

Expand All @@ -13,11 +16,13 @@ void main() async {
AppLogger.configureFlutterDebugPrint();
await HardwareKeyboard.instance.syncKeyboardState().catchError((_) {});
await initializeDateFormatting();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
AppLogger.debug('[FCM Main] Firebase initialized');
configureDependencies();
NotificationService.instance.configureDelegates(
fcmTokenRegistrar: getIt.get<FcmTokenRegistrar>(),
notificationTapRouter: getIt.get<NotificationTapRouter>(),
);

AppLogger.debug(
'Device standalone mode=${DeviceInfoService.isInStandaloneMode}',
Expand Down
Loading
Loading