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.
This commit is contained in:
FuelBoard Contributor
2026-09-16 16:15:07 +01:00
parent 465e6cd4d4
commit 20fe251b8e
6 changed files with 80 additions and 15 deletions
+15 -3
View File
@@ -243,9 +243,21 @@ struct ContentView: View {
) )
} }
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in .onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in
guard let id = note.userInfo?["stationID"] as? String, let info = note.userInfo ?? [:]
let station = stations.first(where: { $0.id == id }) else { return } if let id = info["stationID"] as? String,
monitor.pendingDetailStation = station 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
)
}
} }
) )
} }
+21 -6
View File
@@ -69,20 +69,35 @@ struct FuelBoardApp: App {
/// `http://maps.apple.com` form is also handled, from very old timelines. /// `http://maps.apple.com` form is also handled, from very old timelines.
/// In CarPlay the widget uses the same `maps://` URL, which routes to /// In CarPlay the widget uses the same `maps://` URL, which routes to
/// Apple Maps without ever launching this app. /// Apple Maps without ever launching this app.
/// A `fuelboard://station?stationID=` link (from a Live Activity tap when /// A `fuelboard://station?stationID=&lat=&lng=&name=` link (from a
/// Settings Tap station = More info) posts a notification that /// widget or Live Activity tap when Settings Tap station = More info)
/// ContentView observes to present the detail sheet. /// 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) { private func handleOpenURL(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
else { return } else { return }
if components.scheme == "fuelboard", if components.scheme == "fuelboard",
components.host == "station", components.host == "station" {
let id = components.queryItems?.first(where: { $0.name == "stationID" })?.value { 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( NotificationCenter.default.post(
name: .fuelBoardShowStationDetail, name: .fuelBoardShowStationDetail,
object: nil, object: nil,
userInfo: ["stationID": id] userInfo: userInfo
) )
return return
} }
@@ -2,7 +2,8 @@
// //
// Lives in the widget extension (the standard host for ActivityConfiguration). // Lives in the widget extension (the standard host for ActivityConfiguration).
// Shows the cheapest station for the pinned fuel within the app's chosen // 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 // ADAPTIVE LAYOUT: the Lock Screen body provides two layouts and lets
// ViewThatFits pick by available width, but the narrow `.small` family is NOT // 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 /// 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 { 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!
} }
} }
+3 -1
View File
@@ -9,7 +9,9 @@
// in Settings Live Activity (INDEPENDENT of the Stations-tab selection). // in Settings Live Activity (INDEPENDENT of the Stations-tab selection).
// Everything dynamic lives in ContentState so updates including a fuel // Everything dynamic lives in ContentState so updates including a fuel
// change apply to the running activity without needing to restart it. // 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 ActivityKit
import Foundation import Foundation
+2 -2
View File
@@ -99,7 +99,7 @@ struct FuelPriceWidgetContent: View {
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .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 /// 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 ForEach(entry.stations.prefix(maxRows)) { station in
if isWidget { if isWidget {
Link(destination: station.widgetDirectionsURL ?? URL(string: "maps://")!) { Link(destination: station.tapDestinationURL ?? URL(string: "maps://")!) {
row(station, cheapest: cheapest) row(station, cheapest: cheapest)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
+24
View File
@@ -271,6 +271,30 @@ struct FuelStation: Identifiable, Codable, Equatable {
URL(string: "maps://?daddr=\(lat),\(lng)&t=d") 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. /// Name of the bundled brand logo asset, or nil if unknown.
/// Normalizes messy raw brand strings ("SHELL LEEDS ROAD" "shell"). /// Normalizes messy raw brand strings ("SHELL LEEDS ROAD" "shell").
var brandImageName: String? { var brandImageName: String? {