feat(notifications): register FCM token with backend post-login

Adds NotificationsRemoteDatasource in sf_shared that POSTs the current
Firebase Messaging token to /notifications/push, wired into the legacy
login flow right after getUserInfo so the device can receive targeted
push notifications.

Fire-and-forget with catchError so a failure never blocks login.
This commit is contained in:
2026-04-08 15:40:37 +02:00
parent 60558a4fcf
commit d352aec5be
7 changed files with 62 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ class LegacyLoginViewModel extends Notifier<LegacyLoginViewState>
with LoginFormValidation { with LoginFormValidation {
late final LegacyLoginRepository _repository; late final LegacyLoginRepository _repository;
late final GetUserInfoUseCase _getUserInfoUseCase; late final GetUserInfoUseCase _getUserInfoUseCase;
late final NotificationsRemoteDatasource _notifications;
late final SfTrackingRepository _tracking; late final SfTrackingRepository _tracking;
late final TextEditingController emailController; late final TextEditingController emailController;
@@ -32,6 +33,7 @@ class LegacyLoginViewModel extends Notifier<LegacyLoginViewState>
LegacyLoginViewState build() { LegacyLoginViewState build() {
_repository = ref.read(legacyLoginRepositoryProvider); _repository = ref.read(legacyLoginRepositoryProvider);
_getUserInfoUseCase = ref.read(getUserInfoUseCaseProvider); _getUserInfoUseCase = ref.read(getUserInfoUseCaseProvider);
_notifications = ref.read(notificationsRemoteDatasourceProvider);
_tracking = ref.read(sfTrackingProvider); _tracking = ref.read(sfTrackingProvider);
emailController = TextEditingController(); emailController = TextEditingController();
@@ -224,6 +226,12 @@ class LegacyLoginViewModel extends Notifier<LegacyLoginViewState>
if (!ref.mounted) return; if (!ref.mounted) return;
unawaited(
_notifications.registerCurrentPushToken().catchError((Object e) {
debugPrint('[FCM] failed to register push token post-login: $e');
}),
);
final hasDevices = await _repository.hasDevices(); final hasDevices = await _repository.hasDevices();
if (!ref.mounted) return; if (!ref.mounted) return;

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,9 @@ export 'src/providers/user_info_provider.dart';
export 'src/domain/repositories/user_repository.dart'; export 'src/domain/repositories/user_repository.dart';
export 'src/data/repositories/user_repository_impl.dart'; export 'src/data/repositories/user_repository_impl.dart';
export 'src/data/datasource/user_remote_datasource_impl.dart'; export 'src/data/datasource/user_remote_datasource_impl.dart';
export 'src/data/datasource/notifications_remote_datasource.dart';
export 'src/data/datasource/notifications_remote_datasource_impl.dart';
export 'src/providers/notifications_remote_datasource_provider.dart';
export 'src/domain/entities/beneficiary_validation_entity.dart'; export 'src/domain/entities/beneficiary_validation_entity.dart';
export 'src/domain/entities/transaction_beneficiary_entity.dart'; export 'src/domain/entities/transaction_beneficiary_entity.dart';
export 'src/data/models/payout_beneficiary_model.dart'; export 'src/data/models/payout_beneficiary_model.dart';

View File

@@ -0,0 +1,4 @@
abstract class NotificationsRemoteDatasource {
Future<void> registerCurrentPushToken();
Future<void> registerPushToken(String token);
}

View File

@@ -0,0 +1,34 @@
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'notifications_remote_datasource.dart';
class NotificationsRemoteDatasourceImpl implements NotificationsRemoteDatasource {
NotificationsRemoteDatasourceImpl(this._repository);
final SaveFamilyRepository _repository;
@override
Future<void> registerCurrentPushToken() async {
final token = await FirebaseMessaging.instance.getToken();
if (token == null || token.isEmpty) return;
await registerPushToken(token);
}
@override
Future<void> registerPushToken(String token) async {
try {
await _repository.post<Map<String, dynamic>>(
'/notifications/push',
body: <String, dynamic>{'token': token},
);
} on DioException catch (error) {
final apiMsg = error.response?.data;
final msg = apiMsg is String
? apiMsg
: (error.message ?? 'Error registering push token');
throw Exception(msg);
}
}
}

View File

@@ -0,0 +1,11 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/src/data/datasource/notifications_remote_datasource.dart';
import 'package:sf_shared/src/data/datasource/notifications_remote_datasource_impl.dart';
final notificationsRemoteDatasourceProvider =
Provider<NotificationsRemoteDatasource>((ref) {
final saveFamilyRepository = GetIt.I<SaveFamilyRepository>();
return NotificationsRemoteDatasourceImpl(saveFamilyRepository);
});

View File

@@ -29,6 +29,7 @@ dependencies:
flutter_svg: ^2.2.2 flutter_svg: ^2.2.2
flutter_riverpod: ^3.0.3 flutter_riverpod: ^3.0.3
dio: ^5.9.2 dio: ^5.9.2
firebase_messaging: ^16.1.3
freezed_annotation: ^3.1.0 freezed_annotation: ^3.1.0
freezed: ^3.2.3 freezed: ^3.2.3
json_annotation: ^4.9.0 json_annotation: ^4.9.0