diff --git a/android/app/src/main/kotlin/eu/twonly/MyApplication.kt b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt index 231e247e..9bd91360 100644 --- a/android/app/src/main/kotlin/eu/twonly/MyApplication.kt +++ b/android/app/src/main/kotlin/eu/twonly/MyApplication.kt @@ -3,10 +3,12 @@ package eu.twonly import io.flutter.app.FlutterApplication import dev.fluttercommunity.workmanager.WorkmanagerDebug import dev.fluttercommunity.workmanager.LoggingDebugHandler +import io.crates.keyring.Keyring class MyApplication : FlutterApplication() { override fun onCreate() { super.onCreate() + Keyring.initializeNdkContext(this) // This enables the internal plugin logging to Logcat WorkmanagerDebug.setCurrent(LoggingDebugHandler()) } diff --git a/lib/main.dart b/lib/main.dart index d9c5b744..d5fdf7ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -237,7 +237,6 @@ Future postStartupTasks() async { // 1. Immediate background cleanup (Non-blocking for UI) await twonlyDB.messagesDao.purgeMessageTable(); unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts()); - unawaited(UserDiscoveryService.removeDeletedContacts()); unawaited(MediaFileService.purgeTempFolder()); // 2. Service initializations @@ -245,20 +244,7 @@ Future postStartupTasks() async { unawaited(finishStartedPreprocessing()); unawaited(createPushAvatars()); - if (userService.currentUser.userDiscoveryInitializationError) { - unawaited(() async { - try { - await UserDiscoveryService.initializeOrUpdate( - threshold: userService.currentUser.userDiscoveryThreshold, - sharePromotion: userService.currentUser.userDiscoverySharePromotion, - ); - } catch (e) { - Log.error( - 'Failed to retry UserDiscovery initialization on startup: $e', - ); - } - }()); - } + unawaited(UserDiscoveryService.verifyInitializationOnStartup()); await Future.delayed(const Duration(seconds: 10)); unawaited(initializeBackgroundTaskManager()); diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 0161c1f0..2d4486e5 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -1,8 +1,11 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; @@ -177,7 +180,7 @@ class UserDiscoveryService { } } - static Future removeDeletedContacts() async { + static Future _removeDeletedContacts() async { final subquery = twonlyDB.selectOnly(twonlyDB.contacts) ..addColumns([twonlyDB.contacts.userId]) ..where(twonlyDB.contacts.accountDeleted.equals(true)); @@ -216,4 +219,35 @@ class UserDiscoveryService { u.isUserDiscoveryEnabled = false; }); } + + static Future verifyInitializationOnStartup() async { + await _removeDeletedContacts(); + final configExists = File( + '${AppEnvironment.supportDir}/user_discovery_config.json', + ).existsSync(); + final hasShares = await (twonlyDB.select( + twonlyDB.userDiscoveryShares, + )..limit(1)).get().then((list) => list.isNotEmpty); + + if (userService.currentUser.isUserDiscoveryEnabled && + (userService.currentUser.userDiscoveryInitializationError || + !configExists || + !hasShares)) { + unawaited(() async { + try { + Log.info( + 'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)', + ); + await initializeOrUpdate( + threshold: userService.currentUser.userDiscoveryThreshold, + sharePromotion: userService.currentUser.userDiscoverySharePromotion, + ); + } catch (e) { + Log.error( + 'Failed to retry UserDiscovery initialization on startup: $e', + ); + } + }()); + } + } } diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index 73a6fbf1..27e90248 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -41,6 +42,7 @@ class HomeViewState extends State { late StreamSubscription> _intentStreamSub; late StreamSubscription _deepLinkSub; + StreamSubscription? _onMessageOpenedAppSub; static final streamHomeViewPageIndex = StreamController.broadcast(); @@ -67,6 +69,13 @@ class HomeViewState extends State { streamHomeViewPageIndex.add(0); }); + _onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen(( + message, + ) { + Log.info('Opened app from iOS/Remote push notification tap.'); + streamHomeViewPageIndex.add(0); + }); + unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_initAsync()); @@ -99,10 +108,23 @@ class HomeViewState extends State { final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); + RemoteMessage? initialRemoteMessage; + try { + initialRemoteMessage = + await FirebaseMessaging.instance.getInitialMessage(); + } catch (e) { + Log.error('Could not get initial Firebase message: $e'); + } + if (widget.initialPage == 0 || + initialRemoteMessage != null || (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) { - if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + if (initialRemoteMessage != null) { + Log.info('App launched from iOS/Remote push notification tap.'); + streamHomeViewPageIndex.add(0); + } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? + false) { final payload = notificationAppLaunchDetails?.notificationResponse?.payload; if (payload != null && @@ -134,6 +156,7 @@ class HomeViewState extends State { @override void dispose() { + _onMessageOpenedAppSub?.cancel(); selectNotificationStream.close(); streamHomeViewPageIndex.close(); _disableCameraTimer?.cancel(); diff --git a/lib/src/visual/views/settings/help/contact_us.view.dart b/lib/src/visual/views/settings/help/contact_us.view.dart index fa8011c6..47916631 100644 --- a/lib/src/visual/views/settings/help/contact_us.view.dart +++ b/lib/src/visual/views/settings/help/contact_us.view.dart @@ -7,12 +7,10 @@ import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq.view.dart'; @@ -50,13 +48,6 @@ class _ContactUsState extends State { final uploadRequestBytes = uploadRequest.writeToBuffer(); - final apiAuthTokenRaw = await SecureStorage.instance.read( - key: SecureStorageKeys.apiAuthToken, - ); - if (apiAuthTokenRaw == null) { - Log.error('api auth token not defined.'); - return null; - } final apiUrl = 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';