Fix: Issue with background notifications on Android
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-06-08 22:05:22 +02:00
parent b7b2ad701f
commit 053fbeb66e
8 changed files with 290 additions and 231 deletions

View file

@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.2.31
- Fix: Issue with background notifications on Android
## 0.2.30 ## 0.2.30
- Fix: Changed minimum threshold for the user discovery to 3 - Fix: Changed minimum threshold for the user discovery to 3

View file

@ -76,7 +76,7 @@ void main() async {
unawaited(StartupGuard.markAppStartup()); unawaited(StartupGuard.markAppStartup());
var storageError = await twonlyMinimumInitialization(); var storageError = await twonlyMinimumInitialization();
await initFCMService(); await FcmNotificationService.initStartup();
var userExists = false; var userExists = false;
@ -109,6 +109,8 @@ void main() async {
unawaited(initFileDownloader()); unawaited(initFileDownloader());
if (userExists) { if (userExists) {
unawaited(FcmNotificationService.initAfterUserLoaded());
if (userService.currentUser.allowErrorTrackingViaSentry) { if (userService.currentUser.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true; AppState.allowErrorTrackingViaSentry = true;
await SentryFlutter.init( await SentryFlutter.init(

View file

@ -21,7 +21,8 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
@ -65,13 +66,15 @@ class ApiService {
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream; Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
final _connectionStateController = StreamController<bool>.broadcast(); final _connectionStateController = StreamController<bool>.broadcast();
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream; Stream<bool> get onConnectionStateUpdated =>
_connectionStateController.stream;
final _appOutdatedController = StreamController<void>.broadcast(); final _appOutdatedController = StreamController<void>.broadcast();
Stream<void> get onAppOutdated => _appOutdatedController.stream; Stream<void> get onAppOutdated => _appOutdatedController.stream;
final _newDeviceRegisteredController = StreamController<void>.broadcast(); final _newDeviceRegisteredController = StreamController<void>.broadcast();
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream; Stream<void> get onNewDeviceRegistered =>
_newDeviceRegisteredController.stream;
bool appIsOutdated = false; bool appIsOutdated = false;
bool isAuthenticated = false; bool isAuthenticated = false;
@ -80,7 +83,8 @@ class ApiService {
Timer? reconnectionTimer; Timer? reconnectionTimer;
int _reconnectionDelay = 5; int _reconnectionDelay = 5;
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests = HashMap(); final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests =
HashMap();
IOWebSocketChannel? _channel; IOWebSocketChannel? _channel;
// ignore: cancel_subscriptions // ignore: cancel_subscriptions
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription; StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
@ -112,7 +116,7 @@ class ApiService {
// Function is called after the user is authenticated at the server // Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async { Future<void> onAuthenticated() async {
await initFCMAfterAuthenticated(); await FcmNotificationService.initFCMAfterAuthenticated();
_connectionStateController.add(true); _connectionStateController.add(true);
if (AppState.isInBackgroundTask) { if (AppState.isInBackgroundTask) {
@ -418,7 +422,9 @@ class ApiService {
} }
if (res.error == ErrorCode.UserIdNotFound && contactId != null) { if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
Log.warn('Contact deleted their account $contactId.'); Log.warn('Contact deleted their account $contactId.');
final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull(); final contact = await twonlyDB.contactsDao
.getContactByUserId(contactId)
.getSingleOrNull();
if (contact != null) { if (contact != null) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
@ -483,7 +489,8 @@ class ApiService {
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { if (result.error != ErrorCode.AuthTokenNotValid &&
result.error != ErrorCode.ForegroundSessionConnected) {
Log.error( Log.error(
'got error while authenticating to the server: ${result.error}', 'got error while authenticating to the server: ${result.error}',
); );
@ -521,7 +528,8 @@ class ApiService {
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { if (result.error != ErrorCode.AuthTokenNotValid &&
result.error != ErrorCode.ForegroundSessionConnected) {
Log.error( Log.error(
'got error while authenticating to the server: ${result.error}', 'got error while authenticating to the server: ${result.error}',
); );
@ -553,7 +561,8 @@ class ApiService {
return; return;
} }
final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge(); final handshake = Handshake()
..getAuthChallenge = Handshake_GetAuthChallenge();
final req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
@ -618,7 +627,9 @@ class ApiService {
final register = Handshake_Register() final register = Handshake_Register()
..username = username ..username = username
..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize() ..publicIdentityKey = (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize()
..registrationId = Int64(signalIdentity.registrationId) ..registrationId = Int64(signalIdentity.registrationId)
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
..signedPrekeySignature = signedPreKey.signature ..signedPrekeySignature = signedPreKey.signature

View file

@ -5,6 +5,7 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -281,7 +282,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
Log.info('[$receiptId] Finished handleEncryptedMessage'); Log.info('[$receiptId] Finished handleEncryptedMessage');
if (a == null && b == null) { if (a == null && b == null) {
unawaited(updateLastServerMessageTimestamp()); unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Message was handled without any error. Show push notification to the user for Android. // Message was handled without any error. Show push notification to the user for Android.
await showPushNotificationFromServerMessages( await showPushNotificationFromServerMessages(

View file

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/log.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await FcmNotificationService.handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
}
} else {
// make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000));
}
}

View file

@ -1,5 +1,3 @@
// ignore_for_file: unreachable_from_main
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
@ -7,13 +5,11 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.background.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -21,240 +17,253 @@ import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> checkForTokenUpdates() async { class FcmNotificationService {
try { static Future<void> initStartup() async {
if (!userService.isUserCreated) return; await Firebase.initializeApp(
if (Platform.isIOS) { options: DefaultFirebaseOptions.currentPlatform,
var apnsToken = await FirebaseMessaging.instance.getAPNSToken(); );
for (var i = 0; i < 20; i++) {
if (apnsToken != null) break; FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await Future<void>.delayed(const Duration(seconds: 1));
apnsToken = await FirebaseMessaging.instance.getAPNSToken(); FirebaseMessaging.onMessage.listen(handleRemoteMessage);
} }
if (apnsToken == null) {
Log.error('Could not get APNS token even after 20s...'); static Future<void> initAfterUserLoaded() async {
unawaited(_checkForTokenUpdates());
unawaited(_checkFcmHealthAndResetIfNeeded());
}
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await _checkForTokenUpdates();
return; return;
} }
} final res = await apiService.updateFCMToken(
fcmToken,
final fcmToken = await FirebaseMessaging.instance.getToken(); );
if (fcmToken == null) { if (res.isSuccess) {
Log.error('Could not get fcm token'); Log.info('Uploaded new FCM token!');
return; await UserService.update((u) {
} u.updateFCMToken = false;
Log.info('Loaded FCM token.');
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
})
.onError((err) {
Log.error('could not listen on token refresh');
}); });
} catch (e) { } else {
Log.error('could not load fcm token: $e'); Log.error('Could not update FCM token!');
}
}
} }
}
Future<void> initFCMAfterAuthenticated({bool force = false}) async { static Future<void> resetFCMTokens() async {
final fcmToken = userService.currentUser.fcmToken; await FirebaseInstallations.instance.delete();
if (userService.currentUser.updateFCMToken || force) { Log.info('Firebase Installation successfully deleted.');
if (fcmToken == null) { await FirebaseMessaging.instance.deleteToken();
Log.error('FCM token could not be updated as it is empty'); Log.info('Old FCM deleted.');
await checkForTokenUpdates(); await UserService.update((u) => u.fcmToken = null);
await _checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
}
static Future<void> _checkForTokenUpdates() async {
try {
if (!userService.isUserCreated) {
Log.info(
'Checking for FCM token updates skipped: user is not yet created.',
);
return;
}
if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) {
if (apnsToken != null) break;
await Future<void>.delayed(const Duration(seconds: 1));
apnsToken = await FirebaseMessaging.instance.getAPNSToken();
}
if (apnsToken == null) {
Log.error('Could not get APNS token even after 20s...');
return;
}
}
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
Log.error('Could not get fcm token');
return;
}
Log.info('Loaded FCM token.');
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
}
}
FirebaseMessaging.instance.onTokenRefresh
// ignore: avoid_types_on_closure_parameters
.listen((String fcmToken) async {
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
}
})
.onError((err) {
Log.error('could not listen on token refresh');
});
} catch (e) {
Log.error('could not load fcm token: $e');
}
}
static Future<void> handleRemoteMessage(RemoteMessage message) async {
await _updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS');
}
if (message.notification != null && AppState.isAppInBackground) {
Log.error(
'Got notification but app is in background, so the SDK already have shown the message.',
);
return; return;
} }
final res = await apiService.updateFCMToken(
fcmToken, if (message.notification != null || message.data['title'] != null) {
); final title =
if (res.isSuccess) { message.notification?.title ?? message.data['title'] as String? ?? '';
Log.info('Uploaded new FCM token!'); final body =
await UserService.update((u) { message.notification?.body ?? message.data['body'] as String? ?? '';
u.updateFCMToken = false; await customLocalPushNotification(title, body);
});
} else {
Log.error('Could not update FCM token!');
} }
} }
}
Future<void> resetFCMTokens() async { static Future<void> _updateLastFcmMessageTimestamp() async {
await FirebaseInstallations.instance.delete(); const storage = FlutterSecureStorage();
Log.info('Firebase Installation successfully deleted.'); final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
await FirebaseMessaging.instance.deleteToken(); try {
Log.info('Old FCM deleted.'); await storage.write(
await UserService.update((u) => u.fcmToken = null); key: SecureStorageKeys.lastFcmMessageTimestamp,
await checkForTokenUpdates(); value: nowMs,
await initFCMAfterAuthenticated(force: true); iOptions: const IOSOptions(
} groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
Future<void> initFCMService() async { ),
await Firebase.initializeApp( );
options: DefaultFirebaseOptions.currentPlatform, Log.info('Updated last FCM message timestamp to $nowMs');
); } catch (e) {
Log.error('Could not write last FCM message timestamp: $e');
unawaited(checkForTokenUpdates());
unawaited(checkFcmHealthAndResetIfNeeded());
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
}
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
} }
} else {
// make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000));
}
}
Future<void> handleRemoteMessage(RemoteMessage message) async {
await updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS');
}
if (message.notification != null && AppState.isAppInBackground) {
Log.error(
'Got notification but app is in background, so the SDK already have shown the message.',
);
return;
} }
if (message.notification != null || message.data['title'] != null) { static Future<void> updateLastServerMessageTimestamp() async {
final title = const storage = FlutterSecureStorage();
message.notification?.title ?? message.data['title'] as String? ?? ''; final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
final body = try {
message.notification?.body ?? message.data['body'] as String? ?? ''; await storage.write(
await customLocalPushNotification(title, body); key: SecureStorageKeys.lastServerMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last server message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last server message timestamp: $e');
}
} }
// On Android the push notification is now shown in the server_message.dart. This ensures static Future<void> _checkFcmHealthAndResetIfNeeded() async {
// that the messages was successfully decrypted before showing the push notification if (!userService.isUserCreated) {
// else if (message.data['push_data'] != null) { Log.info('FCM health check skipped: user is not yet created.');
// await handlePushData(message.data['push_data'] as String); return;
// } }
} const storage = FlutterSecureStorage();
try {
final lastFcmStr = await storage.read(
key: SecureStorageKeys.lastFcmMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final lastServerStr = await storage.read(
key: SecureStorageKeys.lastServerMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Future<void> updateLastFcmMessageTimestamp() async { final now = DateTime.now();
const storage = FlutterSecureStorage(); final threeDaysAgo = now.subtract(const Duration(days: 3));
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
try {
await storage.write(
key: SecureStorageKeys.lastFcmMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last FCM message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last FCM message timestamp: $e');
}
}
Future<void> updateLastServerMessageTimestamp() async { DateTime? lastFcmTime;
const storage = FlutterSecureStorage(); if (lastFcmStr != null) {
final nowMs = DateTime.now().millisecondsSinceEpoch.toString(); final ms = int.tryParse(lastFcmStr);
try { if (ms != null) {
await storage.write( lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
key: SecureStorageKeys.lastServerMessageTimestamp, }
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last server message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last server message timestamp: $e');
}
}
Future<void> checkFcmHealthAndResetIfNeeded() async {
if (!userService.isUserCreated) return;
const storage = FlutterSecureStorage();
try {
final lastFcmStr = await storage.read(
key: SecureStorageKeys.lastFcmMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final lastServerStr = await storage.read(
key: SecureStorageKeys.lastServerMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final now = DateTime.now();
final threeDaysAgo = now.subtract(const Duration(days: 3));
DateTime? lastFcmTime;
if (lastFcmStr != null) {
final ms = int.tryParse(lastFcmStr);
if (ms != null) {
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
} }
}
if (lastFcmTime != null) { if (lastFcmTime != null) {
Log.info('Last message received via FCM messaging system: $lastFcmTime'); Log.info(
} else { 'Last message received via FCM messaging system: $lastFcmTime',
Log.info('No record of a message received via FCM messaging system.'); );
} } else {
Log.info('No record of a message received via FCM messaging system.');
DateTime? lastServerTime;
if (lastServerStr != null) {
final ms = int.tryParse(lastServerStr);
if (ms != null) {
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
} }
}
// Check conditions: DateTime? lastServerTime;
// 1. No messages received via FCM in the last 3 days (either null or older than 3 days) if (lastServerStr != null) {
final fcmInactive = lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo); final ms = int.tryParse(lastServerStr);
// 2. Server message received within the last 3 days if (ms != null) {
final serverActive = lastServerTime != null && lastServerTime.isAfter(threeDaysAgo); lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
}
}
if (fcmInactive && serverActive) { final fcmInactive =
Log.warn('FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...'); lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
await resetFCMTokens(); final serverActive =
} else { lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
Log.info('FCM check passed. No reset needed.');
if (fcmInactive && serverActive) {
Log.warn(
'FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...',
);
await resetFCMTokens();
} else {
Log.info('FCM check passed. No reset needed.');
}
} catch (e) {
Log.error('Error during FCM health check: $e');
} }
} catch (e) {
Log.error('Error during FCM health check: $e');
} }
} }

View file

@ -10,6 +10,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -163,6 +164,8 @@ class _RegisterViewState extends State<RegisterView> {
await UserService.save(userData); await UserService.save(userData);
unawaited(FcmNotificationService.initAfterUserLoaded());
await apiService.authenticate(); await apiService.authenticate();
widget.callbackOnSuccess(); widget.callbackOnSuccess();
} catch (e, stack) { } catch (e, stack) {

View file

@ -45,7 +45,7 @@ class _NotificationViewState extends State<NotificationView> {
_isLoadingTroubleshooting = true; _isLoadingTroubleshooting = true;
}); });
await initFCMAfterAuthenticated(force: true); await FcmNotificationService.initFCMAfterAuthenticated(force: true);
await setupNotificationWithUsers(force: true); await setupNotificationWithUsers(force: true);
@ -90,7 +90,7 @@ class _NotificationViewState extends State<NotificationView> {
setState(() { setState(() {
_isLoadingReset = true; _isLoadingReset = true;
}); });
await resetFCMTokens(); await FcmNotificationService.resetFCMTokens();
if (!mounted) return; if (!mounted) return;
await showAlertDialog( await showAlertDialog(
context, context,