`-qaScreenMarker` paints the screen flat magenta, which lets a macOS capture locate the device-screen rectangle exactly and map simulator-screenshot pixels to click coordinates. `simctl` cannot drive taps and the project has no UI-test target, so this is the only way to test real touch behaviour on the sheet. Guarded by #if DEBUG, exactly like the existing -qaCopyPostcode hook.
148 lines
5.8 KiB
Swift
148 lines
5.8 KiB
Swift
import ActivityKit
|
|
import BackgroundTasks
|
|
import SwiftUI
|
|
|
|
@main
|
|
struct FuelBoardApp: App {
|
|
init() {
|
|
WatchSyncManager.shared.activate()
|
|
#if DEBUG
|
|
// QA hook (Debug builds only): `-qaLiveActivity e10|e5|diesel` starts a
|
|
// Live Activity with a long station name so the Lock Screen / island
|
|
// layout can be rendered in the Simulator for visual QA.
|
|
let args = ProcessInfo.processInfo.arguments
|
|
if let idx = args.firstIndex(of: "-qaLiveActivity"),
|
|
args.indices.contains(idx + 1),
|
|
let fuel = FuelType(rawValue: args[idx + 1]) {
|
|
startQALiveActivity(fuel: fuel)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
rootView
|
|
.onOpenURL(perform: handleOpenURL)
|
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardWatchRefreshRequested)) { _ in
|
|
WatchSyncManager.shared.pushSnapshot()
|
|
}
|
|
}
|
|
.backgroundTask(.appRefresh(SmartDataRefreshScheduler.taskIdentifier)) {
|
|
await SmartDataRefreshCoordinator.runBackgroundProbe()
|
|
}
|
|
}
|
|
|
|
/// QA hook (Debug builds only): `-qaScreenMarker` paints the whole screen a
|
|
/// flat magenta. A macOS capture of the Simulator window can then locate the
|
|
/// device-screen rectangle exactly, which yields a pixel-accurate mapping
|
|
/// from simulator screenshots to click coordinates. That is the only way to
|
|
/// drive REAL taps into the Simulator: `simctl` cannot, and this project has
|
|
/// no UI-test target. Everything else renders ContentView as normal.
|
|
@ViewBuilder private var rootView: some View {
|
|
#if DEBUG
|
|
if ProcessInfo.processInfo.arguments.contains("-qaScreenMarker") {
|
|
Color(red: 1, green: 0, blue: 1).ignoresSafeArea()
|
|
} else {
|
|
ContentView()
|
|
}
|
|
#else
|
|
ContentView()
|
|
#endif
|
|
}
|
|
|
|
#if DEBUG
|
|
private func startQALiveActivity(fuel: FuelType) {
|
|
let state = FuelBoardLiveActivityAttributes.ContentState(
|
|
fuel: fuel,
|
|
stationID: "qa-phoenix",
|
|
stationName: "Phoenix Filling Stations",
|
|
brand: "Phoenix",
|
|
pricePence: 1499,
|
|
priceDisplayStyle: FuelStore.loadPriceDisplayStyle(),
|
|
distanceKM: 8.0,
|
|
// QA mock: marked approximate so the hook renders the LONGEST
|
|
// distance form ("Approx. 8.0 mi") and the island / Lock Screen
|
|
// layout can be checked against it.
|
|
distanceIsApproximate: true,
|
|
lat: 51.5,
|
|
lng: -0.12,
|
|
updatedAt: Date()
|
|
)
|
|
let attrs = FuelBoardLiveActivityAttributes()
|
|
do {
|
|
let activity = try Activity.request(
|
|
attributes: attrs,
|
|
content: .init(state: state, staleDate: nil),
|
|
pushType: nil
|
|
)
|
|
print("QA-LIVE-ACTIVITY STARTED id=\(activity.id)")
|
|
} catch {
|
|
print("QA-LIVE-ACTIVITY FAILED: \(error)")
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// Handles deep links that end up in the app. Widget taps arrive here in
|
|
/// two cases:
|
|
/// - legacy/cached widget timelines using the `fuelboard://` relay, or
|
|
/// - the Home Screen delivering a `maps://` URL to the containing app
|
|
/// instead of opening Maps directly (the system's choice on iOS).
|
|
/// Either way we forward to the native Apple Maps directions URL. The
|
|
/// `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=…&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 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: userInfo
|
|
)
|
|
return
|
|
}
|
|
|
|
guard let items = components.queryItems
|
|
else { return }
|
|
|
|
// Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com).
|
|
if let daddr = items.first(where: { $0.name == "daddr" })?.value {
|
|
openMaps(daddr: daddr)
|
|
return
|
|
}
|
|
|
|
// Legacy relay: fuelboard://directions?lat=..&lng=..
|
|
if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init),
|
|
let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) {
|
|
openMaps(daddr: "\(lat),\(lng)")
|
|
}
|
|
}
|
|
|
|
private func openMaps(daddr: String) {
|
|
guard let mapsURL = URL(string: "maps://?daddr=\(daddr)&t=d") else { return }
|
|
UIApplication.shared.open(mapsURL)
|
|
}
|
|
}
|