forked from firka/firka
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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void> _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<String, dynamic>? _getTokenForWatch() {
|
||||
if (!initDone || initData.tokens.isEmpty) {
|
||||
debugPrint('[WatchSync] No token available');
|
||||
|
||||
@@ -226,7 +226,7 @@ Future<void> _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));
|
||||
|
||||
@@ -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<HomeScreen> {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user