From 8a3f77d5657f513745f8b3af65a2820a809e078c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Gergely?= Date: Fri, 19 Dec 2025 14:51:12 +0100 Subject: [PATCH] Implement token expiration handling and reauthentication UI; add reauth toast and update seasonal icon logic --- .../Runner/TimetableActivityAttributes.swift | 637 ------------------ .../TimetableWidget/SeasonalIconHelper.swift | 45 +- .../TimetableWidget/TimeFormatHelper.swift | 85 --- .../TimetableActivityAttributes.swift | 81 ++- .../TimetableLiveActivity.swift | 82 +-- firka/lib/helpers/live_activity_service.dart | 80 ++- .../ui/phone/pages/extras/main_reauth.dart | 61 ++ .../ui/phone/pages/extras/reauth_toast.dart | 90 +++ .../ui/phone/screens/home/home_screen.dart | 26 +- 9 files changed, 374 insertions(+), 813 deletions(-) delete mode 100644 firka/ios/Runner/TimetableActivityAttributes.swift delete mode 100644 firka/ios/TimetableWidget/TimeFormatHelper.swift create mode 100644 firka/lib/ui/phone/pages/extras/main_reauth.dart create mode 100644 firka/lib/ui/phone/pages/extras/reauth_toast.dart diff --git a/firka/ios/Runner/TimetableActivityAttributes.swift b/firka/ios/Runner/TimetableActivityAttributes.swift deleted file mode 100644 index 45dd3f1..0000000 --- a/firka/ios/Runner/TimetableActivityAttributes.swift +++ /dev/null @@ -1,637 +0,0 @@ -import ActivityKit -import Foundation - -struct TimetableActivityAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var isBreak: Bool - var lessonName: String - var lessonTheme: String? - var roomName: String? - var teacherName: String? - var startTime: Date - var endTime: Date - var lessonNumber: Int? - - var mode: String? // "lesson" | "break" | "seasonalBreak" | "xmas" | "newYearEve" | "newYearDay" - var message: String? - var season: String? - - var nextLessonName: String? - var nextRoomName: String? - var nextStartTime: Date? - - var isSubstitution: Bool? - - var isCancelled: Bool? - - var substituteTeacher: String? - - var currentTime: Date - - var tokenExpirationWarning: String? - - enum CodingKeys: String, CodingKey { - - case isBreak - - case lessonName - - case lessonTheme - - case roomName - - case teacherName - - case startTime - - case endTime - - case lessonNumber - - case mode - - case message - - case season - - case nextLessonName - - case nextRoomName - - case nextStartTime - - case isSubstitution - - case isCancelled - - case substituteTeacher - - case currentTime - - case tokenExpirationWarning - - } - - init(isBreak: Bool, lessonName: String, lessonTheme: String?, roomName: String?, teacherName: String?, startTime: Date, endTime: Date, lessonNumber: Int?, mode: String?, message: String?, season: String?, nextLessonName: String?, nextRoomName: String?, nextStartTime: Date?, isSubstitution: Bool?, isCancelled: Bool?, substituteTeacher: String?, currentTime: Date, tokenExpirationWarning: String? = nil) { - - self.isBreak = isBreak - - self.lessonName = lessonName - - self.lessonTheme = lessonTheme - - self.roomName = roomName - - self.teacherName = teacherName - - self.startTime = startTime - - self.endTime = endTime - - self.lessonNumber = lessonNumber - - self.mode = mode - - self.message = message - - self.season = season - - self.nextLessonName = nextLessonName - - self.nextRoomName = nextRoomName - - self.nextStartTime = nextStartTime - - self.isSubstitution = isSubstitution - - self.isCancelled = isCancelled - - self.substituteTeacher = substituteTeacher - - self.currentTime = currentTime - - self.tokenExpirationWarning = tokenExpirationWarning - - } - - - - init(from decoder: Decoder) throws { - - let container = try decoder.container(keyedBy: CodingKeys.self) - - - - let isoFormatter = ISO8601DateFormatter() - - isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - - - - isBreak = try container.decode(Bool.self, forKey: .isBreak) - - lessonName = try container.decode(String.self, forKey: .lessonName) - - lessonTheme = try container.decodeIfPresent(String.self, forKey: .lessonTheme) - - roomName = try container.decodeIfPresent(String.self, forKey: .roomName) - - teacherName = try container.decodeIfPresent(String.self, forKey: .teacherName) - - - - let startTimeStr = try container.decode(String.self, forKey: .startTime) - - guard let startTimeDate = isoFormatter.date(from: startTimeStr) else { - - throw DecodingError.dataCorruptedError(forKey: .startTime, in: container, debugDescription: "Invalid startTime format: \(startTimeStr)") - - } - - startTime = startTimeDate - - - - let endTimeStr = try container.decode(String.self, forKey: .endTime) - - guard let endTimeDate = isoFormatter.date(from: endTimeStr) else { - - throw DecodingError.dataCorruptedError(forKey: .endTime, in: container, debugDescription: "Invalid endTime format: \(endTimeStr)") - - } - - endTime = endTimeDate - - - - lessonNumber = try container.decodeIfPresent(Int.self, forKey: .lessonNumber) - - mode = try container.decodeIfPresent(String.self, forKey: .mode) - - message = try container.decodeIfPresent(String.self, forKey: .message) - - season = try container.decodeIfPresent(String.self, forKey: .season) - - nextLessonName = try container.decodeIfPresent(String.self, forKey: .nextLessonName) - - nextRoomName = try container.decodeIfPresent(String.self, forKey: .nextRoomName) - - - - if let nextStartTimeStr = try container.decodeIfPresent(String.self, forKey: .nextStartTime) { - - nextStartTime = isoFormatter.date(from: nextStartTimeStr) - - } else { - - nextStartTime = nil - - } - - - - isSubstitution = try container.decodeIfPresent(Bool.self, forKey: .isSubstitution) - - isCancelled = try container.decodeIfPresent(Bool.self, forKey: .isCancelled) - - substituteTeacher = try container.decodeIfPresent(String.self, forKey: .substituteTeacher) - - tokenExpirationWarning = try container.decodeIfPresent(String.self, forKey: .tokenExpirationWarning) - - - - let currentTimeStr = try container.decode(String.self, forKey: .currentTime) - - guard let currentTimeDate = isoFormatter.date(from: currentTimeStr) else { - - throw DecodingError.dataCorruptedError(forKey: .currentTime, in: container, debugDescription: "Invalid currentTime format: \(currentTimeStr)") - - } - - currentTime = currentTimeDate - - } - - - - func encode(to encoder: Encoder) throws { - - var container = encoder.container(keyedBy: CodingKeys.self) - - - - let isoFormatter = ISO8601DateFormatter() - - isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - - - - try container.encode(isBreak, forKey: .isBreak) - - try container.encode(lessonName, forKey: .lessonName) - - try container.encodeIfPresent(lessonTheme, forKey: .lessonTheme) - - try container.encodeIfPresent(roomName, forKey: .roomName) - - try container.encodeIfPresent(teacherName, forKey: .teacherName) - - - - try container.encode(isoFormatter.string(from: startTime), forKey: .startTime) - - try container.encode(isoFormatter.string(from: endTime), forKey: .endTime) - - - - try container.encodeIfPresent(lessonNumber, forKey: .lessonNumber) - - try container.encodeIfPresent(mode, forKey: .mode) - - try container.encodeIfPresent(message, forKey: .message) - - try container.encodeIfPresent(season, forKey: .season) - - try container.encodeIfPresent(nextLessonName, forKey: .nextLessonName) - - try container.encodeIfPresent(nextRoomName, forKey: .nextRoomName) - - - - if let nextStartTime = nextStartTime { - - try container.encode(isoFormatter.string(from: nextStartTime), forKey: .nextStartTime) - - } - - - - try container.encodeIfPresent(isSubstitution, forKey: .isSubstitution) - - try container.encodeIfPresent(isCancelled, forKey: .isCancelled) - - try container.encodeIfPresent(substituteTeacher, forKey: .substituteTeacher) - - try container.encodeIfPresent(tokenExpirationWarning, forKey: .tokenExpirationWarning) - - - - try container.encode(isoFormatter.string(from: currentTime), forKey: .currentTime) - - } - - } - - - - var studentName: String - - var schoolName: String - - } - - - - extension TimetableActivityAttributes.ContentState { - - var timeRemaining: TimeInterval { - - return endTime.timeIntervalSince(currentTime) - - } - - - - var isBeforeSchool: Bool { - - return currentTime < startTime && !isBreak - - } - - - - var formattedStartTime: String { - - let formatter = DateFormatter() - - formatter.dateFormat = "HH:mm" - - formatter.timeZone = TimeZone(identifier: "UTC") - - - let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: startTime) ?? startTime - - return formatter.string(from: adjustedDate) - - } - - - - var formattedEndTime: String { - - let formatter = DateFormatter() - - formatter.dateFormat = "HH:mm" - - - formatter.timeZone = TimeZone(identifier: "UTC") - - let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: endTime) ?? endTime - - return formatter.string(from: adjustedDate) - - } - - - - var formattedNextStartTime: String { - - guard let nextStartTime = nextStartTime else { return "" } - - let formatter = DateFormatter() - - formatter.dateFormat = "HH:mm" - - - formatter.timeZone = TimeZone(identifier: "UTC") - - let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: nextStartTime) ?? nextStartTime - - return formatter.string(from: adjustedDate) - - } - - - - var timeRemainingText: String { - - let remaining = timeRemaining - - - - if remaining < 0 { - - return "0:00" - - } - - - - let hours = Int(remaining) / 3600 - - let minutes = (Int(remaining) % 3600) / 60 - - let seconds = Int(remaining) % 60 - - - - if hours > 0 { - - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - - } else if minutes > 0 { - - return String(format: "%d:%02d", minutes, seconds) - - } else { - - return String(format: "0:%02d", seconds) - - } - - } - - - - - var seasonalRemainingText: String { - let remaining = max(0, timeRemaining) - let hours = Int(remaining) / 3600 - if hours >= 24 { - let days = hours / 24 - return "Szünetből hátralévő idő: \(days) nap" - } - return "Szünetből hátralévő idő: \(hours) óra" - } - - - - var seasonalDisplayValue: String { - let remaining = max(0, timeRemaining) - let hours = Int(remaining) / 3600 - if hours >= 24 { - let days = hours / 24 - return "\(days) nap" - } - return "\(hours) óra" - } - - } - - - - - extension TimetableActivityAttributes.ContentState { - - func toJSON() -> [String: Any] { - - var json: [String: Any] = [ - - "isBreak": isBreak, - - "lessonName": lessonName, - - "startTime": ISO8601DateFormatter().string(from: startTime), - - "endTime": ISO8601DateFormatter().string(from: endTime), - - "currentTime": ISO8601DateFormatter().string(from: currentTime) - - ] - - - - if let isSubstitution = isSubstitution { - - json["isSubstitution"] = isSubstitution - - } - - if let isCancelled = isCancelled { - - json["isCancelled"] = isCancelled - - } - - if let lessonTheme = lessonTheme { - - json["lessonTheme"] = lessonTheme - - } - - if let roomName = roomName { - - json["roomName"] = roomName - - } - - if let teacherName = teacherName { - - json["teacherName"] = teacherName - - } - - if let lessonNumber = lessonNumber { - - json["lessonNumber"] = lessonNumber - - } - - if let nextLessonName = nextLessonName { - - json["nextLessonName"] = nextLessonName - - } - - if let nextRoomName = nextRoomName { - - json["nextRoomName"] = nextRoomName - - } - - if let nextStartTime = nextStartTime { - - json["nextStartTime"] = ISO8601DateFormatter().string(from: nextStartTime) - - } - - if let substituteTeacher = substituteTeacher { - - json["substituteTeacher"] = substituteTeacher - - } - - if let mode = mode { - - json["mode"] = mode - - } - - if let message = message { - - json["message"] = message - - } - - if let season = season { - - json["season"] = season - - } - - if let tokenExpirationWarning = tokenExpirationWarning { - - json["tokenExpirationWarning"] = tokenExpirationWarning - - } - - - - return json - - } - - - - static func fromJSON(_ json: [String: Any]) -> TimetableActivityAttributes.ContentState? { - - let isoFormatter = ISO8601DateFormatter() - - isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - - - - guard let isBreak = json["isBreak"] as? Bool, - - let lessonName = json["lessonName"] as? String, - - let startTimeStr = json["startTime"] as? String, - - let endTimeStr = json["endTime"] as? String, - - let startTime = isoFormatter.date(from: startTimeStr), - - let endTime = isoFormatter.date(from: endTimeStr) else { - - return nil - - } - - - let currentTimeStr = json["currentTime"] as? String - - let currentTime = currentTimeStr.flatMap { isoFormatter.date(from: $0) } ?? Date() - - - - let nextStartTime: Date? - - if let nextStartTimeStr = json["nextStartTime"] as? String { - - nextStartTime = isoFormatter.date(from: nextStartTimeStr) - - } else { - - nextStartTime = nil - - } - - - - return TimetableActivityAttributes.ContentState( - - isBreak: isBreak, - - lessonName: lessonName, - - lessonTheme: json["lessonTheme"] as? String, - - roomName: json["roomName"] as? String, - - teacherName: json["teacherName"] as? String, - - startTime: startTime, - - endTime: endTime, - - lessonNumber: json["lessonNumber"] as? Int, - - mode: json["mode"] as? String, - - message: json["message"] as? String, - - season: json["season"] as? String, - - nextLessonName: json["nextLessonName"] as? String, - - nextRoomName: json["nextRoomName"] as? String, - - nextStartTime: nextStartTime, - - isSubstitution: json["isSubstitution"] as? Bool, - - isCancelled: json["isCancelled"] as? Bool, - - substituteTeacher: json["substituteTeacher"] as? String, - - currentTime: currentTime, - - tokenExpirationWarning: json["tokenExpirationWarning"] as? String - - ) - - } - - } - - diff --git a/firka/ios/TimetableWidget/SeasonalIconHelper.swift b/firka/ios/TimetableWidget/SeasonalIconHelper.swift index 7d918c6..126c8c0 100644 --- a/firka/ios/TimetableWidget/SeasonalIconHelper.swift +++ b/firka/ios/TimetableWidget/SeasonalIconHelper.swift @@ -1,11 +1,11 @@ import SwiftUI struct SeasonalIconHelper { - static func iconName(for mode: String?, season: String?) -> String { + static func iconName(for mode: String?, season: String?, lessonIcon: String? = nil) -> String { guard let mode = mode else { - return "book.fill" + return lessonIcon ?? "book.fill" } - + switch mode { case "xmas": return "gift.fill" @@ -33,26 +33,55 @@ struct SeasonalIconHelper { default: return "snowflake" } + case "lesson": + return lessonIcon ?? "book.fill" default: - return "book.fill" + return lessonIcon ?? "book.fill" } } - static func iconColor(for mode: String?) -> Color { + static func iconColor(for mode: String?, season: String? = nil) -> Color { guard let mode = mode else { return .green } - + switch mode { case "beforeSchool": return .orange - case "xmas", "newYearEve", "newYearDay", "seasonalBreak": - return .green + case "xmas": + return .red + case "newYearEve": + return .purple + case "newYearDay": + return .mint + case "seasonalBreak": + return seasonColor(for: season) default: return .green } } + static func seasonColor(for season: String?) -> Color { + guard let season = season else { + return .blue + } + + switch season { + case "spring": + return .green + case "summer": + return .blue + case "autumn": + return .orange + case "winter": + return .cyan + case "other": + return .blue + default: + return .blue + } + } + static func isSeasonalMode(_ mode: String?) -> Bool { guard let mode = mode else { return false diff --git a/firka/ios/TimetableWidget/TimeFormatHelper.swift b/firka/ios/TimetableWidget/TimeFormatHelper.swift deleted file mode 100644 index 3c160f2..0000000 --- a/firka/ios/TimetableWidget/TimeFormatHelper.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Foundation -import ActivityKit - -struct TimeFormatHelper { - static func compactTime(remaining: TimeInterval, labels: TimetableActivityAttributes.ContentState.Labels?) -> String { - let lang = detectLanguage(from: labels) - - let hours = Int(max(0, remaining)) / 3600 - let minutes = (Int(max(0, remaining)) % 3600) / 60 - - if hours >= 1 { - return shortHours(hours, language: lang) - } else { - return shortMinutes(minutes, language: lang) - } - } - - static func compactSeasonalBreak(from message: String, labels: TimetableActivityAttributes.ContentState.Labels?) -> String { - let lang = detectLanguage(from: labels) - - let components = message.split(separator: " ") - let number = components.first.map(String.init) ?? "" - - let isDay = message.lowercased().contains("day") || - message.lowercased().contains("nap") || - message.lowercased().contains("tag") - - if isDay { - return shortDays(Int(number) ?? 0, language: lang) - } else { - return shortHours(Int(number) ?? 0, language: lang) - } - } - - // MARK: - Language Detection - - private static func detectLanguage(from labels: TimetableActivityAttributes.ContentState.Labels?) -> String { - guard let labels = labels else { - return Locale.current.languageCode ?? "hu" - } - - if let text = labels.remainingLabel ?? labels.cancelledText { - let lower = text.lowercased() - - if lower.contains("time") || lower.contains("remaining") || lower.contains("cancelled") { - return "en" - } else if lower.contains("verbleibende") || lower.contains("zeit") || lower.contains("entfallen") { - return "de" - } else { - return "hu" - } - } - - return Locale.current.languageCode ?? "hu" - } - - // MARK: - Private Helpers - - private static func shortHours(_ hours: Int, language: String) -> String { - switch language { - case "en": return "\(hours)h" - case "de": return "\(hours)td" - case "hu": return "\(hours)ó" - default: return "\(hours)h" - } - } - - private static func shortMinutes(_ minutes: Int, language: String) -> String { - switch language { - case "en": return "\(minutes)m" - case "de": return "\(minutes)Min" - case "hu": return "\(minutes)p" - default: return "\(minutes)m" - } - } - - private static func shortDays(_ days: Int, language: String) -> String { - switch language { - case "en": return "\(days)d" - case "de": return "\(days)T" - case "hu": return "\(days)n" - default: return "\(days)d" - } - } -} diff --git a/firka/ios/TimetableWidget/TimetableActivityAttributes.swift b/firka/ios/TimetableWidget/TimetableActivityAttributes.swift index 421fb97..b7c6438 100644 --- a/firka/ios/TimetableWidget/TimetableActivityAttributes.swift +++ b/firka/ios/TimetableWidget/TimetableActivityAttributes.swift @@ -28,6 +28,8 @@ struct TimetableActivityAttributes: ActivityAttributes { var labels: Labels? var tokenExpirationWarning: String? + var compactTimerText: String? + var lessonIcon: String? struct Labels: Codable, Hashable { var title: String? @@ -66,9 +68,11 @@ struct TimetableActivityAttributes: ActivityAttributes { case currentTime case labels case tokenExpirationWarning + case compactTimerText + case lessonIcon } - init(isBreak: Bool, lessonName: String, lessonTheme: String?, roomName: String?, teacherName: String?, startTime: Date, endTime: Date, lessonNumber: Int?, mode: String?, message: String?, season: String?, nextLessonName: String?, nextRoomName: String?, nextStartTime: Date?, isSubstitution: Bool, isCancelled: Bool, substituteTeacher: String?, currentTime: Date, labels: Labels? = nil, tokenExpirationWarning: String? = nil) { + init(isBreak: Bool, lessonName: String, lessonTheme: String?, roomName: String?, teacherName: String?, startTime: Date, endTime: Date, lessonNumber: Int?, mode: String?, message: String?, season: String?, nextLessonName: String?, nextRoomName: String?, nextStartTime: Date?, isSubstitution: Bool, isCancelled: Bool, substituteTeacher: String?, currentTime: Date, labels: Labels? = nil, tokenExpirationWarning: String? = nil, compactTimerText: String? = nil, lessonIcon: String? = nil) { self.isBreak = isBreak self.lessonName = lessonName self.lessonTheme = lessonTheme @@ -89,50 +93,54 @@ struct TimetableActivityAttributes: ActivityAttributes { self.currentTime = currentTime self.labels = labels self.tokenExpirationWarning = tokenExpirationWarning + self.compactTimerText = compactTimerText + self.lessonIcon = lessonIcon } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - + isBreak = try container.decode(Bool.self, forKey: .isBreak) lessonName = try container.decode(String.self, forKey: .lessonName) lessonTheme = try container.decodeIfPresent(String.self, forKey: .lessonTheme) roomName = try container.decodeIfPresent(String.self, forKey: .roomName) teacherName = try container.decodeIfPresent(String.self, forKey: .teacherName) - + let startTimeStr = try container.decode(String.self, forKey: .startTime) guard let startTimeDate = isoFormatter.date(from: startTimeStr) else { throw DecodingError.dataCorruptedError(forKey: .startTime, in: container, debugDescription: "Invalid startTime format: \(startTimeStr)") } startTime = startTimeDate - + let endTimeStr = try container.decode(String.self, forKey: .endTime) guard let endTimeDate = isoFormatter.date(from: endTimeStr) else { throw DecodingError.dataCorruptedError(forKey: .endTime, in: container, debugDescription: "Invalid endTime format: \(endTimeStr)") } endTime = endTimeDate - + lessonNumber = try container.decodeIfPresent(Int.self, forKey: .lessonNumber) mode = try container.decodeIfPresent(String.self, forKey: .mode) message = try container.decodeIfPresent(String.self, forKey: .message) season = try container.decodeIfPresent(String.self, forKey: .season) nextLessonName = try container.decodeIfPresent(String.self, forKey: .nextLessonName) nextRoomName = try container.decodeIfPresent(String.self, forKey: .nextRoomName) - + if let nextStartTimeStr = try container.decodeIfPresent(String.self, forKey: .nextStartTime) { nextStartTime = isoFormatter.date(from: nextStartTimeStr) } else { nextStartTime = nil } - + isSubstitution = try container.decode(Bool.self, forKey: .isSubstitution) isCancelled = try container.decode(Bool.self, forKey: .isCancelled) substituteTeacher = try container.decodeIfPresent(String.self, forKey: .substituteTeacher) labels = try container.decodeIfPresent(Labels.self, forKey: .labels) tokenExpirationWarning = try container.decodeIfPresent(String.self, forKey: .tokenExpirationWarning) + compactTimerText = try container.decodeIfPresent(String.self, forKey: .compactTimerText) + lessonIcon = try container.decodeIfPresent(String.self, forKey: .lessonIcon) let currentTimeStr = try container.decode(String.self, forKey: .currentTime) guard let currentTimeDate = isoFormatter.date(from: currentTimeStr) else { @@ -140,43 +148,45 @@ struct TimetableActivityAttributes: ActivityAttributes { } currentTime = currentTimeDate } - + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - + let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - + try container.encode(isBreak, forKey: .isBreak) try container.encode(lessonName, forKey: .lessonName) try container.encodeIfPresent(lessonTheme, forKey: .lessonTheme) try container.encodeIfPresent(roomName, forKey: .roomName) try container.encodeIfPresent(teacherName, forKey: .teacherName) - + try container.encode(isoFormatter.string(from: startTime), forKey: .startTime) try container.encode(isoFormatter.string(from: endTime), forKey: .endTime) - + try container.encodeIfPresent(lessonNumber, forKey: .lessonNumber) try container.encodeIfPresent(mode, forKey: .mode) try container.encodeIfPresent(message, forKey: .message) try container.encodeIfPresent(season, forKey: .season) try container.encodeIfPresent(nextLessonName, forKey: .nextLessonName) try container.encodeIfPresent(nextRoomName, forKey: .nextRoomName) - + if let nextStartTime = nextStartTime { try container.encode(isoFormatter.string(from: nextStartTime), forKey: .nextStartTime) } - + try container.encode(isSubstitution, forKey: .isSubstitution) try container.encode(isCancelled, forKey: .isCancelled) try container.encodeIfPresent(substituteTeacher, forKey: .substituteTeacher) try container.encodeIfPresent(labels, forKey: .labels) try container.encodeIfPresent(tokenExpirationWarning, forKey: .tokenExpirationWarning) + try container.encodeIfPresent(compactTimerText, forKey: .compactTimerText) + try container.encodeIfPresent(lessonIcon, forKey: .lessonIcon) try container.encode(isoFormatter.string(from: currentTime), forKey: .currentTime) } } - + var studentName: String var schoolName: String } @@ -185,11 +195,11 @@ extension TimetableActivityAttributes.ContentState { var timeRemaining: TimeInterval { return endTime.timeIntervalSince(currentTime) } - + var isBeforeSchool: Bool { return currentTime < startTime && !isBreak } - + var formattedStartTime: String { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" @@ -197,7 +207,7 @@ extension TimetableActivityAttributes.ContentState { let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: startTime) ?? startTime return formatter.string(from: adjustedDate) } - + var formattedEndTime: String { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" @@ -205,7 +215,7 @@ extension TimetableActivityAttributes.ContentState { let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: endTime) ?? endTime return formatter.string(from: adjustedDate) } - + var formattedNextStartTime: String { guard let nextStartTime = nextStartTime else { return "" } let formatter = DateFormatter() @@ -214,18 +224,18 @@ extension TimetableActivityAttributes.ContentState { let adjustedDate = Calendar.current.date(byAdding: .hour, value: 1, to: nextStartTime) ?? nextStartTime return formatter.string(from: adjustedDate) } - + var timeRemainingText: String { let remaining = timeRemaining - + if remaining < 0 { return "0:00" } - + let hours = Int(remaining) / 3600 let minutes = (Int(remaining) % 3600) / 60 let seconds = Int(remaining) % 60 - + if hours > 0 { return String(format: "%d:%02d:%02d", hours, minutes, seconds) } else if minutes > 0 { @@ -234,7 +244,7 @@ extension TimetableActivityAttributes.ContentState { return String(format: "0:%02d", seconds) } } - + var seasonalRemainingText: String { if let remainingLabel = labels?.remainingLabel, let message = message, !message.isEmpty { return "\(remainingLabel): \(message)" @@ -248,7 +258,7 @@ extension TimetableActivityAttributes.ContentState { } return "Szünetből hátralévő idő: \(hours) óra" } - + var seasonalDisplayValue: String { if let message = message, !message.isEmpty { return message @@ -275,7 +285,7 @@ extension TimetableActivityAttributes.ContentState { "isCancelled": isCancelled, "currentTime": ISO8601DateFormatter().string(from: currentTime) ] - + if let lessonTheme = lessonTheme { json["lessonTheme"] = lessonTheme } @@ -312,10 +322,16 @@ extension TimetableActivityAttributes.ContentState { if let tokenExpirationWarning = tokenExpirationWarning { json["tokenExpirationWarning"] = tokenExpirationWarning } + if let compactTimerText = compactTimerText { + json["compactTimerText"] = compactTimerText + } + if let lessonIcon = lessonIcon { + json["lessonIcon"] = lessonIcon + } return json } - + static func fromJSON(_ json: [String: Any]) -> TimetableActivityAttributes.ContentState? { let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] @@ -333,7 +349,7 @@ extension TimetableActivityAttributes.ContentState { let currentTimeStr = json["currentTime"] as? String let currentTime = currentTimeStr.flatMap { isoFormatter.date(from: $0) } ?? Date() - + let nextStartTime: Date? if let nextStartTimeStr = json["nextStartTime"] as? String { nextStartTime = isoFormatter.date(from: nextStartTimeStr) @@ -382,8 +398,9 @@ extension TimetableActivityAttributes.ContentState { substituteTeacher: json["substituteTeacher"] as? String, currentTime: currentTime, labels: labels, - tokenExpirationWarning: json["tokenExpirationWarning"] as? String + tokenExpirationWarning: json["tokenExpirationWarning"] as? String, + compactTimerText: json["compactTimerText"] as? String, + lessonIcon: json["lessonIcon"] as? String ) } } - diff --git a/firka/ios/TimetableWidget/TimetableLiveActivity.swift b/firka/ios/TimetableWidget/TimetableLiveActivity.swift index 9fc70b3..c42aed8 100644 --- a/firka/ios/TimetableWidget/TimetableLiveActivity.swift +++ b/firka/ios/TimetableWidget/TimetableLiveActivity.swift @@ -45,9 +45,9 @@ struct TimetableLiveActivity: Widget { VStack(spacing: 4) { if mode == "beforeSchool" { HStack(alignment: .center, spacing: 6) { - Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season)) + Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season, lessonIcon: context.state.lessonIcon)) .font(.system(size: 18)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) Text(context.state.labels?.title ?? "Hamarosan suli") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) @@ -90,7 +90,7 @@ struct TimetableLiveActivity: Widget { HStack(alignment: .center, spacing: 6) { Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season)) .font(.system(size: 18)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) Text(context.state.message ?? "Szünet") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) @@ -100,7 +100,7 @@ struct TimetableLiveActivity: Widget { HStack(alignment: .center, spacing: 6) { Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season)) .font(.system(size: 18)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) Text(context.state.labels?.title ?? context.state.lessonName) .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) @@ -111,7 +111,7 @@ struct TimetableLiveActivity: Widget { HStack(alignment: .center, spacing: 6) { Image(systemName: "cup.and.saucer.fill") .font(.system(size: 18)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) Text(context.state.labels?.title ?? "Szünet") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) @@ -173,6 +173,7 @@ struct TimetableLiveActivity: Widget { } DynamicIslandExpandedRegion(.bottom) { + let season = context.state.season ?? "" VStack(spacing: 4) { HStack { Spacer() @@ -182,20 +183,20 @@ struct TimetableLiveActivity: Widget { if context.state.endTime > context.state.currentTime { Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true) .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } else { Text(context.state.formattedEndTime) .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } } else if mode == "seasonalBreak" { Text(context.state.seasonalRemainingText) .font(.system(size: 18, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } else if context.state.isCancelled ?? false { @@ -206,7 +207,7 @@ struct TimetableLiveActivity: Widget { } else { Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true) .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } @@ -234,59 +235,44 @@ struct TimetableLiveActivity: Widget { let season = context.state.season ?? "" let iconName: String = { if SeasonalIconHelper.isSeasonalMode(mode) || mode == "beforeSchool" { - return SeasonalIconHelper.iconName(for: mode, season: season) + return SeasonalIconHelper.iconName(for: mode, season: season, lessonIcon: context.state.lessonIcon) } else { - return context.state.isBreak ? "cup.and.saucer.fill" : "book.fill" + return context.state.isBreak ? "cup.and.saucer.fill" : (context.state.lessonIcon ?? "book.fill") } }() Image(systemName: iconName) .font(.system(size: 14)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) } compactTrailing: { - if mode == "seasonalBreak" { - // Seasonal break: static short format from backend message - let compactText = TimeFormatHelper.compactSeasonalBreak( - from: context.state.message ?? "", - labels: context.state.labels - ) - - Text(compactText) - .font(.system(size: 12, weight: .semibold, design: .rounded)) - .foregroundColor(.green) - .frame(width: 50) - } else if mode == "xmas" || mode == "newYearDay" { + let season = context.state.season ?? "" + if mode == "xmas" || mode == "newYearDay" { Text("") .frame(width: 50) } else if context.state.isCancelled ?? false { Text("❌") .font(.system(size: 12, weight: .semibold)) .frame(width: 50) + } else if let compactText = context.state.compactTimerText { + Text(compactText) + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) + .frame(width: 50) } else { - TimelineView(.periodic(from: context.state.currentTime, by: 60.0)) { timeline in - let remaining = context.state.endTime.timeIntervalSince(timeline.date) - let compactText = TimeFormatHelper.compactTime( - remaining: remaining, - labels: context.state.labels - ) - - Text(compactText) - .font(.system(size: 12, weight: .semibold, design: .rounded)) - .foregroundColor(.green) - .frame(width: 50) - } + Text("") + .frame(width: 50) } } minimal: { let season = context.state.season ?? "" let iconName: String = { if SeasonalIconHelper.isSeasonalMode(mode) || mode == "beforeSchool" { - return SeasonalIconHelper.iconName(for: mode, season: season) + return SeasonalIconHelper.iconName(for: mode, season: season, lessonIcon: context.state.lessonIcon) } else { - return context.state.isBreak ? "cup.and.saucer.fill" : "book.fill" + return context.state.isBreak ? "cup.and.saucer.fill" : (context.state.lessonIcon ?? "book.fill") } }() Image(systemName: iconName) .font(.system(size: 12)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) } } } @@ -310,17 +296,17 @@ struct TimetableLiveActivityView: View { }() : nil let iconName: String = { if SeasonalIconHelper.isSeasonalMode(mode) || mode == "beforeSchool" { - return SeasonalIconHelper.iconName(for: mode, season: season) + return SeasonalIconHelper.iconName(for: mode, season: season, lessonIcon: context.state.lessonIcon) } else { - return context.state.isBreak ? "cup.and.saucer.fill" : "book.fill" + return context.state.isBreak ? "cup.and.saucer.fill" : (context.state.lessonIcon ?? "book.fill") } }() - + // Header HStack { Image(systemName: iconName) .font(.system(size: 24)) - .foregroundColor(SeasonalIconHelper.iconColor(for: mode)) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode, season: season)) VStack(alignment: .leading, spacing: 2) { if mode == "beforeSchool" { @@ -496,17 +482,17 @@ struct TimetableLiveActivityView: View { Text(context.state.labels?.timerLabel ?? "Első óra kezdése") .font(.system(size: 10)) .foregroundColor(.gray) - + if context.state.endTime > context.state.currentTime { Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true) .font(.system(size: 32, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode3, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } else { Text(context.state.formattedEndTime) .font(.system(size: 32, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode3, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } @@ -516,7 +502,7 @@ struct TimetableLiveActivityView: View { .foregroundColor(.gray) Text(context.state.seasonalDisplayValue) .font(.system(size: 32, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode3, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } else { @@ -542,7 +528,7 @@ struct TimetableLiveActivityView: View { .foregroundColor(.gray) Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true) .font(.system(size: 32, weight: .bold, design: .rounded)) - .foregroundColor(.green) + .foregroundColor(SeasonalIconHelper.iconColor(for: mode3, season: season)) .multilineTextAlignment(.center) .monospacedDigit() } diff --git a/firka/lib/helpers/live_activity_service.dart b/firka/lib/helpers/live_activity_service.dart index c46849b..f3bbb07 100644 --- a/firka/lib/helpers/live_activity_service.dart +++ b/firka/lib/helpers/live_activity_service.dart @@ -41,6 +41,17 @@ class LiveActivityService { static bool? _lastSentMorningNotificationEnabled; static const Duration _morningNotificationDebounceInterval = Duration(seconds: 3); + static bool _tokenExpired = false; + + /// Check if token has expired (for UI notification) + static bool get isTokenExpired => _tokenExpired; + + /// Clear token expiration flag (call after successful login) + static void clearTokenExpiration() { + _tokenExpired = false; + _logger.info('Token expiration flag cleared'); + } + /// Get current bellDelay value from settings static double? _getCurrentBellDelay() { try { @@ -733,6 +744,11 @@ class LiveActivityService { searchStartDate: nextWeekStart, ); + if (searchResult.tokenExpired) { + _tokenExpired = true; + _logger.warning('[GlobalSearch] onUserLogin: Token expired during global search'); + } + allLessons.addAll(searchResult.allLessons); _logger.info('[GlobalSearch] onUserLogin: Global searcher returned ${searchResult.allLessons.length} lessons'); @@ -1038,6 +1054,11 @@ class LiveActivityService { searchStartDate: nextWeekStart, ); + if (searchResult.tokenExpired) { + _tokenExpired = true; + _logger.warning('[GlobalSearch] Token expired during global search'); + } + allLessons.addAll(searchResult.allLessons); _logger.info('[GlobalSearch] Global searcher returned ${searchResult.allLessons.length} lessons'); @@ -1596,6 +1617,7 @@ class LiveActivityService { List allLessons = []; Lesson? lastBreakDay; Lesson? firstSchoolDayAfterBreak; + bool tokenExpired = false; DateTime currentSearchDate = searchStartDate; int weeksSearched = 0; @@ -1650,7 +1672,58 @@ class LiveActivityService { _logger.info('[GlobalBreakSearcher] Week ${weeksSearched + 1} returned no data, continuing search...'); } } catch (e) { - _logger.warning('[GlobalBreakSearcher] Error fetching week ${weeksSearched + 1}: $e'); + final isTokenError = e.toString().contains('TokenExpiredException') || + e.toString().contains('InvalidGrantException'); + + if (isTokenError) { + tokenExpired = true; + _logger.warning('[GlobalBreakSearcher] Token expired during week ${weeksSearched + 1}, falling back to cache'); + } else { + _logger.warning('[GlobalBreakSearcher] Error fetching week ${weeksSearched + 1}: $e'); + } + + try { + final cachedResponse = await client.getTimeTable(weekStart, weekEnd, forceCache: true); + + if (cachedResponse.response != null && cachedResponse.response!.isNotEmpty) { + final weekLessons = cachedResponse.response!; + _logger.info('[GlobalBreakSearcher] Loaded ${weekLessons.length} lessons from cache for week ${weeksSearched + 1}'); + + final breakEvents = weekLessons.where((lesson) { + final uid = lesson.uid.toLowerCase(); + final name = lesson.name.toLowerCase(); + return uid.contains('tanevrendjeesemeny') && + !name.contains('tanítási nap') && + (name.contains('pihenőnap') || + name.contains('munkaszüneti') || + name.contains('ünnepnap') || + name.contains('tanítás nélküli') || + name.contains('nem órarendi nap')); + }).toList(); + + final schoolLessons = weekLessons.where((lesson) { + final uid = lesson.uid.toLowerCase(); + return uid.contains('orarendiora') || uid.contains('tanitasiora') || uid.contains('uresora'); + }).toList(); + + allLessons.addAll(weekLessons); + + if (breakEvents.isNotEmpty) { + breakEvents.sort((a, b) => a.start.compareTo(b.start)); + lastBreakDay = breakEvents.last; + _logger.info('[GlobalBreakSearcher] Found ${breakEvents.length} break event(s) in cached week ${weeksSearched + 1}, last: ${lastBreakDay.name} on ${lastBreakDay.date.split('T')[0]}'); + } else if (schoolLessons.isNotEmpty) { + schoolLessons.sort((a, b) => a.start.compareTo(b.start)); + firstSchoolDayAfterBreak = schoolLessons.first; + _logger.info('[GlobalBreakSearcher] Found first school day after break in cache: ${firstSchoolDayAfterBreak.name} on ${firstSchoolDayAfterBreak.date.split('T')[0]}'); + break; + } + } else { + _logger.info('[GlobalBreakSearcher] No cache available for week ${weeksSearched + 1}'); + } + } catch (cacheError) { + _logger.warning('[GlobalBreakSearcher] Cache fallback also failed for week ${weeksSearched + 1}: $cacheError'); + } } currentSearchDate = currentSearchDate.add(const Duration(days: 7)); @@ -1665,12 +1738,13 @@ class LiveActivityService { _logger.warning('[GlobalBreakSearcher] Reached maximum search limit ($maxWeeks weeks)'); } - _logger.info('[GlobalBreakSearcher] Search completed: ${allLessons.length} lessons found, last break: ${lastBreakDay?.date.split('T')[0] ?? 'none'}, first school day: ${firstSchoolDayAfterBreak?.date.split('T')[0] ?? 'none'}'); + _logger.info('[GlobalBreakSearcher] Search completed: ${allLessons.length} lessons found, last break: ${lastBreakDay?.date.split('T')[0] ?? 'none'}, first school day: ${firstSchoolDayAfterBreak?.date.split('T')[0] ?? 'none'}, tokenExpired: $tokenExpired'); return _GlobalSearchResult( allLessons: allLessons, lastBreakDay: lastBreakDay, firstSchoolDayAfterBreak: firstSchoolDayAfterBreak, + tokenExpired: tokenExpired, ); } } @@ -1680,10 +1754,12 @@ class _GlobalSearchResult { final List allLessons; final Lesson? lastBreakDay; final Lesson? firstSchoolDayAfterBreak; + final bool tokenExpired; _GlobalSearchResult({ required this.allLessons, this.lastBreakDay, this.firstSchoolDayAfterBreak, + this.tokenExpired = false, }); } \ No newline at end of file diff --git a/firka/lib/ui/phone/pages/extras/main_reauth.dart b/firka/lib/ui/phone/pages/extras/main_reauth.dart new file mode 100644 index 0000000..53331d9 --- /dev/null +++ b/firka/lib/ui/phone/pages/extras/main_reauth.dart @@ -0,0 +1,61 @@ +import 'package:firka/helpers/settings.dart'; +import 'package:firka/main.dart'; +import 'package:firka/ui/model/style.dart'; +import 'package:firka/ui/phone/widgets/login_webview.dart'; +import 'package:flutter/material.dart'; + +void showReauthBottomSheet(BuildContext context, AppInitialization data, String message) { + final accountPicker = (data.settings + .group("profile_settings")["e_kreta_account_picker"] + as SettingsKretenAccountPicker); + + final currentToken = data.tokens.isNotEmpty && accountPicker.accountIndex < data.tokens.length + ? data.tokens[accountPicker.accountIndex] + : null; + + final username = currentToken?.studentId; + final schoolId = currentToken?.iss; + + showModalBottomSheet( + context: context, + elevation: 100, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Container( + decoration: BoxDecoration( + color: appStyle.colors.background, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: appStyle.colors.errorCard, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Text( + message, + style: appStyle.fonts.B_16R.copyWith( + color: appStyle.colors.errorText, + ), + textAlign: TextAlign.center, + ), + ), + Expanded( + child: LoginWebviewWidget( + data, + username: username, + schoolId: schoolId, + ), + ), + ], + ), + ); + }, + ); +} diff --git a/firka/lib/ui/phone/pages/extras/reauth_toast.dart b/firka/lib/ui/phone/pages/extras/reauth_toast.dart new file mode 100644 index 0000000..27b1dc0 --- /dev/null +++ b/firka/lib/ui/phone/pages/extras/reauth_toast.dart @@ -0,0 +1,90 @@ +import 'package:firka/main.dart'; +import 'package:firka/ui/model/style.dart'; +import 'package:firka/ui/phone/pages/extras/main_reauth.dart'; +import 'package:firka/ui/widget/firka_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:majesticons_flutter/majesticons_flutter.dart'; + +class ReauthToastWidget extends StatefulWidget { + final AppInitialization data; + final Function() onDismiss; + + const ReauthToastWidget({ + Key? key, + required this.data, + required this.onDismiss, + }) : super(key: key); + + @override + State createState() => _ReauthToastWidgetState(); +} + +class _ReauthToastWidgetState extends State { + double _dragOffset = 0; + + @override + Widget build(BuildContext context) { + return Positioned( + top: MediaQuery.of(context).size.height / 1.6 + _dragOffset, + left: 0.0, + right: 0.0, + bottom: 0, + child: Center( + child: GestureDetector( + onTap: () { + showReauthBottomSheet(context, widget.data, widget.data.l10n.reauth); + }, + onVerticalDragUpdate: (details) { + setState(() { + _dragOffset += details.delta.dy; + if (_dragOffset < 0) _dragOffset = 0; + }); + }, + onVerticalDragEnd: (details) { + if (_dragOffset > 50) { + widget.onDismiss(); + } else { + setState(() { + _dragOffset = 0; + }); + } + }, + child: Card( + color: appStyle.colors.errorCard, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(200)), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.data.l10n.reauth, + style: appStyle.fonts.B_16SB + .copyWith(color: appStyle.colors.errorText), + ), + SizedBox(width: 8), + FirkaIconWidget( + FirkaIconType.majesticons, + Majesticon.loginSolid, + color: appStyle.colors.errorAccent, + size: 24, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +Widget buildReauthToast(BuildContext context, AppInitialization data, Function() onDismiss) { + return ReauthToastWidget( + data: data, + onDismiss: onDismiss, + ); +} diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart index 0fb2b24..408989d 100644 --- a/firka/lib/ui/phone/screens/home/home_screen.dart +++ b/firka/lib/ui/phone/screens/home/home_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:firka/helpers/api/client/kreta_stream.dart'; +import 'package:firka/helpers/api/exceptions/token.dart'; import 'package:firka/helpers/extensions.dart'; import 'package:firka/helpers/live_activity_service.dart'; import 'package:firka/helpers/settings.dart'; @@ -9,6 +10,8 @@ import 'package:firka/helpers/update_notifier.dart'; import 'package:firka/main.dart'; import 'package:firka/ui/model/style.dart'; import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart'; +import 'package:firka/ui/phone/pages/extras/main_reauth.dart'; +import 'package:firka/ui/phone/pages/extras/reauth_toast.dart'; import 'package:firka/ui/phone/pages/home/home_grades.dart'; import 'package:firka/ui/phone/pages/home/home_main.dart'; import 'package:firka/ui/phone/pages/home/home_subpage.dart'; @@ -133,7 +136,29 @@ class _HomeScreenState extends FirkaState { await HomeWidget.updateWidget( qualifiedAndroidName: "app.firka.naplo.glance.TimetableWidget"); } + + if (Platform.isIOS && LiveActivityService.isTokenExpired && !_disposed) { + showReauthBottomSheet(context, widget.data, widget.data.l10n.reauth); + } + } catch (e) { + if (e is TokenExpiredException || e is InvalidGrantException) { + activeToast = ActiveToastType.reauth; + + if (_disposed) return; + setState(() { + toast = buildReauthToast(context, widget.data, () { + if (!_disposed) { + setState(() { + activeToast = ActiveToastType.none; + toast = null; + }); + } + }); + }); + return; + } + activeToast = ActiveToastType.error; var dismissDelay = 120; @@ -167,7 +192,6 @@ class _HomeScreenState extends FirkaState { padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, - // Use min to prevent filling the width children: [ Text( widget.data.l10n.api_error,