Description
I'm using an Appwrite function triggered by a cron job to schedule daily notifications via FCM. While notifications are sent at the correct time, I'm occasionally receiving duplicate notifications on the same device.
What I've Checked
- Scheduling Logic: The scheduling function updates the existing notification when called multiple times.
- Message IDs: Each message ID is unique for the combination of target (device) and date.
- Appwrite Console: Every device has exactly one notification target, and each target is included in one notification schedule.
- App-side Code: My app isn't processing background notifications in a way that could cause duplicates.
Example Screenshot
Code
Scheduler Appwrite Function
Future<ResponseModel> setRemindersForUser(
context, User user, Messaging messaging) async {
context.log('processing user ${user.name}');
// Retrieve user's timezone offset
final timezoneOffset = user.prefs.data['timezone'];
if (timezoneOffset != null) {
context.log('retrieved timezone offset: $timezoneOffset');
// Parse timezone offset
context.log('Original timezone offset: $timezoneOffset');
final isOffsetNegative = timezoneOffset.startsWith('-');
final pureOffsetDuration =
timezoneOffset.replaceAll('-', '').split('.').first;
context.log('Pure offset duration: $pureOffsetDuration');
final offsetHour = int.parse(pureOffsetDuration.split(':')[0]);
final offsetMin = int.parse(pureOffsetDuration.split(':')[1]);
final offsetSec = int.parse(pureOffsetDuration.split(':')[2]);
final offsetDuration = Duration(
hours: offsetHour,
minutes: offsetMin,
seconds: offsetSec,
);
context.log('Offset duration: $offsetDuration');
// Calculate next 9 PM in user's local time
final now = DateTime.now().toUtc();
context.log('Current UTC time: $now');
final userTime = isOffsetNegative
? now.subtract(offsetDuration)
: now.add(offsetDuration);
context.log('Current user time: $userTime');
DateTime next9PM =
DateTime(userTime.year, userTime.month, userTime.day, 21);
if (userTime.isAfter(next9PM)) {
next9PM = next9PM.add(Duration(days: 1));
}
context.log('Next 9 PM user time: $next9PM');
// Convert next9PM to UTC
final next9PMUtc = isOffsetNegative
? next9PM.add(offsetDuration)
: next9PM.subtract(offsetDuration);
final messageId = _generateMessageId(user, next9PMUtc);
late Function(
{required String messageId,
required String title,
required String body,
List<String>? topics,
List<String>? users,
List<String>? targets,
Map? data,
String? action,
String? image,
String? icon,
String? sound,
String? color,
String? tag,
bool? draft,
String? scheduledAt}) scheduler;
try {
context.log('check existing message');
// update if message already scheduled
await messaging.getMessage(messageId: messageId);
// no error thrown means message exists
scheduler = messaging.updatePush;
context.log('found existing message');
} catch (e) {
// create if message not scheduled
context.log('no existing message found');
scheduler = messaging.createPush;
context.log('should create new message');
context.log(e.toString());
}
// Schedule push notification
context.log('scheduling push notification');
final userPushTargets = user.targets
.where((element) => element.providerType == "push")
.toList();
context.log('user push targets: ${jsonEncode(userPushTargets.map(
(e) => e.toMap(),
).toList())}');
try {
final content = dynamicNotifications.random;
final result = await scheduler(
messageId: messageId,
title: content.$1,
body: content.$2,
scheduledAt: next9PMUtc.toIso8601String(),
targets: userPushTargets
.map(
(e) => e.$id,
)
.toList(),
);
context
.log('scheduled push notification!: $messageId - ${result.toMap()}');
return ResponseModel.success(null);
} catch (e) {
context.log(e.toString());
return ResponseModel.failed(message: e.toString());
}
}
return ResponseModel.failed(message: 'Timezone offset not found');
}
Flutter App Notification Handler Code
static Future<bool> initialize() async {
try {
final result = await FirebaseMessaging.instance.requestPermission();
if (Platform.isAndroid) {
await localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(reminderChannel);
}
await localNotifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(),
),
);
_addOnReceiveMessageListener();
_addTokenRefreshListener();
return result.authorizationStatus == AuthorizationStatus.authorized;
} catch (e) {
log(e.toString());
}
return false;
}
static void _addOnReceiveMessageListener() {
FirebaseMessaging.onMessage.listen((message) {
if (message.notification != null) {
_showLocalNotification(
title: message.notification!.title ?? '',
message: message.notification!.body ?? '',
data: jsonEncode(message.data));
}
});
}
static void _showLocalNotification(
{required String title, required String message, String? data}) {
FlutterLocalNotificationsPlugin()
.show(generateTimeBasedId(), title, message,
_details,
payload: data)
.then((_) {});
}
static void _addTokenRefreshListener() {
FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
// updating fcm token on appwrite
});
}
I'm looking for insights or suggestions on what might be causing these duplicate notifications.
Description
I'm using an Appwrite function triggered by a cron job to schedule daily notifications via FCM. While notifications are sent at the correct time, I'm occasionally receiving duplicate notifications on the same device.
What I've Checked
- Scheduling Logic: The scheduling function updates the existing notification when called multiple times.
- Message IDs: Each message ID is unique for the combination of target (device) and date.
- Appwrite Console: Every device has exactly one notification target, and each target is included in one notification schedule.
- App-side Code: My app isn't processing background notifications in a way that could cause duplicates.
Example Screenshot
Code
Scheduler Appwrite Function
Future<ResponseModel> setRemindersForUser(
context, User user, Messaging messaging) async {
context.log('processing user ${user.name}');
// Retrieve user's timezone offset
final timezoneOffset = user.prefs.data['timezone'];
if (timezoneOffset != null) {
context.log('retrieved timezone offset: $timezoneOffset');
// Parse timezone offset
context.log('Original timezone offset: $timezoneOffset');
final isOffsetNegative = timezoneOffset.startsWith('-');
final pureOffsetDuration =
timezoneOffset.replaceAll('-', '').split('.').first;
context.log('Pure offset duration: $pureOffsetDuration');
final offsetHour = int.parse(pureOffsetDuration.split(':')[0]);
final offsetMin = int.parse(pureOffsetDuration.split(':')[1]);
final offsetSec = int.parse(pureOffsetDuration.split(':')[2]);
final offsetDuration = Duration(
hours: offsetHour,
minutes: offsetMin,
seconds: offsetSec,
);
context.log('Offset duration: $offsetDuration');
// Calculate next 9 PM in user's local time
final now = DateTime.now().toUtc();
context.log('Current UTC time: $now');
final userTime = isOffsetNegative
? now.subtract(offsetDuration)
: now.add(offsetDuration);
context.log('Current user time: $userTime');
DateTime next9PM =
DateTime(userTime.year, userTime.month, userTime.day, 21);
if (userTime.isAfter(next9PM)) {
next9PM = next9PM.add(Duration(days: 1));
}
context.log('Next 9 PM user time: $next9PM');
// Convert next9PM to UTC
final next9PMUtc = isOffsetNegative
? next9PM.add(offsetDuration)
: next9PM.subtract(offsetDuration);
final messageId = _generateMessageId(user, next9PMUtc);
late Function(
{required String messageId,
required String title,
required String body,
List<String>? topics,
List<String>? users,
List<String>? targets,
Map? data,
String? action,
String? image,
String? icon,
String? sound,
String? color,
String? tag,
bool? draft,
String? scheduledAt}) scheduler;
try {
context.log('check existing message');
// update if message already scheduled
await messaging.getMessage(messageId: messageId);
// no error thrown means message exists
scheduler = messaging.updatePush;
context.log('found existing message');
} catch (e) {
// create if message not scheduled
context.log('no existing message found');
scheduler = messaging.createPush;
context.log('should create new message');
context.log(e.toString());
}
// Schedule push notification
context.log('scheduling push notification');
final userPushTargets = user.targets
.where((element) => element.providerType == "push")
.toList();
context.log('user push targets: ${jsonEncode(userPushTargets.map(
(e) => e.toMap(),
).toList())}');
try {
final content = dynamicNotifications.random;
final result = await scheduler(
messageId: messageId,
title: content.$1,
body: content.$2,
scheduledAt: next9PMUtc.toIso8601String(),
targets: userPushTargets
.map(
(e) => e.$id,
)
.toList(),
);
context
.log('scheduled push notification!: $messageId - ${result.toMap()}');
return ResponseModel.success(null);
} catch (e) {
context.log(e.toString());
return ResponseModel.failed(message: e.toString());
}
}
return ResponseModel.failed(message: 'Timezone offset not found');
}
Flutter App Notification Handler Code
static Future<bool> initialize() async {
try {
final result = await FirebaseMessaging.instance.requestPermission();
if (Platform.isAndroid) {
await localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(reminderChannel);
}
await localNotifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(),
),
);
_addOnReceiveMessageListener();
_addTokenRefreshListener();
return result.authorizationStatus == AuthorizationStatus.authorized;
} catch (e) {
log(e.toString());
}
return false;
}
static void _addOnReceiveMessageListener() {
FirebaseMessaging.onMessage.listen((message) {
if (message.notification != null) {
_showLocalNotification(
title: message.notification!.title ?? '',
message: message.notification!.body ?? '',
data: jsonEncode(message.data));
}
});
}
static void _showLocalNotification(
{required String title, required String message, String? data}) {
FlutterLocalNotificationsPlugin()
.show(generateTimeBasedId(), title, message,
_details,
payload: data)
.then((_) {});
}
static void _addTokenRefreshListener() {
FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
// updating fcm token on appwrite
});
}
I'm looking for insights or suggestions on what might be causing these duplicate notifications.
Share asked Mar 10 at 8:14 Morteza MohammadiMorteza Mohammadi 3421 gold badge10 silver badges21 bronze badges 5 |1 Answer
Reset to default 2 +50When App is in the background and Notification is sent the the firebase is automatically handling it. and when you again call the show notification method two notifications are displayed. to handle this situation you can either conditionally call the show notification method you can do some changes in the push(while creating notification)
in this case don't send title and body directly instead send it in the data by doing so you're sending a "data" message from Firebase Messaging instead of sending a "notification" message. checkout the difference https://firebase.google/docs/cloud-messaging/concept-options#notifications_and_data_messages
schedule notification like this:
final result = await scheduler(
messageId: messageId,
//remove these
// title: content.$1, body: content.$2,
data: {
"title": content.$1,
"body": content.$2,
},
scheduledAt: next9PMUtc.toIso8601String(),
targets: userPushTargets
.map(
(e) => e.$id,
)
.toList(),
);
and when displaying the notification get the title
and the body
from the data field.
Difference between notification message and data message.
scheduler
– Munsif Ali Commented Mar 12 at 8:36Messaging
your define type or this is fromAppWrite
? – Munsif Ali Commented Mar 12 at 9:13final messaging = Messaging(client);
. I'm usingdart_appwrite: ^13.0.0
– Morteza Mohammadi Commented Mar 12 at 10:41