diff --git a/ios/Sources/RootView.swift b/ios/Sources/RootView.swift index 39419a9..3b673ae 100644 --- a/ios/Sources/RootView.swift +++ b/ios/Sources/RootView.swift @@ -4,6 +4,8 @@ struct RootView: View { @EnvironmentObject private var session: Session @EnvironmentObject private var router: Router @EnvironmentObject private var display: DisplaySettings + /// Über einen gescannten Universal Link geöffnetes Einzelstück. + @State private var linkedItem: Item? var body: some View { Group { @@ -41,6 +43,14 @@ 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 + } + .sheet(item: $linkedItem) { it in + NavigationStack { ItemEditView(item: it) } + } } } diff --git a/ios/Sources/Vorrania.entitlements b/ios/Sources/Vorrania.entitlements new file mode 100644 index 0000000..4e3230e --- /dev/null +++ b/ios/Sources/Vorrania.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:vorrania-ch.scarriffle.com + + + diff --git a/ios/Sources/VorraniaApp.swift b/ios/Sources/VorraniaApp.swift index 08e4370..5b9648d 100644 --- a/ios/Sources/VorraniaApp.swift +++ b/ios/Sources/VorraniaApp.swift @@ -15,6 +15,8 @@ enum Route: String { final class Router: ObservableObject { @Published var route: Route? + /// Über einen Universal Link (…/i/) zu öffnendes Einzelstück. + @Published var openItemUid: String? func handle(shortcut type: String) { if type.hasSuffix("checkin") { route = .checkin } @@ -28,11 +30,18 @@ final class Router: ObservableObject { route = .expiring } - /// vorrania://checkin bzw. vorrania://checkout + /// vorrania://checkin bzw. vorrania://checkout – oder ein Universal Link + /// (https://…/i/), der das Einzelstück direkt öffnet. func handle(url: URL) { - guard url.scheme == "vorrania" else { return } - let target = (url.host ?? url.path.replacingOccurrences(of: "/", with: "")).lowercased() - if let route = Route(rawValue: target) { self.route = route } + if url.scheme == "vorrania" { + let target = (url.host ?? url.path.replacingOccurrences(of: "/", with: "")).lowercased() + if let route = Route(rawValue: target) { self.route = route } + return + } + let parts = url.pathComponents // z.B. ["/", "i", "ABC123"] + if let idx = parts.firstIndex(of: "i"), idx + 1 < parts.count { + openItemUid = parts[idx + 1].uppercased() + } } } @@ -51,6 +60,9 @@ struct VorraniaApp: App { .environmentObject(router) .environmentObject(display) .onOpenURL { router.handle(url: $0) } + .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in + if let url = activity.webpageURL { router.handle(url: url) } + } .onAppear { if let type = AppDelegate.pendingShortcut { router.handle(shortcut: type) diff --git a/ios/Vorrania.xcodeproj/project.pbxproj b/ios/Vorrania.xcodeproj/project.pbxproj index 6f982ef..4fa6564 100644 --- a/ios/Vorrania.xcodeproj/project.pbxproj +++ b/ios/Vorrania.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ C35D34107C8CBB8B7C6C6650 /* LocalOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOverview.swift; sourceTree = ""; }; CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPicker.swift; sourceTree = ""; }; DC0DDD06332E567E70CA842F /* CheckOutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckOutView.swift; sourceTree = ""; }; + DE4C9D922F5AC05270B1CF9B /* Vorrania.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vorrania.entitlements; sourceTree = ""; }; E580BA2815BB8F6341D64B47 /* ChartCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartCards.swift; sourceTree = ""; }; E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; EA706060734632DE78FA1073 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = ""; }; @@ -123,6 +124,7 @@ EA706060734632DE78FA1073 /* ServerListView.swift */, 121D44B9990DA931A9CD5847 /* ServerProfiles.swift */, AD2F9406BD0D4F0D30FED345 /* Session.swift */, + DE4C9D922F5AC05270B1CF9B /* Vorrania.entitlements */, 9948219CDDC4188EA4298F22 /* VorraniaApp.swift */, ); path = Sources; @@ -317,6 +319,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Sources/Vorrania.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PP34X97WS3; @@ -338,6 +341,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Sources/Vorrania.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PP34X97WS3; diff --git a/ios/project.yml b/ios/project.yml index fc7b445..9a3d45a 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -39,3 +39,11 @@ targets: TARGETED_DEVICE_FAMILY: "1,2" SWIFT_VERSION: "5.9" GENERATE_INFOPLIST_FILE: NO + # Universal Links: …/i/ öffnet direkt die App. Dafür muss im + # Apple-Developer-Portal die Capability "Associated Domains" für die App-ID + # aktiv sein (einmalig), sonst schlägt das Signieren fehl. + entitlements: + path: Sources/Vorrania.entitlements + properties: + com.apple.developer.associated-domains: + - applinks:vorrania-ch.scarriffle.com diff --git a/web/nginx.conf b/web/nginx.conf index 58719b3..7fff941 100644 --- a/web/nginx.conf +++ b/web/nginx.conf @@ -9,6 +9,14 @@ server { # den Upload ueberhaupt sieht. client_max_body_size 12m; + # Universal Links: iOS holt diese Datei, um /i/-Links direkt in der App + # zu öffnen. Bewusst nur die Einzelstück-Pfade (/i/*); Lagerort-Links (/l/*) + # bleiben im Browser. Team-/Bundle-ID aus ios/project.yml. + location = /.well-known/apple-app-site-association { + default_type application/json; + return 200 '{"applinks":{"details":[{"appIDs":["PP34X97WS3.com.scarriffle.vorrania"],"components":[{"/":"/i/*"}]}]}}'; + } + # SPA-Routing: unbekannte Pfade auf index.html mappen location / { try_files $uri $uri/ /index.html;