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.
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');
}
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.
When 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.com/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.