Universal Links: gescannter Einzelstück-Link öffnet die App
Server liefert /.well-known/apple-app-site-association (nur /i/* beansprucht; /l/* bleibt im Browser). iOS: Associated-Domains-Entitlement, Universal Link (https://…/i/<UID>) wird abgefangen (onContinueUserActivity) und öffnet das Einzelstück direkt. Hinweis: Damit das Signieren durchläuft, muss die Capability "Associated Domains" einmalig im Apple-Developer-Portal für die App-ID aktiv sein. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ struct RootView: View {
|
|||||||
@EnvironmentObject private var session: Session
|
@EnvironmentObject private var session: Session
|
||||||
@EnvironmentObject private var router: Router
|
@EnvironmentObject private var router: Router
|
||||||
@EnvironmentObject private var display: DisplaySettings
|
@EnvironmentObject private var display: DisplaySettings
|
||||||
|
/// Über einen gescannten Universal Link geöffnetes Einzelstück.
|
||||||
|
@State private var linkedItem: Item?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
ios/Sources/Vorrania.entitlements
Normal file
10
ios/Sources/Vorrania.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?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>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:vorrania-ch.scarriffle.com</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -15,6 +15,8 @@ enum Route: String {
|
|||||||
|
|
||||||
final class Router: ObservableObject {
|
final class Router: ObservableObject {
|
||||||
@Published var route: Route?
|
@Published var route: Route?
|
||||||
|
/// Über einen Universal Link (…/i/<UID>) zu öffnendes Einzelstück.
|
||||||
|
@Published var openItemUid: String?
|
||||||
|
|
||||||
func handle(shortcut type: String) {
|
func handle(shortcut type: String) {
|
||||||
if type.hasSuffix("checkin") { route = .checkin }
|
if type.hasSuffix("checkin") { route = .checkin }
|
||||||
@@ -28,11 +30,18 @@ final class Router: ObservableObject {
|
|||||||
route = .expiring
|
route = .expiring
|
||||||
}
|
}
|
||||||
|
|
||||||
/// vorrania://checkin bzw. vorrania://checkout
|
/// vorrania://checkin bzw. vorrania://checkout – oder ein Universal Link
|
||||||
|
/// (https://…/i/<UID>), der das Einzelstück direkt öffnet.
|
||||||
func handle(url: URL) {
|
func handle(url: URL) {
|
||||||
guard url.scheme == "vorrania" else { return }
|
if url.scheme == "vorrania" {
|
||||||
let target = (url.host ?? url.path.replacingOccurrences(of: "/", with: "")).lowercased()
|
let target = (url.host ?? url.path.replacingOccurrences(of: "/", with: "")).lowercased()
|
||||||
if let route = Route(rawValue: target) { self.route = route }
|
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(router)
|
||||||
.environmentObject(display)
|
.environmentObject(display)
|
||||||
.onOpenURL { router.handle(url: $0) }
|
.onOpenURL { router.handle(url: $0) }
|
||||||
|
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||||
|
if let url = activity.webpageURL { router.handle(url: url) }
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if let type = AppDelegate.pendingShortcut {
|
if let type = AppDelegate.pendingShortcut {
|
||||||
router.handle(shortcut: type)
|
router.handle(shortcut: type)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
C35D34107C8CBB8B7C6C6650 /* LocalOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOverview.swift; sourceTree = "<group>"; };
|
C35D34107C8CBB8B7C6C6650 /* LocalOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOverview.swift; sourceTree = "<group>"; };
|
||||||
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPicker.swift; sourceTree = "<group>"; };
|
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPicker.swift; sourceTree = "<group>"; };
|
||||||
DC0DDD06332E567E70CA842F /* CheckOutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckOutView.swift; sourceTree = "<group>"; };
|
DC0DDD06332E567E70CA842F /* CheckOutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckOutView.swift; sourceTree = "<group>"; };
|
||||||
|
DE4C9D922F5AC05270B1CF9B /* Vorrania.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Vorrania.entitlements; sourceTree = "<group>"; };
|
||||||
E580BA2815BB8F6341D64B47 /* ChartCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartCards.swift; sourceTree = "<group>"; };
|
E580BA2815BB8F6341D64B47 /* ChartCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartCards.swift; sourceTree = "<group>"; };
|
||||||
E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
|
E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
|
||||||
EA706060734632DE78FA1073 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = "<group>"; };
|
EA706060734632DE78FA1073 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = "<group>"; };
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
EA706060734632DE78FA1073 /* ServerListView.swift */,
|
EA706060734632DE78FA1073 /* ServerListView.swift */,
|
||||||
121D44B9990DA931A9CD5847 /* ServerProfiles.swift */,
|
121D44B9990DA931A9CD5847 /* ServerProfiles.swift */,
|
||||||
AD2F9406BD0D4F0D30FED345 /* Session.swift */,
|
AD2F9406BD0D4F0D30FED345 /* Session.swift */,
|
||||||
|
DE4C9D922F5AC05270B1CF9B /* Vorrania.entitlements */,
|
||||||
9948219CDDC4188EA4298F22 /* VorraniaApp.swift */,
|
9948219CDDC4188EA4298F22 /* VorraniaApp.swift */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
@@ -317,6 +319,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Sources/Vorrania.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
@@ -338,6 +341,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Sources/Vorrania.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
|||||||
@@ -39,3 +39,11 @@ targets:
|
|||||||
TARGETED_DEVICE_FAMILY: "1,2"
|
TARGETED_DEVICE_FAMILY: "1,2"
|
||||||
SWIFT_VERSION: "5.9"
|
SWIFT_VERSION: "5.9"
|
||||||
GENERATE_INFOPLIST_FILE: NO
|
GENERATE_INFOPLIST_FILE: NO
|
||||||
|
# Universal Links: …/i/<UID> ö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
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ server {
|
|||||||
# den Upload ueberhaupt sieht.
|
# den Upload ueberhaupt sieht.
|
||||||
client_max_body_size 12m;
|
client_max_body_size 12m;
|
||||||
|
|
||||||
|
# Universal Links: iOS holt diese Datei, um /i/<UID>-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
|
# SPA-Routing: unbekannte Pfade auf index.html mappen
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
Reference in New Issue
Block a user