From 20fe251b8ed32d403b814e8624864cd531a1b55a Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Wed, 16 Sep 2026 16:15:07 +0100 Subject: [PATCH] feat: honour Tap station action in widgets Small widgetURL and medium list Links route through StationTapAction via FuelStation.tapDestinationURL: Open map keeps the Maps URL, More info uses a fuelboard://station deep link (stationID plus coordinate/name fallback). Deep link carries fallback coords; ContentView presents the detail sheet when the station is cached, else the map sheet. Live Activity detailURL upgraded to the same rich link. --- FuelBoard/ContentView.swift | 18 ++++++++++--- FuelBoard/FuelBoardApp.swift | 27 ++++++++++++++----- .../FuelBoardLiveActivityView.swift | 18 ++++++++++--- Shared/FuelBoardLiveActivity.swift | 4 ++- Shared/FuelPriceWidgetViews.swift | 4 +-- Shared/FuelStore.swift | 24 +++++++++++++++++ 6 files changed, 80 insertions(+), 15 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 26c12a3..b3c0547 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -243,9 +243,21 @@ struct ContentView: View { ) } .onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in - guard let id = note.userInfo?["stationID"] as? String, - let station = stations.first(where: { $0.id == id }) else { return } - monitor.pendingDetailStation = station + let info = note.userInfo ?? [:] + if let id = info["stationID"] as? String, + let station = stations.first(where: { $0.id == id }) { + monitor.pendingDetailStation = station + return + } + // Station left the cache — fall back to the map sheet so + // the tap still shows something. + if let lat = info["stationLat"] as? Double, + let lng = info["stationLng"] as? Double { + let name = info["stationName"] as? String ?? "Station" + monitor.pendingStationMap = StationMapRequest( + name: name, latitude: lat, longitude: lng + ) + } } ) } diff --git a/FuelBoard/FuelBoardApp.swift b/FuelBoard/FuelBoardApp.swift index 67a4e25..edb1d02 100644 --- a/FuelBoard/FuelBoardApp.swift +++ b/FuelBoard/FuelBoardApp.swift @@ -69,20 +69,35 @@ struct FuelBoardApp: App { /// `http://maps.apple.com` form is also handled, from very old timelines. /// In CarPlay the widget uses the same `maps://` URL, which routes to /// Apple Maps without ever launching this app. - /// A `fuelboard://station?stationID=…` link (from a Live Activity tap when - /// Settings → Tap station = More info) posts a notification that - /// ContentView observes to present the detail sheet. + /// A `fuelboard://station?stationID=…&lat=…&lng=…&name=…` link (from a + /// widget or Live Activity tap when Settings → Tap station = More info) + /// posts a notification that ContentView observes to present the detail + /// sheet (falling back to the map sheet when the station has left + /// the cache). private func handleOpenURL(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } if components.scheme == "fuelboard", - components.host == "station", - let id = components.queryItems?.first(where: { $0.name == "stationID" })?.value { + components.host == "station" { + let items = components.queryItems ?? [] + var userInfo: [String: Any] = [:] + if let id = items.first(where: { $0.name == "stationID" })?.value { + userInfo["stationID"] = id + } + if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init) { + userInfo["stationLat"] = lat + } + if let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) { + userInfo["stationLng"] = lng + } + if let name = items.first(where: { $0.name == "name" })?.value { + userInfo["stationName"] = name + } NotificationCenter.default.post( name: .fuelBoardShowStationDetail, object: nil, - userInfo: ["stationID": id] + userInfo: userInfo ) return } diff --git a/FuelBoardWidgets/FuelBoardLiveActivityView.swift b/FuelBoardWidgets/FuelBoardLiveActivityView.swift index b13ca78..feb6da0 100644 --- a/FuelBoardWidgets/FuelBoardLiveActivityView.swift +++ b/FuelBoardWidgets/FuelBoardLiveActivityView.swift @@ -2,7 +2,8 @@ // // Lives in the widget extension (the standard host for ActivityConfiguration). // Shows the cheapest station for the pinned fuel within the app's chosen -// radius. Tapping anywhere opens Apple Maps directions to that station. +// radius. Tapping honours Settings → Tap station: Open map opens Apple Maps +// directions to that station, More info opens the detail sheet in the app. // // ADAPTIVE LAYOUT: the Lock Screen body provides two layouts and lets // ViewThatFits pick by available width, but the narrow `.small` family is NOT @@ -254,8 +255,19 @@ extension FuelBoardLiveActivityAttributes.ContentState { } /// In-app deep link to the station's detail sheet, used when - /// Settings → Tap station = More info. + /// Settings → Tap station = More info. Carries coordinate/name fallback + /// so the app can still show the map sheet when the station has left + /// the cache. var detailURL: URL { - URL(string: "fuelboard://station?stationID=\(stationID)")! + var components = URLComponents() + components.scheme = "fuelboard" + components.host = "station" + components.queryItems = [ + URLQueryItem(name: "stationID", value: stationID), + URLQueryItem(name: "lat", value: String(lat)), + URLQueryItem(name: "lng", value: String(lng)), + URLQueryItem(name: "name", value: stationName), + ] + return components.url! } } \ No newline at end of file diff --git a/Shared/FuelBoardLiveActivity.swift b/Shared/FuelBoardLiveActivity.swift index 5b78eb1..25babe0 100644 --- a/Shared/FuelBoardLiveActivity.swift +++ b/Shared/FuelBoardLiveActivity.swift @@ -9,7 +9,9 @@ // in Settings → Live Activity (INDEPENDENT of the Stations-tab selection). // Everything dynamic lives in ContentState so updates — including a fuel // change — apply to the running activity without needing to restart it. -// Tapping the activity opens Apple Maps driving directions (maps://). +// Tapping the activity honours Settings → Tap station: Open map opens Apple +// Maps driving directions (maps://), More info opens the detail sheet in +// the app (fuelboard://station). import ActivityKit import Foundation diff --git a/Shared/FuelPriceWidgetViews.swift b/Shared/FuelPriceWidgetViews.swift index cb898b9..876b44e 100644 --- a/Shared/FuelPriceWidgetViews.swift +++ b/Shared/FuelPriceWidgetViews.swift @@ -99,7 +99,7 @@ struct FuelPriceWidgetContent: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .modifier(WidgetURLModifier(url: isWidget ? station.widgetDirectionsURL : nil)) + .modifier(WidgetURLModifier(url: isWidget ? station.tapDestinationURL : nil)) } /// How many station rows each family can fit: medium widgets are short @@ -129,7 +129,7 @@ struct FuelPriceWidgetContent: View { } ForEach(entry.stations.prefix(maxRows)) { station in if isWidget { - Link(destination: station.widgetDirectionsURL ?? URL(string: "maps://")!) { + Link(destination: station.tapDestinationURL ?? URL(string: "maps://")!) { row(station, cheapest: cheapest) } .buttonStyle(.plain) diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index c81ed27..76196d8 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -271,6 +271,30 @@ struct FuelStation: Identifiable, Codable, Equatable { URL(string: "maps://?daddr=\(lat),\(lng)&t=d") } + /// In-app deep link to this station's More info sheet. Carries the + /// stationID plus coordinate/name fallback so the app can still show the + /// map sheet when the station has left the cache. + var detailDeepLinkURL: URL? { + var components = URLComponents() + components.scheme = "fuelboard" + components.host = "station" + components.queryItems = [ + URLQueryItem(name: "stationID", value: id), + URLQueryItem(name: "lat", value: String(lat)), + URLQueryItem(name: "lng", value: String(lng)), + URLQueryItem(name: "name", value: name), + ] + return components.url + } + + /// Widget/Live Activity tap destination honouring Settings → Tap station: + /// Open map → Apple Maps directions; More info → in-app detail deep link. + var tapDestinationURL: URL? { + FuelStore.loadStationTapAction() == .showDetails + ? detailDeepLinkURL + : widgetDirectionsURL + } + /// Name of the bundled brand logo asset, or nil if unknown. /// Normalizes messy raw brand strings ("SHELL LEEDS ROAD" → "shell"). var brandImageName: String? {