Add splash assets and watch token recovery

Add Android and iOS launch/splash assets and flutter_native_splash config, and update launch_background.xml/styles for Android 12 support and dark mode.

Implement robust Apple Watch token recovery and refresh behavior: add recovery state and async recovery flow to DataStore (iCloud check, API refresh, request from iPhone), show a recovering UI in ContentView, attempt proactive recoveries on startup, and add WatchToken/iCloudTokenManager model support. WatchConnectivityManager now attempts to refresh expired tokens before replying to iPhone requests. BackgroundRefreshManager scheduling was improved to choose intervals based on timetable and user settings with debug logs. SettingsView defaults to Auto (0) and adds the Auto picker option and localization strings.

Also update entitlements across watch/widget extensions to include com.apple.developer.ubiquity-kvstore-identifier, rename watch app icon entry, and add project.pbxproj references for new source files.
This commit is contained in:
Horváth Gergely
2026-02-06 22:23:18 +01:00
parent b58e60a1f8
commit 0f3dcf58a5
67 changed files with 941 additions and 206 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#7ca120</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#7ca120</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -0,0 +1,16 @@
flutter_native_splash:
color: "#7ca120"
image: assets/images/logos/splash.png
# Dark mode - same color as light mode for consistency
color_dark: "#7ca120"
image_dark: assets/images/logos/splash.png
android_12:
image: assets/images/logos/splash.png
color: "#7ca120"
color_dark: "#7ca120"
image_dark: assets/images/logos/splash.png
ios: true
web: false

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Icon-App-1024x1024@1x.png",
"filename" : "Icon-1024.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"

View File

@@ -8,8 +8,20 @@ struct ContentView: View {
var body: some View {
Group {
if dataStore.needsReauth && dataStore.hasToken {
if dataStore.isRecoveringToken {
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.2)
Text("recovering_token".localized)
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
else if dataStore.needsReauth && dataStore.hasToken {
ReauthRequiredView(onTokenReceived: {
dataStore.resetRecoveryState()
dataStore.checkTokenState()
Task {
await dataStore.refreshAll()
@@ -30,7 +42,15 @@ struct ContentView: View {
dataStore.loadFromCache()
if dataStore.hasToken {
await dataStore.refreshTokenProactively()
await dataStore.refreshAll()
if (dataStore.error == "token_expired" || dataStore.error == "no_token") && !dataStore.recoveryAttempted {
let recovered = await dataStore.attemptTokenRecovery()
if recovered {
await dataStore.refreshAll()
}
}
} else {
requestToken()
}

View File

@@ -6,5 +6,7 @@
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@@ -8,6 +8,7 @@ struct FirkaWatchApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.dark)
}
}
}

View File

@@ -147,6 +147,7 @@ class WatchL10n {
// Settings
"settings": "Beállítások",
"refresh_interval": "Frissítési időköz",
"auto": "Automatikus",
"15_minutes": "15 perc",
"30_minutes": "30 perc",
"1_hour": "1 óra",
@@ -183,6 +184,7 @@ class WatchL10n {
"sync_failed": "Sikertelen",
"phone_not_reachable": "iPhone nem elérhető",
"connecting": "Kapcsolódás...",
"recovering_token": "Token helyreállítása...",
]
private static let englishStrings: [String: String] = [
@@ -230,6 +232,7 @@ class WatchL10n {
// Settings
"settings": "Settings",
"refresh_interval": "Refresh Interval",
"auto": "Auto",
"15_minutes": "15 minutes",
"30_minutes": "30 minutes",
"1_hour": "1 hour",
@@ -266,6 +269,7 @@ class WatchL10n {
"sync_failed": "Failed",
"phone_not_reachable": "iPhone not reachable",
"connecting": "Connecting...",
"recovering_token": "Recovering session...",
]
private static let germanStrings: [String: String] = [
@@ -313,6 +317,7 @@ class WatchL10n {
// Settings
"settings": "Einstellungen",
"refresh_interval": "Aktualisierungsintervall",
"auto": "Automatisch",
"15_minutes": "15 Minuten",
"30_minutes": "30 Minuten",
"1_hour": "1 Stunde",
@@ -349,6 +354,7 @@ class WatchL10n {
"sync_failed": "Fehlgeschlagen",
"phone_not_reachable": "iPhone nicht erreichbar",
"connecting": "Verbindung...",
"recovering_token": "Sitzung wiederherstellen...",
]
}

View File

@@ -8,30 +8,108 @@ class BackgroundRefreshManager {
private init() {}
func scheduleNextRefresh() {
let calendar = Calendar.current
let now = Date()
let hour = calendar.component(.hour, from: now)
let weekday = calendar.component(.weekday, from: now)
let isWeekday = weekday >= 2 && weekday <= 6
let interval = calculateOptimalRefreshInterval()
let interval: TimeInterval
if isWeekday && hour >= 6 && hour <= 16 {
interval = 15 * 60 // 15 minutes during school hours
} else {
interval = 60 * 60 // 1 hour outside school hours
}
let preferredDate = now.addingTimeInterval(interval)
let preferredDate = Date().addingTimeInterval(interval)
WKApplication.shared().scheduleBackgroundRefresh(
withPreferredDate: preferredDate,
userInfo: nil
) { error in
if let error = error {
print("[BackgroundRefresh] Schedule error: \(error)")
} else {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
print("[BackgroundRefresh] Next refresh scheduled at: \(formatter.string(from: preferredDate)) (\(Int(interval/60)) min)")
}
}
}
private func calculateOptimalRefreshInterval() -> TimeInterval {
let userRefreshMinutes = UserDefaults.standard.integer(forKey: "refreshInterval")
let now = Date()
let calendar = Calendar.current
let todayLessons = getTodayLessons()
guard !todayLessons.isEmpty else {
return getDefaultInterval(userSetting: userRefreshMinutes, now: now, calendar: calendar)
}
let sortedLessons = todayLessons.sorted { $0.start < $1.start }
guard let firstLesson = sortedLessons.first,
let lastLesson = sortedLessons.last else {
return getDefaultInterval(userSetting: userRefreshMinutes, now: now, calendar: calendar)
}
let firstStart = firstLesson.start
let lastEnd = lastLesson.end
let schoolStartBuffer = firstStart.addingTimeInterval(-30 * 60)
if now < schoolStartBuffer {
let intervalUntilWakeUp = schoolStartBuffer.timeIntervalSince(now)
let interval = max(intervalUntilWakeUp, 15 * 60)
print("[BackgroundRefresh] Before school - next refresh in \(Int(interval/60)) min (30 min before first lesson)")
return min(interval, 60 * 60) // Max 1 hour
}
if now >= schoolStartBuffer && now <= lastEnd {
let interval = TimeInterval((userRefreshMinutes > 0 ? userRefreshMinutes : 15) * 60)
print("[BackgroundRefresh] During school - using \(Int(interval/60)) min interval")
return interval
}
let tomorrowLessons = getTomorrowLessons()
if !tomorrowLessons.isEmpty,
let tomorrowFirst = tomorrowLessons.sorted(by: { $0.start < $1.start }).first {
let tomorrowStartBuffer = tomorrowFirst.start.addingTimeInterval(-30 * 60)
let timeUntilTomorrowWakeUp = tomorrowStartBuffer.timeIntervalSince(now)
if timeUntilTomorrowWakeUp > 2 * 60 * 60 {
print("[BackgroundRefresh] After school - 1 hour interval (tomorrow's first lesson in \(Int(timeUntilTomorrowWakeUp/60)) min)")
return 60 * 60
} else {
print("[BackgroundRefresh] After school, tomorrow soon - 30 min interval")
return 30 * 60
}
}
print("[BackgroundRefresh] After school, no tomorrow lessons - 1 hour interval")
return 60 * 60
}
private func getDefaultInterval(userSetting: Int, now: Date, calendar: Calendar) -> TimeInterval {
if userSetting > 0 {
print("[BackgroundRefresh] No timetable - using user setting: \(userSetting) min")
return TimeInterval(userSetting * 60)
}
let hour = calendar.component(.hour, from: now)
let weekday = calendar.component(.weekday, from: now)
let isWeekday = weekday >= 2 && weekday <= 6
if isWeekday && hour >= 6 && hour <= 16 {
print("[BackgroundRefresh] No timetable - weekday school hours: 15 min")
return 15 * 60
} else {
print("[BackgroundRefresh] No timetable - off hours: 1 hour")
return 60 * 60
}
}
private func getTodayLessons() -> [WidgetLesson] {
guard let data = DataStore.shared.data else { return [] }
return data.timetable.today
}
private func getTomorrowLessons() -> [WidgetLesson] {
guard let data = DataStore.shared.data else { return [] }
return data.timetable.tomorrow
}
func handleBackgroundRefresh() async {
await DataStore.shared.refreshAll()

View File

@@ -1,6 +1,7 @@
import Foundation
import Observation
import WidgetKit
import WatchConnectivity
// MARK: - Cache Wrapper
@@ -20,10 +21,14 @@ class DataStore {
var isLoading: Bool = false
var error: String?
var isRecoveringToken: Bool = false
private(set) var recoveryAttempted: Bool = false
private(set) var hasToken: Bool = false
var needsReauth: Bool {
error == "token_expired" || error == "no_token"
(error == "token_expired" || error == "no_token") && recoveryAttempted && !isRecoveringToken
}
private let appGroupID = "group.app.firka.firkaa"
@@ -136,6 +141,117 @@ class DataStore {
print("[Watch] Reauth required state set")
}
func resetRecoveryState() {
recoveryAttempted = false
error = nil
print("[Watch] Recovery state reset")
}
func attemptTokenRecovery() async -> Bool {
guard !isRecoveringToken else {
print("[Watch] Token recovery already in progress")
return false
}
isRecoveringToken = true
error = nil
print("[Watch] Starting background token recovery...")
defer {
isRecoveringToken = false
}
print("[Watch] Recovery Step 1: Checking iCloud for updated token...")
if let iCloudToken = iCloudTokenManager.shared.loadToken() {
if !isTokenExpired(iCloudToken) {
print("[Watch] Recovery: Found valid token in iCloud!")
try? TokenManager.shared.saveToken(iCloudToken)
checkTokenState()
return true
} else {
print("[Watch] Recovery: iCloud token is expired, trying refresh...")
}
}
print("[Watch] Recovery Step 2: Attempting API token refresh...")
do {
_ = try await TokenManager.shared.refreshToken()
print("[Watch] Recovery: Token refresh succeeded!")
checkTokenState()
return true
} catch {
print("[Watch] Recovery: API token refresh failed: \(error)")
}
print("[Watch] Recovery Step 3: Checking if iPhone is reachable...")
if await requestTokenFromiPhoneAsync() {
print("[Watch] Recovery: Got token from iPhone!")
checkTokenState()
return true
}
print("[Watch] Recovery: All attempts failed, will show reauth screen")
recoveryAttempted = true
self.error = "token_expired"
return false
}
private func isTokenExpired(_ token: WatchToken) -> Bool {
let expiryThreshold = token.expiryDate.addingTimeInterval(-60)
return Date() >= expiryThreshold
}
private func requestTokenFromiPhoneAsync() async -> Bool {
return await withCheckedContinuation { continuation in
guard WCSession.default.activationState == .activated,
WCSession.default.isReachable else {
print("[Watch] Recovery: iPhone not reachable")
continuation.resume(returning: false)
return
}
print("[Watch] Recovery: Requesting token from iPhone...")
WCSession.default.sendMessage(
["action": "requestToken"],
replyHandler: { response in
if let authDict = response["auth"] as? [String: Any] {
print("[Watch] Recovery: Received token from iPhone")
self.processAuthDataSync(authDict)
continuation.resume(returning: true)
} else {
print("[Watch] Recovery: iPhone returned no token")
continuation.resume(returning: false)
}
},
errorHandler: { error in
print("[Watch] Recovery: iPhone request failed: \(error.localizedDescription)")
continuation.resume(returning: false)
}
)
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
}
}
}
private func processAuthDataSync(_ authDict: [String: Any]) {
do {
let jsonData = try JSONSerialization.data(withJSONObject: authDict)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let timestamp = try container.decode(Int64.self)
return Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
}
let token = try decoder.decode(WatchToken.self, from: jsonData)
try TokenManager.shared.saveToken(token)
print("[Watch] Recovery: Token saved from iPhone")
} catch {
print("[Watch] Recovery: Failed to process auth data: \(error)")
}
}
private func refreshComplications() {
WidgetCenter.shared.reloadAllTimelines()
print("[Watch] Complications refreshed")

View File

@@ -79,12 +79,43 @@ class WatchConnectivityManager: NSObject, WCSessionDelegate {
}
private func handleGetTokenRequest(replyHandler: @escaping ([String: Any]) -> Void) {
guard let token = TokenManager.shared.loadToken() else {
guard TokenManager.shared.loadToken() != nil else {
print("[Watch] No token to send to iPhone")
replyHandler(["error": "no_token"])
return
}
if TokenManager.shared.isTokenExpired() {
print("[Watch] Token expired, attempting refresh before sending to iPhone...")
Task {
do {
let freshToken = try await KretaAPIClient.shared.getValidToken()
print("[Watch] Token refresh succeeded, sending fresh token to iPhone")
let tokenData: [String: Any] = [
"studentId": freshToken.studentId,
"studentIdNorm": freshToken.studentIdNorm,
"iss": freshToken.iss,
"idToken": freshToken.idToken,
"accessToken": freshToken.accessToken,
"refreshToken": freshToken.refreshToken,
"expiryDate": Int64(freshToken.expiryDate.timeIntervalSince1970 * 1000)
]
replyHandler(["token": tokenData])
} catch {
print("[Watch] Token refresh failed after all retries: \(error)")
replyHandler(["error": "refresh_failed"])
}
}
return
}
guard let token = TokenManager.shared.loadToken() else {
replyHandler(["error": "no_token"])
return
}
let tokenData: [String: Any] = [
"studentId": token.studentId,
"studentIdNorm": token.studentIdNorm,

View File

@@ -1,7 +1,7 @@
import SwiftUI
struct SettingsView: View {
@AppStorage("refreshInterval") private var refreshInterval: Int = 15
@AppStorage("refreshInterval") private var refreshInterval: Int = 0
@State private var l10n = WatchL10n.shared
var body: some View {
@@ -30,6 +30,7 @@ struct SettingsView: View {
Section("refresh".localized) {
Picker("refresh_interval".localized, selection: $refreshInterval) {
Text("auto".localized).tag(0)
Text("15_minutes".localized).tag(15)
Text("30_minutes".localized).tag(30)
Text("1_hour".localized).tag(60)

View File

@@ -6,5 +6,7 @@
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@@ -6,5 +6,7 @@
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@@ -8,5 +8,7 @@
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 70;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -17,6 +17,10 @@
4F30C7692E8FBF9D008BB46C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */; };
4F30C7782E8FBF9F008BB46C /* LiveActivityWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4F30C7652E8FBF9D008BB46C /* LiveActivityWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4F45A1752F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F45A1732F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift */; };
4F5824802F35468E00B92EA7 /* iCloudTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F58247F2F35468D00B92EA7 /* iCloudTokenManager.swift */; };
4F5824812F35468E00B92EA7 /* iCloudTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F58247F2F35468D00B92EA7 /* iCloudTokenManager.swift */; };
4F5824832F3548B800B92EA7 /* WatchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5824822F3548B800B92EA7 /* WatchToken.swift */; };
4F5824842F3548B800B92EA7 /* WatchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5824822F3548B800B92EA7 /* WatchToken.swift */; };
4F5965F82F2F0C1600A3DB03 /* WatchSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5965F72F2F0C1600A3DB03 /* WatchSessionManager.swift */; };
4F5965FE2F2F0EAF00A3DB03 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */; };
4F5965FF2F2F0EAF00A3DB03 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */; };
@@ -178,6 +182,8 @@
4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
4F45A1732F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetMethodChannel.swift; sourceTree = "<group>"; };
4F58247F2F35468D00B92EA7 /* iCloudTokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudTokenManager.swift; sourceTree = "<group>"; };
4F5824822F3548B800B92EA7 /* WatchToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchToken.swift; sourceTree = "<group>"; };
4F5965F72F2F0C1600A3DB03 /* WatchSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSessionManager.swift; sourceTree = "<group>"; };
4F5965FD2F2F0EAF00A3DB03 /* FirkaWatchComplicationsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FirkaWatchComplicationsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
4F5966162F2F0F6500A3DB03 /* FirkaWatchComplicationsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirkaWatchComplicationsExtension.entitlements; sourceTree = "<group>"; };
@@ -216,35 +222,35 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
4F0EA0512F2BD2A2003CC89E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
4F0EA0512F2BD2A2003CC89E /* Exceptions for "HomeWidgetsExtension" folder in "Runner" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Controls/AppControls.swift,
);
target = 97C146ED1CF9000F007C117D /* Runner */;
};
4F4E70D02EF565FF00C90AD1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
4F4E70D02EF565FF00C90AD1 /* Exceptions for "LiveActivityWidget" folder in "Runner" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
ActivityAttributes.swift,
);
target = 97C146ED1CF9000F007C117D /* Runner */;
};
4F5966082F2F0EB100A3DB03 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
4F5966082F2F0EB100A3DB03 /* Exceptions for "FirkaWatchComplications" folder in "FirkaWatchComplicationsExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4F5965FC2F2F0EAF00A3DB03 /* FirkaWatchComplicationsExtension */;
};
4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "LiveActivityWidget" folder in "LiveActivityWidget" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4F30C7642E8FBF9D008BB46C /* LiveActivityWidget */;
};
4FE64E472F27B07B006F9205 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
@@ -254,10 +260,55 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
4F30C76A2E8FBF9D008BB46C /* LiveActivityWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F4E70D02EF565FF00C90AD1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = LiveActivityWidget; sourceTree = "<group>"; };
4F5966002F2F0EAF00A3DB03 /* FirkaWatchComplications */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F5966082F2F0EB100A3DB03 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FirkaWatchComplications; sourceTree = "<group>"; };
4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F0EA0512F2BD2A2003CC89E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 4FE64E472F27B07B006F9205 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = HomeWidgetsExtension; sourceTree = "<group>"; };
4FF81B7B2F2EB4C100E95BA0 /* FirkaWatch Watch App */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "FirkaWatch Watch App"; sourceTree = "<group>"; };
4F30C76A2E8FBF9D008BB46C /* LiveActivityWidget */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4F4E70D02EF565FF00C90AD1 /* Exceptions for "LiveActivityWidget" folder in "Runner" target */,
4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "LiveActivityWidget" folder in "LiveActivityWidget" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = LiveActivityWidget;
sourceTree = "<group>";
};
4F5966002F2F0EAF00A3DB03 /* FirkaWatchComplications */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4F5966082F2F0EB100A3DB03 /* Exceptions for "FirkaWatchComplications" folder in "FirkaWatchComplicationsExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = FirkaWatchComplications;
sourceTree = "<group>";
};
4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4F0EA0512F2BD2A2003CC89E /* Exceptions for "HomeWidgetsExtension" folder in "Runner" target */,
4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = HomeWidgetsExtension;
sourceTree = "<group>";
};
4FF81B7B2F2EB4C100E95BA0 /* FirkaWatch Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = "FirkaWatch Watch App";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -339,6 +390,7 @@
4F7701ED2F2EC2F500B79171 /* KretaAPIClient.swift */,
4F7701EE2F2EC2F500B79171 /* TokenManager.swift */,
4FCB030C2F330F3B00418E63 /* KretaAPIModels.swift */,
4F58247F2F35468D00B92EA7 /* iCloudTokenManager.swift */,
);
path = API;
sourceTree = "<group>";
@@ -360,6 +412,7 @@
4F7701D32F2EC1AA00B79171 /* Subject.swift */,
4F7701D42F2EC1AA00B79171 /* WidgetColors.swift */,
4F7701D52F2EC1AA00B79171 /* WidgetData.swift */,
4F5824822F3548B800B92EA7 /* WatchToken.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -367,9 +420,9 @@
4F7701D72F2EC1AA00B79171 /* Shared */ = {
isa = PBXGroup;
children = (
4F7701D62F2EC1AA00B79171 /* Models */,
4F7701CD2F2EC1AA00B79171 /* API */,
4F7701CF2F2EC1AA00B79171 /* Helpers */,
4F7701D62F2EC1AA00B79171 /* Models */,
);
path = Shared;
sourceTree = SOURCE_ROOT;
@@ -703,14 +756,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -767,7 +816,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
D576F90540C8E625A9A12317 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -799,14 +848,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -872,9 +917,11 @@
4F7701E62F2EC1AA00B79171 /* Grade.swift in Sources */,
4F7701E72F2EC1AA00B79171 /* WidgetColors.swift in Sources */,
4F7701E82F2EC1AA00B79171 /* Average.swift in Sources */,
4F5824842F3548B800B92EA7 /* WatchToken.swift in Sources */,
4FCB030D2F330F3B00418E63 /* KretaAPIModels.swift in Sources */,
4F7701E92F2EC1AA00B79171 /* SeasonalIconHelper.swift in Sources */,
4F7701EA2F2EC1AA00B79171 /* Lesson.swift in Sources */,
4F5824802F35468E00B92EA7 /* iCloudTokenManager.swift in Sources */,
4F7701EB2F2EC1AA00B79171 /* Subject.swift in Sources */,
4F7701EC2F2EC1AA00B79171 /* WidgetData.swift in Sources */,
4F7701EF2F2EC2F500B79171 /* TokenManager.swift in Sources */,
@@ -891,6 +938,8 @@
4F30C7592E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift in Sources */,
4F45A1752F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift in Sources */,
4F5965F82F2F0C1600A3DB03 /* WatchSessionManager.swift in Sources */,
4F5824832F3548B800B92EA7 /* WatchToken.swift in Sources */,
4F5824812F35468E00B92EA7 /* iCloudTokenManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1027,7 +1076,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1021;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -1059,7 +1108,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1078,7 +1127,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1095,7 +1144,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1122,7 +1171,7 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1173,7 +1222,7 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1221,7 +1270,7 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1268,7 +1317,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1321,7 +1370,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1371,7 +1420,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1422,7 +1471,7 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1473,7 +1522,7 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1520,7 +1569,7 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1567,7 +1616,6 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1620,7 +1668,6 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1670,7 +1717,6 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1828,7 +1874,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1021;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -1864,7 +1910,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1021;
CURRENT_PROJECT_VERSION = 1062;
DEVELOPMENT_TEAM = UT7MSP4GWZ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -20,6 +20,20 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4FF81B792F2EB4C100E95BA0"
BuildableName = "FirkaWatch Watch App.app"
BlueprintName = "FirkaWatch Watch App"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@@ -97,5 +111,23 @@
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Increment Build Number"
scriptText = "cd &quot;${SRCROOT}&quot;&#10;/usr/bin/xcrun agvtool next-version -all&#10;echo &quot;Build number incremented&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "darkbackground.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,23 +1,56 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "LaunchImageDark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "LaunchImageDark@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "LaunchImageDark@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
@@ -32,6 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="LaunchImage" width="600" height="600"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>

View File

@@ -1,87 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.firka.timetable.refresh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>hu</string>
<string>de</string>
</array>
<key>CFBundleDisplayName</key>
<string>Firka Testing</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>firka</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.firka.firkaa</string>
<key>CFBundleURLSchemes</key>
<array>
<string>firka</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.firka.timetable.refresh</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>hu</string>
<string>de</string>
</array>
<key>CFBundleDisplayName</key>
<string>Firka Testing</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>firka</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.firka.firkaa</string>
<key>CFBundleURLSchemes</key>
<array>
<string>firka</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1062</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -10,5 +10,7 @@
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@@ -1,27 +1,6 @@
import Foundation
import Security
// MARK: - Token Structure
struct WatchToken: Codable {
let accessToken: String
let refreshToken: String
let idToken: String
let iss: String
let studentId: String
let studentIdNorm: Int64
let expiryDate: Date
enum CodingKeys: String, CodingKey {
case accessToken
case refreshToken
case idToken
case iss
case studentId
case studentIdNorm
case expiryDate
}
}
// MARK: - Token Response Structure
private struct TokenRefreshResponse: Decodable {
let accessToken: String
@@ -59,7 +38,40 @@ class TokenManager {
private let clientID = "kreta-ellenorzo-student-mobile-ios"
private let userAgent = "eKretaStudent/264745 CFNetwork/1494.0.7 Darwin/23.4.0"
private init() {}
#if os(iOS)
private let deviceName = "iPhone"
#elseif os(watchOS)
private let deviceName = "Watch"
#endif
private init() {
iCloudTokenManager.shared.observeChanges { [weak self] iCloudToken in
guard let self = self else { return }
if let localToken = self.loadTokenFromKeychain() {
if iCloudToken.expiryDate > localToken.expiryDate {
print("[TokenManager] iCloud token is fresher (\(iCloudToken.expiryDate) > \(localToken.expiryDate)), updating local cache")
try? self.saveTokenToKeychain(iCloudToken)
try? self.saveTokenToFile(iCloudToken)
#if os(watchOS)
DataStore.shared.checkTokenState()
#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)
}
} else {
print("[TokenManager] No local token, using iCloud token")
try? self.saveTokenToKeychain(iCloudToken)
try? self.saveTokenToFile(iCloudToken)
#if os(watchOS)
DataStore.shared.checkTokenState()
#endif
}
}
}
// MARK: - File Management
private func getTokenFilePath() -> URL? {
@@ -69,12 +81,48 @@ class TokenManager {
return containerURL.appendingPathComponent(tokenFileName)
}
// MARK: - Load Token
// MARK: - Load Token (fresher-wins strategy)
func loadToken() -> WatchToken? {
if let token = loadTokenFromKeychain() {
return token
let iCloudToken = iCloudTokenManager.shared.loadToken()
let keychainToken = loadTokenFromKeychain()
let fileToken = loadTokenFromFile()
var candidates: [(token: WatchToken, source: String)] = []
if let t = iCloudToken { candidates.append((t, "iCloud")) }
if let t = keychainToken { candidates.append((t, "keychain")) }
if let t = fileToken { candidates.append((t, "file")) }
guard !candidates.isEmpty else {
print("[TokenManager] No token found anywhere")
return nil
}
let freshest = candidates.max(by: { $0.token.expiryDate < $1.token.expiryDate })!
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
formatter.timeZone = TimeZone.current
print("[TokenManager] Token sources found: \(candidates.map { "\($0.source): \(formatter.string(from: $0.token.expiryDate))" }.joined(separator: ", "))")
print("[TokenManager] Using freshest token from \(freshest.source) (expiry: \(formatter.string(from: freshest.token.expiryDate)))")
if iCloudToken == nil || iCloudToken!.expiryDate < freshest.token.expiryDate {
print("[TokenManager] Syncing fresher token to iCloud")
iCloudTokenManager.shared.saveToken(freshest.token, deviceName: deviceName)
}
if keychainToken == nil || keychainToken!.expiryDate < freshest.token.expiryDate {
print("[TokenManager] Syncing fresher token to keychain")
try? saveTokenToKeychain(freshest.token)
}
if fileToken == nil || fileToken!.expiryDate < freshest.token.expiryDate {
print("[TokenManager] Syncing fresher token to file")
try? saveTokenToFile(freshest.token)
}
return freshest.token
}
private func loadTokenFromFile() -> WatchToken? {
guard let filePath = getTokenFilePath() else {
return nil
}
@@ -83,11 +131,7 @@ class TokenManager {
let data = try Data(contentsOf: filePath)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let token = try decoder.decode(WatchToken.self, from: data)
try? saveTokenToKeychain(token)
return token
return try decoder.decode(WatchToken.self, from: data)
} catch {
return nil
}
@@ -95,16 +139,33 @@ class TokenManager {
// MARK: - Delete Token
func deleteToken() {
print("[TokenManager] Deleting token from all storage locations")
deleteTokenFromKeychain()
iCloudTokenManager.shared.deleteToken()
guard let filePath = getTokenFilePath() else { return }
try? FileManager.default.removeItem(at: filePath)
}
// MARK: - Save Token
// MARK: - Save Token (to all storage locations)
func saveToken(_ token: WatchToken) throws {
print("[TokenManager] Saving token to all storage locations")
try saveTokenToKeychain(token)
iCloudTokenManager.shared.saveToken(token, deviceName: deviceName)
guard let filePath = getTokenFilePath() else {
throw TokenError.networkError
}
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(token)
try data.write(to: filePath)
}
private func saveTokenToFile(_ token: WatchToken) throws {
guard let filePath = getTokenFilePath() else {
throw TokenError.networkError
}

View File

@@ -0,0 +1,170 @@
import Foundation
class iCloudTokenManager {
static let shared = iCloudTokenManager()
private let iCloudStore = NSUbiquitousKeyValueStore.default
private let kAccessToken = "firka_access_token"
private let kRefreshToken = "firka_refresh_token"
private let kIdToken = "firka_id_token"
private let kIss = "firka_iss"
private let kStudentId = "firka_student_id"
private let kStudentIdNorm = "firka_student_id_norm"
private let kExpiryDate = "firka_expiry_date"
private let kLastUpdatedDevice = "firka_last_updated_device"
private let kLastUpdateTimestamp = "firka_last_update_timestamp"
private var changeObserver: ((WatchToken) -> Void)?
private var isAvailable = false
private init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(iCloudStoreDidChange(_:)),
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: iCloudStore
)
isAvailable = iCloudStore.synchronize()
if isAvailable {
print("[iCloud] iCloud KeyValue Store available and synced")
} else {
print("[iCloud] iCloud not available (not signed in or disabled) - using local storage only")
}
}
func saveToken(_ token: WatchToken, deviceName: String) {
guard isAvailable else {
return
}
print("[iCloud] Saving token to iCloud from \(deviceName)")
iCloudStore.set(token.accessToken, forKey: kAccessToken)
iCloudStore.set(token.refreshToken, forKey: kRefreshToken)
iCloudStore.set(token.idToken, forKey: kIdToken)
iCloudStore.set(token.iss, forKey: kIss)
iCloudStore.set(token.studentId, forKey: kStudentId)
iCloudStore.set(token.studentIdNorm, forKey: kStudentIdNorm)
iCloudStore.set(token.expiryDate.timeIntervalSince1970, forKey: kExpiryDate)
iCloudStore.set(deviceName, forKey: kLastUpdatedDevice)
iCloudStore.set(Date().timeIntervalSince1970, forKey: kLastUpdateTimestamp)
let success = iCloudStore.synchronize()
if success {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
formatter.timeZone = TimeZone.current
print("[iCloud] Token saved successfully, expiry: \(formatter.string(from: token.expiryDate))")
} else {
print("[iCloud] Failed to synchronize token to iCloud")
}
}
func loadToken() -> WatchToken? {
guard isAvailable else {
return nil
}
iCloudStore.synchronize()
guard let accessToken = iCloudStore.string(forKey: kAccessToken),
let refreshToken = iCloudStore.string(forKey: kRefreshToken),
let idToken = iCloudStore.string(forKey: kIdToken),
let iss = iCloudStore.string(forKey: kIss),
let studentId = iCloudStore.string(forKey: kStudentId) else {
print("[iCloud] No token found in iCloud")
return nil
}
let studentIdNorm = iCloudStore.longLong(forKey: kStudentIdNorm)
let expiryTimestamp = iCloudStore.double(forKey: kExpiryDate)
let lastDevice = iCloudStore.string(forKey: kLastUpdatedDevice) ?? "unknown"
guard expiryTimestamp > 0 else {
print("[iCloud] Invalid expiry date in iCloud")
return nil
}
let expiryDate = Date(timeIntervalSince1970: expiryTimestamp)
let token = WatchToken(
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
iss: iss,
studentId: studentId,
studentIdNorm: studentIdNorm,
expiryDate: expiryDate
)
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
formatter.timeZone = TimeZone.current
print("[iCloud] Token loaded from iCloud (last updated by: \(lastDevice)), expiry: \(formatter.string(from: expiryDate))")
return token
}
func deleteToken() {
guard isAvailable else {
return
}
print("[iCloud] Deleting token from iCloud")
iCloudStore.removeObject(forKey: kAccessToken)
iCloudStore.removeObject(forKey: kRefreshToken)
iCloudStore.removeObject(forKey: kIdToken)
iCloudStore.removeObject(forKey: kIss)
iCloudStore.removeObject(forKey: kStudentId)
iCloudStore.removeObject(forKey: kStudentIdNorm)
iCloudStore.removeObject(forKey: kExpiryDate)
iCloudStore.removeObject(forKey: kLastUpdatedDevice)
iCloudStore.removeObject(forKey: kLastUpdateTimestamp)
iCloudStore.synchronize()
}
func observeChanges(_ observer: @escaping (WatchToken) -> Void) {
self.changeObserver = observer
}
@objc private func iCloudStoreDidChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let changeReason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else {
return
}
if changeReason == NSUbiquitousKeyValueStoreServerChange ||
changeReason == NSUbiquitousKeyValueStoreInitialSyncChange {
print("[iCloud] Token changed externally in iCloud")
if let token = loadToken() {
let lastDevice = iCloudStore.string(forKey: kLastUpdatedDevice) ?? "unknown"
print("[iCloud] Received updated token from: \(lastDevice)")
changeObserver?(token)
}
}
}
func getLastUpdatedDevice() -> String? {
guard isAvailable else {
return nil
}
iCloudStore.synchronize()
return iCloudStore.string(forKey: kLastUpdatedDevice)
}
func getLastUpdateTimestamp() -> Date? {
guard isAvailable else {
return nil
}
iCloudStore.synchronize()
let timestamp = iCloudStore.double(forKey: kLastUpdateTimestamp)
guard timestamp > 0 else { return nil }
return Date(timeIntervalSince1970: timestamp)
}
}

View File

@@ -0,0 +1,22 @@
import Foundation
// MARK: - Token Structure
struct WatchToken: Codable {
let accessToken: String
let refreshToken: String
let idToken: String
let iss: String
let studentId: String
let studentIdNorm: Int64
let expiryDate: Date
enum CodingKeys: String, CodingKey {
case accessToken
case refreshToken
case idToken
case iss
case studentId
case studentIdNorm
case expiryDate
}
}

View File

@@ -963,7 +963,9 @@ class LiveActivityService {
await _markAsRegistered();
await _saveLastUpdate();
await _startPlaceholderActivity(allLessons, studentName);
if (liveActivityEnabled) {
await _startPlaceholderActivity(allLessons, studentName);
}
await _startTimetableMonitoring(
client: client,

View File

@@ -24,6 +24,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:intl/intl.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
@@ -328,7 +329,8 @@ void main() async {
runZonedGuarded(() async {
logger.finest("Initializing app");
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await dotenv.load(fileName: ".env");
logger.info("Environment variables loaded");
@@ -486,6 +488,8 @@ class InitializationScreen extends StatelessWidget {
initData = snapshot.data!;
initDone = true;
FlutterNativeSplash.remove();
WatchSyncHelper.initialize();
var watch = WatchConnectivity();
@@ -597,16 +601,8 @@ class InitializationScreen extends StatelessWidget {
home: DefaultAssetBundle(
bundle: FirkaBundle(),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: const Color(0xFF7CA021),
)
],
),
),
backgroundColor: const Color(0xFF7CA120),
body: Container(), // Covered by native splash
),
),
);

View File

@@ -85,6 +85,7 @@ dev_dependencies:
android_notification_icons: ^0.0.1
integration_test:
sdk: flutter
flutter_native_splash: ^2.4.7
android_notification_icons:
image_path: 'assets/images/logos/dave_monochrome.png'