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.
BIN
firka/android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
firka/android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
firka/android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
firka/android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
firka/android/app/src/main/res/drawable-night-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
BIN
firka/android/app/src/main/res/drawable-night-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
firka/android/app/src/main/res/drawable-night-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -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>
|
||||
|
After Width: | Height: | Size: 16 KiB |
BIN
firka/android/app/src/main/res/drawable-night-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
firka/android/app/src/main/res/drawable-night-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
firka/android/app/src/main/res/drawable-night-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
firka/android/app/src/main/res/drawable-night/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -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>
|
||||
BIN
firka/android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
BIN
firka/android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
firka/android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
firka/android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
firka/android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -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>
|
||||
|
||||
21
firka/android/app/src/main/res/values-night-v31/styles.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
21
firka/android/app/src/main/res/values-v31/styles.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
16
firka/flutter_native_splash.yaml
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"filename" : "Icon-1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
|
||||
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,6 +8,7 @@ struct FirkaWatchApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "${SRCROOT}" /usr/bin/xcrun agvtool next-version -all echo "Build number incremented" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
||||
22
firka/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
firka/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
BIN
firka/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 24 KiB |
BIN
firka/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png
vendored
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
firka/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
firka/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
170
firka/ios/Shared/API/iCloudTokenManager.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
22
firka/ios/Shared/Models/WatchToken.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -963,7 +963,9 @@ class LiveActivityService {
|
||||
await _markAsRegistered();
|
||||
await _saveLastUpdate();
|
||||
|
||||
await _startPlaceholderActivity(allLessons, studentName);
|
||||
if (liveActivityEnabled) {
|
||||
await _startPlaceholderActivity(allLessons, studentName);
|
||||
}
|
||||
|
||||
await _startTimetableMonitoring(
|
||||
client: client,
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
|
||||