From b9de46f0ed5186cf94ba40975216370869121b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Gergely?= Date: Sun, 8 Feb 2026 16:18:28 +0100 Subject: [PATCH] Add iCloud token sync & improve recovery Add iCloud-based token recovery and saving across iOS/watch components. Introduces WatchSyncHelper.checkAndRecoverFromiCloud and saveTokenToiCloud to fetch/save fresher tokens via a method channel, calls the check on app init and before proactive refresh, and attempts to save refreshed tokens to iCloud. Adds native handlers in WatchSessionManager (checkiCloudToken, saveTokeToniCloud) and updates DataStore token recovery to distinguish network errors vs permanently invalid tokens (avoid unnecessary reauth). Update logging messages accordingly. Also update ubiquity-kvstore identifiers in entitlements to use explicit group.app.firka.firkaa and bump iOS build/version (CURRENT_PROJECT_VERSION and CFBundleVersion) to 1068. --- .../FirkaWatch Watch App.entitlements | 2 +- .../Services/DataStore.swift | 32 ++++- ...kaWatchComplicationsExtension.entitlements | 2 +- firka/ios/Runner.xcodeproj/project.pbxproj | 30 ++--- firka/ios/Runner/Info.plist | 2 +- firka/ios/Runner/Runner.entitlements | 2 +- firka/ios/Runner/WatchSessionManager.swift | 68 ++++++++++ .../lib/helpers/api/client/kreta_client.dart | 44 ++++++- firka/lib/helpers/watch_sync_helper.dart | 120 ++++++++++++++++++ firka/lib/main.dart | 15 ++- 10 files changed, 290 insertions(+), 27 deletions(-) diff --git a/firka/ios/FirkaWatch Watch App/FirkaWatch Watch App.entitlements b/firka/ios/FirkaWatch Watch App/FirkaWatch Watch App.entitlements index af013c5f..a96138b9 100644 --- a/firka/ios/FirkaWatch Watch App/FirkaWatch Watch App.entitlements +++ b/firka/ios/FirkaWatch Watch App/FirkaWatch Watch App.entitlements @@ -7,6 +7,6 @@ group.app.firka.firkaa com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)$(CFBundleIdentifier) + $(TeamIdentifierPrefix)app.firka.firkaa diff --git a/firka/ios/FirkaWatch Watch App/Services/DataStore.swift b/firka/ios/FirkaWatch Watch App/Services/DataStore.swift index bf65526a..65082211 100644 --- a/firka/ios/FirkaWatch Watch App/Services/DataStore.swift +++ b/firka/ios/FirkaWatch Watch App/Services/DataStore.swift @@ -174,13 +174,27 @@ class DataStore { } print("[Watch] Recovery Step 2: Attempting API token refresh...") + var isNetworkError = false + var isTokenPermanentlyInvalid = false + do { _ = try await TokenManager.shared.refreshToken() print("[Watch] Recovery: Token refresh succeeded!") checkTokenState() return true + } catch let tokenError as TokenError { + print("[Watch] Recovery: API token refresh failed: \(tokenError)") + switch tokenError { + case .networkError: + isNetworkError = true + case .refreshExpired, .invalidGrant: + isTokenPermanentlyInvalid = true + default: + break + } } catch { - print("[Watch] Recovery: API token refresh failed: \(error)") + print("[Watch] Recovery: API token refresh failed with unknown error: \(error)") + isNetworkError = true // Assume network issue for unknown errors } print("[Watch] Recovery Step 3: Checking if iPhone is reachable...") @@ -190,9 +204,19 @@ class DataStore { return true } - print("[Watch] Recovery: All attempts failed, will show reauth screen") - recoveryAttempted = true - self.error = "token_expired" + if isTokenPermanentlyInvalid { + print("[Watch] Recovery: Token permanently invalid, showing reauth screen") + recoveryAttempted = true + self.error = "token_expired" + } else if isNetworkError { + print("[Watch] Recovery: Network error - not showing reauth, user can retry") + self.error = "network" + } else { + print("[Watch] Recovery: Unknown failure, showing reauth screen") + recoveryAttempted = true + self.error = "token_expired" + } + return false } diff --git a/firka/ios/FirkaWatchComplicationsExtension.entitlements b/firka/ios/FirkaWatchComplicationsExtension.entitlements index af013c5f..a96138b9 100644 --- a/firka/ios/FirkaWatchComplicationsExtension.entitlements +++ b/firka/ios/FirkaWatchComplicationsExtension.entitlements @@ -7,6 +7,6 @@ group.app.firka.firkaa com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)$(CFBundleIdentifier) + $(TeamIdentifierPrefix)app.firka.firkaa diff --git a/firka/ios/Runner.xcodeproj/project.pbxproj b/firka/ios/Runner.xcodeproj/project.pbxproj index d97857b3..d04c6649 100644 --- a/firka/ios/Runner.xcodeproj/project.pbxproj +++ b/firka/ios/Runner.xcodeproj/project.pbxproj @@ -1076,7 +1076,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -1108,7 +1108,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; @@ -1127,7 +1127,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; @@ -1144,7 +1144,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; @@ -1171,7 +1171,7 @@ CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1222,7 +1222,7 @@ CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1270,7 +1270,7 @@ CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1317,7 +1317,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1370,7 +1370,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1420,7 +1420,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1471,7 +1471,7 @@ CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1522,7 +1522,7 @@ CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1569,7 +1569,7 @@ CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1874,7 +1874,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -1910,7 +1910,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1062; + CURRENT_PROJECT_VERSION = 1068; DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/firka/ios/Runner/Info.plist b/firka/ios/Runner/Info.plist index 069b1006..338316ae 100644 --- a/firka/ios/Runner/Info.plist +++ b/firka/ios/Runner/Info.plist @@ -46,7 +46,7 @@ CFBundleVersion - 1062 + 1068 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/firka/ios/Runner/Runner.entitlements b/firka/ios/Runner/Runner.entitlements index 17ab5942..48392c50 100644 --- a/firka/ios/Runner/Runner.entitlements +++ b/firka/ios/Runner/Runner.entitlements @@ -11,6 +11,6 @@ group.app.firka.firkaa com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)$(CFBundleIdentifier) + $(TeamIdentifierPrefix)app.firka.firkaa diff --git a/firka/ios/Runner/WatchSessionManager.swift b/firka/ios/Runner/WatchSessionManager.swift index a2b17714..540d9284 100644 --- a/firka/ios/Runner/WatchSessionManager.swift +++ b/firka/ios/Runner/WatchSessionManager.swift @@ -29,6 +29,10 @@ class WatchSessionManager: NSObject, WCSessionDelegate { self?.handleNotifyReauthRequired(result: result) case "requestTokenFromWatch": self?.handleRequestTokenFromWatch(result: result) + case "checkiCloudToken": + self?.handleCheckiCloudToken(result: result) + case "saveTokeToniCloud": + self?.handleSaveTokenToiCloud(arguments: call.arguments, result: result) default: result(FlutterMethodNotImplemented) } @@ -151,6 +155,70 @@ class WatchSessionManager: NSObject, WCSessionDelegate { ) } + private func handleCheckiCloudToken(result: @escaping FlutterResult) { + print("[WatchSessionManager] Checking iCloud for token...") + + guard let token = iCloudTokenManager.shared.loadToken() else { + print("[WatchSessionManager] No token in iCloud") + result(["error": "no_token"]) + return + } + + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + print("[WatchSessionManager] Found iCloud token, expiry: \(formatter.string(from: token.expiryDate))") + + let tokenData: [String: Any] = [ + "studentId": token.studentId, + "studentIdNorm": token.studentIdNorm, + "iss": token.iss, + "idToken": token.idToken, + "accessToken": token.accessToken, + "refreshToken": token.refreshToken, + "expiryDate": Int64(token.expiryDate.timeIntervalSince1970 * 1000) + ] + + result(tokenData) + } + + private func handleSaveTokenToiCloud(arguments: Any?, result: @escaping FlutterResult) { + guard let tokenData = arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGS", message: "Arguments must be a dictionary", details: nil)) + return + } + + guard let accessToken = tokenData["accessToken"] as? String, + let refreshToken = tokenData["refreshToken"] as? String, + let idToken = tokenData["idToken"] as? String, + let iss = tokenData["iss"] as? String, + let studentId = tokenData["studentId"] as? String, + let expiryMs = tokenData["expiryDate"] as? Int64 else { + result(FlutterError(code: "INVALID_ARGS", message: "Missing required token fields", details: nil)) + return + } + + let studentIdNorm = tokenData["studentIdNorm"] as? Int64 ?? 0 + let expiryDate = Date(timeIntervalSince1970: Double(expiryMs) / 1000.0) + + let token = WatchToken( + accessToken: accessToken, + refreshToken: refreshToken, + idToken: idToken, + iss: iss, + studentId: studentId, + studentIdNorm: studentIdNorm, + expiryDate: expiryDate + ) + + iCloudTokenManager.shared.saveToken(token, deviceName: "iPhone") + + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + print("[WatchSessionManager] Token saved to iCloud, expiry: \(formatter.string(from: expiryDate))") + + result(nil) + } + func session( _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, diff --git a/firka/lib/helpers/api/client/kreta_client.dart b/firka/lib/helpers/api/client/kreta_client.dart index 1e8fa324..8d98e10b 100644 --- a/firka/lib/helpers/api/client/kreta_client.dart +++ b/firka/lib/helpers/api/client/kreta_client.dart @@ -94,6 +94,16 @@ class KretaClient { if (!Platform.isIOS || !initDone) return false; try { + final recoveredFromiCloud = await WatchSyncHelper.checkAndRecoverFromiCloud( + isar: initData.isar, + tokens: initData.tokens, + client: initData.client, + ); + if (recoveredFromiCloud) { + debugPrint('[KretaClient] Recovered fresh token from iCloud'); + return true; + } + await WatchSyncHelper.syncTokenFromWatch( isar: initData.isar, tokens: initData.tokens, @@ -113,7 +123,7 @@ class KretaClient { return false; } catch (e) { - debugPrint('[KretaClient] Failed to recover from Watch: $e'); + debugPrint('[KretaClient] Failed to recover from Watch/iCloud: $e'); return false; } } @@ -126,7 +136,25 @@ class KretaClient { final fiveMinutesFromNow = now.add(const Duration(minutes: 5)); if (model.expiryDate == null || model.expiryDate!.isBefore(fiveMinutesFromNow)) { - logger.info("[Proactive] Token expired or expiring soon, refreshing proactively..."); + logger.info("[Proactive] Token expired or expiring soon..."); + + if (Platform.isIOS && initDone) { + final recoveredFromiCloud = await WatchSyncHelper.checkAndRecoverFromiCloud( + isar: isar, + tokens: initData.tokens, + client: this, + ); + if (recoveredFromiCloud) { + logger.info("[Proactive] Found fresh token in iCloud, no refresh needed"); + initData.tokens = await isar.tokenModels.where().findAll(); + if (initData.tokens.isNotEmpty) { + model = initData.tokens.first; + } + return true; + } + } + + logger.info("[Proactive] No fresh token in iCloud, refreshing..."); try { var extended = await extendToken(model); @@ -140,6 +168,12 @@ class KretaClient { model = tokenModel; if (Platform.isIOS) { + try { + await WatchSyncHelper.saveTokenToiCloud(tokenModel); + } catch (e) { + debugPrint('[KretaClient] iCloud token sync skipped: $e'); + } + try { await _watchChannel.invokeMethod('sendTokenToWatch', { 'studentId': model.studentId, @@ -207,6 +241,12 @@ class KretaClient { model = tokenModel; if (Platform.isIOS) { + try { + await WatchSyncHelper.saveTokenToiCloud(tokenModel); + } catch (e) { + debugPrint('[KretaClient] iCloud token sync skipped: $e'); + } + try { await _watchChannel.invokeMethod('sendTokenToWatch', { 'studentId': model.studentId, diff --git a/firka/lib/helpers/watch_sync_helper.dart b/firka/lib/helpers/watch_sync_helper.dart index 5c6a415d..34fd0722 100644 --- a/firka/lib/helpers/watch_sync_helper.dart +++ b/firka/lib/helpers/watch_sync_helper.dart @@ -194,6 +194,126 @@ class WatchSyncHelper { } } + /// Check iCloud for a fresher token and update local storage if found. + /// This should be called on app startup BEFORE any API calls. + /// Returns true if a fresher token was found and applied. + static Future checkAndRecoverFromiCloud({ + Isar? isar, + List? tokens, + KretaClient? client, + }) async { + if (!Platform.isIOS) return false; + + final effectiveIsar = isar ?? (initDone ? initData.isar : null); + final effectiveTokens = tokens ?? (initDone ? initData.tokens : null); + final effectiveClient = client ?? (initDone ? initData.client : null); + + if (effectiveIsar == null) { + debugPrint('[WatchSync] Cannot check iCloud: no isar available'); + return false; + } + + try { + debugPrint('[WatchSync] Checking iCloud for fresher token...'); + final result = await _watchChannel.invokeMethod('checkiCloudToken'); + + if (result == null) { + debugPrint('[WatchSync] No response from native'); + return false; + } + + final tokenData = result as Map; + if (tokenData.containsKey('error')) { + debugPrint('[WatchSync] iCloud check returned: ${tokenData['error']}'); + return false; + } + + final iCloudExpiry = tokenData['expiryDate'] as int?; + if (iCloudExpiry == null) { + debugPrint('[WatchSync] iCloud token has no expiry'); + return false; + } + + final iCloudExpiryDate = DateTime.fromMillisecondsSinceEpoch(iCloudExpiry); + + if (iCloudExpiryDate.isBefore(DateTime.now())) { + debugPrint('[WatchSync] iCloud token is expired'); + return false; + } + + final currentToken = effectiveTokens?.isNotEmpty == true ? effectiveTokens!.first : null; + final localExpiry = currentToken?.expiryDate; + + if (localExpiry == null || iCloudExpiryDate.isAfter(localExpiry)) { + debugPrint('[WatchSync] iCloud has fresher token! iCloud: $iCloudExpiryDate, Local: $localExpiry'); + + final newToken = TokenModel.fromValues( + (tokenData['studentIdNorm'] as int?) ?? 0, + tokenData['studentId'] as String, + tokenData['iss'] as String, + tokenData['idToken'] as String, + tokenData['accessToken'] as String, + tokenData['refreshToken'] as String, + iCloudExpiry, + ); + + await effectiveIsar.writeTxn(() async { + await effectiveIsar.tokenModels.put(newToken); + }); + + final updatedTokens = await effectiveIsar.tokenModels.where().findAll(); + + if (initDone) { + initData.tokens = updatedTokens; + } + + if (effectiveClient != null) { + effectiveClient.model = newToken; + } + + KretaClient.clearReauthFlag(); + + debugPrint('[WatchSync] Token recovered from iCloud! New expiry: $iCloudExpiryDate'); + return true; + } else { + debugPrint('[WatchSync] Local token is same or fresher. Local: $localExpiry, iCloud: $iCloudExpiryDate'); + return false; + } + } catch (e) { + debugPrint('[WatchSync] Failed to check iCloud: $e'); + return false; + } + } + + /// Save token to iCloud. Call this after refreshing token on iPhone. + static Future saveTokenToiCloud(TokenModel token) async { + if (!Platform.isIOS) return; + + if (token.accessToken == null || + token.refreshToken == null || + token.expiryDate == null) { + debugPrint('[WatchSync] Token incomplete, not saving to iCloud'); + return; + } + + final tokenData = { + 'studentId': token.studentId, + 'studentIdNorm': token.studentIdNorm, + 'iss': token.iss, + 'idToken': token.idToken, + 'accessToken': token.accessToken, + 'refreshToken': token.refreshToken, + 'expiryDate': token.expiryDate!.millisecondsSinceEpoch, + }; + + try { + await _watchChannel.invokeMethod('saveTokeToniCloud', tokenData); + debugPrint('[WatchSync] Token saved to iCloud'); + } catch (e) { + debugPrint('[WatchSync] Failed to save token to iCloud: $e'); + } + } + static Future syncTokenFromWatch({ Isar? isar, List? tokens, diff --git a/firka/lib/main.dart b/firka/lib/main.dart index edff368c..9d70e27d 100644 --- a/firka/lib/main.dart +++ b/firka/lib/main.dart @@ -215,10 +215,21 @@ Future _initData(AppInitialization init) async { logger.fine("Initializing kréta client as: ${token.studentId}"); init.client = KretaClient(token, init.isar); - // Sync token from Watch first (Watch might have fresher token) if (Platform.isIOS) { - await Future.delayed(const Duration(milliseconds: 300)); + final recoveredFromiCloud = await WatchSyncHelper.checkAndRecoverFromiCloud( + isar: init.isar, + tokens: init.tokens, + client: init.client, + ); + if (recoveredFromiCloud) { + init.tokens = await init.isar.tokenModels.where().findAll(); + if (init.tokens.isNotEmpty) { + init.client.model = init.tokens.first; + } + logger.info('[Init] Recovered fresher token from iCloud'); + } + await Future.delayed(const Duration(milliseconds: 300)); await WatchSyncHelper.syncTokenFromWatch( isar: init.isar, tokens: init.tokens,