From 5403d1324d41bff50b0e84cbc21b2604b33f8b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Gergely?= Date: Mon, 9 Feb 2026 20:29:06 +0100 Subject: [PATCH] Handle iCloud token recovery and notify Flutter Add end-to-end handling for tokens recovered from iCloud: TokenManager now detects fresh iCloud tokens and posts a "TokenRecoveredFromiCloud" Notification on iOS; WatchSessionManager observes that notification and invokes the Flutter method "onTokenRecoveredFromiCloud". On the Dart side, WatchSyncHelper handles the new callback and runs checkAndRecoverFromiCloud (clearing the reauth flag when appropriate). HomeScreen now attempts proactive iCloud recovery on iOS during startup when the token is expired/expiring or reauth is required, retrying with incremental delays and updating the client/token cache on success. Also minor log text tweak in main.dart and necessary imports added to home_screen.dart. --- firka/ios/Runner/WatchSessionManager.swift | 12 ++++++ firka/ios/Shared/API/TokenManager.swift | 26 +++++++++++++ firka/lib/helpers/watch_sync_helper.dart | 35 +++++++++++++++++ firka/lib/main.dart | 2 +- .../ui/phone/screens/home/home_screen.dart | 39 +++++++++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/firka/ios/Runner/WatchSessionManager.swift b/firka/ios/Runner/WatchSessionManager.swift index 540d928..af165b6 100644 --- a/firka/ios/Runner/WatchSessionManager.swift +++ b/firka/ios/Runner/WatchSessionManager.swift @@ -45,6 +45,18 @@ class WatchSessionManager: NSObject, WCSessionDelegate { } else { print("[WatchSessionManager] WCSession not supported on this device") } + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleTokenRecoveredFromiCloud), + name: Notification.Name("TokenRecoveredFromiCloud"), + object: nil + ) + } + + @objc private func handleTokenRecoveredFromiCloud() { + print("[WatchSessionManager] Token recovered from iCloud, notifying Flutter to clear reauth flag") + flutterChannel?.invokeMethod("onTokenRecoveredFromiCloud", arguments: nil) } private func handleSendTokenToWatch(arguments: Any?, result: @escaping FlutterResult) { diff --git a/firka/ios/Shared/API/TokenManager.swift b/firka/ios/Shared/API/TokenManager.swift index ab50b09..a39307e 100644 --- a/firka/ios/Shared/API/TokenManager.swift +++ b/firka/ios/Shared/API/TokenManager.swift @@ -48,6 +48,8 @@ class TokenManager { iCloudTokenManager.shared.observeChanges { [weak self] iCloudToken in guard let self = self else { return } + let isValidToken = iCloudToken.expiryDate > Date().addingTimeInterval(60) + if let localToken = self.loadTokenFromKeychain() { if iCloudToken.expiryDate > localToken.expiryDate { print("[TokenManager] iCloud token is fresher (\(iCloudToken.expiryDate) > \(localToken.expiryDate)), updating local cache") @@ -57,6 +59,12 @@ class TokenManager { #if os(watchOS) DataStore.shared.checkTokenState() #endif + + #if os(iOS) + if isValidToken { + self.notifyiOSTokenRecovered() + } + #endif } else { print("[TokenManager] Local token is fresher or equal, ignoring iCloud update and pushing local to iCloud") iCloudTokenManager.shared.saveToken(localToken, deviceName: self.deviceName) @@ -69,10 +77,28 @@ class TokenManager { #if os(watchOS) DataStore.shared.checkTokenState() #endif + + #if os(iOS) + if isValidToken { + self.notifyiOSTokenRecovered() + } + #endif } } } + #if os(iOS) + private func notifyiOSTokenRecovered() { + print("[TokenManager] Valid token received from iCloud, notifying Flutter to clear reauth flag") + DispatchQueue.main.async { + NotificationCenter.default.post( + name: Notification.Name("TokenRecoveredFromiCloud"), + object: nil + ) + } + } + #endif + // MARK: - File Management private func getTokenFilePath() -> URL? { guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else { diff --git a/firka/lib/helpers/watch_sync_helper.dart b/firka/lib/helpers/watch_sync_helper.dart index 34fd072..3663baf 100644 --- a/firka/lib/helpers/watch_sync_helper.dart +++ b/firka/lib/helpers/watch_sync_helper.dart @@ -34,11 +34,46 @@ class WatchSyncHelper { case 'onTokenFromWatch': debugPrint('[WatchSync] Token received from Watch'); return await _processTokenFromWatch(call.arguments); + case 'onTokenRecoveredFromiCloud': + debugPrint('[WatchSync] Token recovered from iCloud notification received'); + await _handleTokenRecoveredFromiCloud(); + return null; default: return null; } } + /// Called when iOS receives a fresh token from iCloud (e.g., Watch refreshed) + /// This clears the reauth flag if it was set, since we now have a valid token + static Future _handleTokenRecoveredFromiCloud() async { + if (!initDone) { + debugPrint('[WatchSync] Cannot handle iCloud recovery: app not initialized'); + return; + } + + try { + final recovered = await checkAndRecoverFromiCloud( + isar: initData.isar, + tokens: initData.tokens, + client: initData.client, + ); + + if (recovered) { + debugPrint('[WatchSync] Token recovered from iCloud, reauth flag cleared'); + } else { + if (initData.tokens.isNotEmpty) { + final token = initData.tokens.first; + if (token.expiryDate != null && token.expiryDate!.isAfter(DateTime.now())) { + KretaClient.clearReauthFlag(); + debugPrint('[WatchSync] Cleared reauth flag after iCloud notification (token is valid)'); + } + } + } + } catch (e) { + debugPrint('[WatchSync] Failed to handle iCloud recovery: $e'); + } + } + static Map? _getTokenForWatch() { if (!initDone || initData.tokens.isEmpty) { debugPrint('[WatchSync] No token available'); diff --git a/firka/lib/main.dart b/firka/lib/main.dart index 9d70e27..5457cfe 100644 --- a/firka/lib/main.dart +++ b/firka/lib/main.dart @@ -226,7 +226,7 @@ Future _initData(AppInitialization init) async { if (init.tokens.isNotEmpty) { init.client.model = init.tokens.first; } - logger.info('[Init] Recovered fresher token from iCloud'); + logger.info('[Init] Recovered fresher token from iCloud (immediate)'); } await Future.delayed(const Duration(milliseconds: 300)); diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart index 98617dd..8395e4a 100644 --- a/firka/lib/ui/phone/screens/home/home_screen.dart +++ b/firka/lib/ui/phone/screens/home/home_screen.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:io'; +import 'package:isar/isar.dart'; + import 'package:firka/helpers/api/client/kreta_client.dart'; +import 'package:firka/helpers/db/models/token_model.dart'; import 'package:firka/helpers/api/client/kreta_stream.dart'; import 'package:firka/helpers/api/exceptions/token.dart'; import 'package:firka/helpers/extensions.dart'; @@ -31,6 +34,7 @@ import '../../../../helpers/debug_helper.dart'; import '../../../../helpers/firka_bundle.dart'; import '../../../../helpers/firka_state.dart'; import '../../../../helpers/image_preloader.dart'; +import '../../../../helpers/watch_sync_helper.dart'; import '../../../widget/delayed_spinner.dart'; import '../../../widget/firka_icon.dart'; import '../../pages/extras/extras.dart'; @@ -207,6 +211,41 @@ class _HomeScreenState extends FirkaState { try { _prefetched = true; + if (Platform.isIOS) { + final token = widget.data.tokens.isNotEmpty ? widget.data.tokens.first : null; + final tokenExpiry = token?.expiryDate; + final isTokenExpiredOrExpiring = tokenExpiry == null || + tokenExpiry.isBefore(DateTime.now().add(const Duration(minutes: 5))); + + if (isTokenExpiredOrExpiring || KretaClient.needsReauth) { + logger.info('[Home] Token expired/expiring or needsReauth, trying iCloud recovery...'); + + const delays = [1, 5, 10, 30, 60]; + + for (int attempt = 0; attempt < delays.length; attempt++) { + await Future.delayed(Duration(seconds: delays[attempt])); + + final recovered = await WatchSyncHelper.checkAndRecoverFromiCloud( + isar: widget.data.isar, + tokens: widget.data.tokens, + client: widget.data.client, + ); + + if (recovered) { + widget.data.tokens = await widget.data.isar.tokenModels.where().findAll(); + if (widget.data.tokens.isNotEmpty) { + widget.data.client.model = widget.data.tokens.first; + } + KretaClient.clearReauthFlag(); + logger.info('[Home] Recovered token from iCloud (attempt ${attempt + 1}, after ${delays[attempt]}s)'); + break; + } + + logger.fine('[Home] iCloud check attempt ${attempt + 1} (after ${delays[attempt]}s): no fresher token yet'); + } + } + } + await fetchData(); if (Platform.isAndroid) {