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) {