diff --git a/ios/Sources/RootView.swift b/ios/Sources/RootView.swift index 3b673ae..ec42317 100644 --- a/ios/Sources/RootView.swift +++ b/ios/Sources/RootView.swift @@ -6,6 +6,7 @@ struct RootView: View { @EnvironmentObject private var display: DisplaySettings /// Über einen gescannten Universal Link geöffnetes Einzelstück. @State private var linkedItem: Item? + @State private var linkError: String? var body: some View { Group { @@ -45,12 +46,25 @@ struct RootView: View { } .task(id: router.openItemUid) { guard let uid = router.openItemUid else { return } - linkedItem = try? await APIClient.shared.itemByUid(uid: uid) router.openItemUid = nil + guard session.isLoggedIn else { + linkError = "Bitte zuerst am passenden Server anmelden, dann erneut scannen." + return + } + do { + linkedItem = try await APIClient.shared.itemByUid(uid: uid) + } catch { + linkError = "Einzelstück \(uid) nicht gefunden: \(error.localizedDescription)" + } } .sheet(item: $linkedItem) { it in NavigationStack { ItemEditView(item: it) } } + .alert("Einzelstück öffnen", isPresented: Binding( + get: { linkError != nil }, set: { if !$0 { linkError = nil } } + )) { + Button("OK", role: .cancel) { linkError = nil } + } message: { Text(linkError ?? "") } } } diff --git a/ios/Sources/Session.swift b/ios/Sources/Session.swift index b5a6e0b..7a087de 100644 --- a/ios/Sources/Session.swift +++ b/ios/Sources/Session.swift @@ -140,6 +140,17 @@ final class Session: ObservableObject { loadTokenForActiveProfile() } + /// Wechselt (falls vorhanden und noetig) auf das Profil, dessen Adresse zum + /// Host eines Links passt – damit ein gescannter QR den richtigen Server + /// abfragt, auch wenn gerade ein anderer aktiv ist. + func switchToProfile(matching url: URL) { + guard let host = url.host?.lowercased() else { return } + if activeProfile?.url?.host?.lowercased() == host { return } + if let match = profiles.first(where: { $0.url?.host?.lowercased() == host }) { + switchTo(match.id) + } + } + // MARK: - Anmeldung /// Legt die Adresse fuer die Anmeldung fest: aktualisiert das aktive Profil diff --git a/ios/Sources/VorraniaApp.swift b/ios/Sources/VorraniaApp.swift index 5b9648d..7830317 100644 --- a/ios/Sources/VorraniaApp.swift +++ b/ios/Sources/VorraniaApp.swift @@ -40,6 +40,8 @@ final class Router: ObservableObject { } let parts = url.pathComponents // z.B. ["/", "i", "ABC123"] if let idx = parts.firstIndex(of: "i"), idx + 1 < parts.count { + // Auf den Server aus dem Link wechseln, damit die UID dort gesucht wird. + Session.shared.switchToProfile(matching: url) openItemUid = parts[idx + 1].uppercased() } } @@ -73,6 +75,11 @@ struct VorraniaApp: App { router.handle(expiryProfile: profile) AppDelegate.pendingExpiryProfile = nil } + // Kaltstart aus einem gescannten Universal Link. + if let url = AppDelegate.pendingURL { + router.handle(url: url) + AppDelegate.pendingURL = nil + } } .onReceive(NotificationCenter.default.publisher(for: AppDelegate.shortcutNotification)) { note in if let type = note.object as? String { router.handle(shortcut: type) } @@ -80,6 +87,9 @@ struct VorraniaApp: App { .onReceive(NotificationCenter.default.publisher(for: AppDelegate.expiryTapNotification)) { note in if let profile = note.object as? String { router.handle(expiryProfile: profile) } } + .onReceive(NotificationCenter.default.publisher(for: AppDelegate.urlNotification)) { note in + if let url = note.object as? URL { router.handle(url: url) } + } } // Im Vordergrund den Meldungsplan auffrischen und den naechsten // Hintergrundlauf einplanen. @@ -101,8 +111,10 @@ struct VorraniaApp: App { final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { static let shortcutNotification = Notification.Name("VorraniaShortcut") static let expiryTapNotification = Notification.Name("VorraniaExpiryTap") + static let urlNotification = Notification.Name("VorraniaOpenURL") static var pendingShortcut: String? static var pendingExpiryProfile: String? + static var pendingURL: URL? static let refreshTaskID = "com.scarriffle.vorrania.refresh" @@ -132,6 +144,17 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent completionHandler(true) } + // Universal Link (…/i/) – auch beim Kaltstart, falls die SwiftUI-Hooks + // ihn nicht mitbekommen. + func application(_ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL else { return false } + AppDelegate.pendingURL = url + NotificationCenter.default.post(name: AppDelegate.urlNotification, object: url) + return true + } + // MARK: - Benachrichtigungen /// Auch im Vordergrund als Banner zeigen - sonst bliebe eine faellige