Skip to content

Commit 2bd03f9

Browse files
committed
refactor(notifications): improve device registration idempotency and cleanup
- Replace simple device ID with UUID for better uniqueness - Implement proactive cleanup of existing devices for a user - Log warnings instead of throwing errors during cleanup failures - Update comments to reflect new logic and error handling
1 parent bde96de commit 2bd03f9

File tree

1 file changed

+32
-17
lines changed

1 file changed

+32
-17
lines changed

lib/notifications/services/firebase_push_notification_service.dart

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:data_repository/data_repository.dart';
66
import 'package:firebase_messaging/firebase_messaging.dart';
77
import 'package:flutter_news_app_mobile_client_full_source_code/notifications/services/push_notification_service.dart';
88
import 'package:logging/logging.dart';
9+
import 'package:uuid/uuid.dart';
910

1011
/// A concrete implementation of [PushNotificationService] for Firebase Cloud
1112
/// Messaging (FCM).
@@ -115,35 +116,50 @@ class FirebasePushNotificationService implements PushNotificationService {
115116
}
116117

117118
_logger.fine('FCM token received for registration: $token');
118-
// The device ID is now a composite key of userId and provider name to
119-
// ensure idempotency and align with the backend's delete-then-create
120-
// pattern.
121-
final deviceId = '${userId}_${PushNotificationProvider.firebase.name}';
122-
123-
// First, attempt to delete any existing device registration for this user
124-
// and provider. This ensures a clean state and handles token updates
125-
// by effectively performing a "delete-then-create".
119+
120+
// To ensure a user only receives notifications on their most recently
121+
// used device, we proactively clear all of their previous device
122+
// registrations before creating a new one. This prevents "ghost"
123+
// notifications from being sent to old, unused installations (e.g.,
124+
// after a user gets a new phone or reinstalls the app).
126125
try {
127-
await _pushNotificationDeviceRepository.delete(id: deviceId);
128-
_logger.info('Existing device registration deleted for $deviceId.');
129-
} on NotFoundException {
130-
_logger.info(
131-
'No existing device registration found for $deviceId. Proceeding with creation.',
126+
final existingDevices = await _pushNotificationDeviceRepository.readAll(
127+
userId: userId,
132128
);
129+
130+
if (existingDevices.items.isNotEmpty) {
131+
_logger.info(
132+
'Found ${existingDevices.items.length} existing device(s) for user $userId. Deleting...',
133+
);
134+
await Future.wait(
135+
existingDevices.items.map(
136+
(device) => _pushNotificationDeviceRepository.delete(
137+
id: device.id,
138+
userId: userId,
139+
),
140+
),
141+
);
142+
_logger.info('All existing devices for user $userId deleted.');
143+
}
133144
} catch (e, s) {
145+
// If the proactive cleanup fails (e.g., due to a temporary network
146+
// issue), we log the error but do not halt the registration process.
147+
// The backend's passive, self-healing mechanism (which prunes invalid
148+
// tokens upon send failure) will eventually clean up any orphaned
149+
// device records. This ensures that a failure in cleanup does not
150+
// prevent the user from receiving notifications on their new device.
134151
_logger.warning(
135-
'Failed to delete existing device registration for $deviceId. Proceeding with creation anyway. Error: $e',
152+
'Could not clean up existing devices for user $userId, proceeding with registration. Error: $e',
136153
e,
137154
s,
138155
);
139156
}
140157

141158
final newDevice = PushNotificationDevice(
142-
id: deviceId,
159+
id: const Uuid().v4(),
143160
userId: userId,
144161
platform: Platform.isIOS ? DevicePlatform.ios : DevicePlatform.android,
145162
providerTokens: {PushNotificationProvider.firebase: token},
146-
// Timestamps are managed by the backend, but we provide initial values.
147163
createdAt: DateTime.now(),
148164
updatedAt: DateTime.now(),
149165
);
@@ -152,7 +168,6 @@ class FirebasePushNotificationService implements PushNotificationService {
152168
_logger.info('Device successfully registered with backend.');
153169
} catch (e, s) {
154170
_logger.severe('Failed to register device.', e, s);
155-
// Re-throwing allows the caller (e.g., AppBloc) to know about the failure.
156171
rethrow;
157172
}
158173
}

0 commit comments

Comments
 (0)