History v2: everything — Wallet auto-prompt, age, track replay, share
Adds the deferred pieces from the v1 ship, plus a Mail Share
Extension target so the iOS share sheet picks up flight emails.
Track replay
- `LoggedFlight.icao24` field — populated from FR24 enrichment on
live-tap adds.
- HistoryDetailView's track query now fires for any flight younger
than 7 days that has an icao24, pulling the actual flown path
from OpenSky's /tracks/all endpoint. Falls back to a clean
great-circle arc otherwise.
Wallet auto-prompt
- RootView subscribes to WalletPassObserver.shared. When the user
adds a boarding pass to Apple Wallet, the observer's published
`pendingPass` flips and we present AddFlightView pre-filled with
the parsed origin / destination / flight # / date.
Airframe age + first-flight date
- `AirframeMetadataService` queries OpenSky's
/api/metadata/aircraft/icao/{icao24} endpoint. Caches results in
the existing `AirframeMetadata` SwiftData model so we never
re-fetch the same airframe twice. (jetphotos and planespotters
pages are both Cloudflare-gated; OpenSky's metadata API is the
cleanest free source.)
- HistoryDetailView fires the lookup on appear and persists the
result; the aircraft card already renders "Age" when a date is
cached.
Mail Share Extension
- New `FlightsShareExtension` Xcode target (app-extension product
type) built into the app bundle via an Embed Foundation
Extensions copy phase.
- `ShareViewController` (SLComposeServiceViewController) parses
shared text + URLs for flight-shaped codes ("AA 2178"), route
hints ("DFW → ORD"), and date strings.
- On Save, the extension builds a `flights://import?carrier=…&num=
…&dep=…&arr=…&date=…` URL and opens it via the responder-chain
openURL trick (Share Extensions can't access UIApplication
directly).
- Host app handles the URL via `.onOpenURL` in RootView, switches
to the History tab and presents AddFlightView prefilled.
- App now has an actual Info.plist (CFBundleURLTypes registered
for `flights://`); switched from GENERATE_INFOPLIST_FILE to
INFOPLIST_FILE for the app target.
If the dev portal hasn't registered bundle id
`com.flights.app.share` for the team, the signed archive will
fail. In that case the simpler URL-scheme path still works —
users can hit `flights://import?...` from a Shortcut.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,9 @@ struct HistoryDetailView: View {
|
||||
@State private var track: AircraftTrack?
|
||||
@State private var editedNotes: String = ""
|
||||
@State private var showDeleteConfirm = false
|
||||
/// Re-render trigger after we upsert airframe metadata. SwiftData
|
||||
/// changes don't auto-invalidate non-@Query views.
|
||||
@State private var metadataLoaded = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -44,9 +47,10 @@ struct HistoryDetailView: View {
|
||||
.task {
|
||||
editedNotes = flight.notes ?? ""
|
||||
if let reg = flight.registration {
|
||||
photo = await AircraftPhotoService.shared.photo(registration: reg, icao24: "")
|
||||
photo = await AircraftPhotoService.shared.photo(registration: reg, icao24: flight.icao24 ?? "")
|
||||
}
|
||||
await loadTrackIfRecent()
|
||||
await loadAirframeMetadata()
|
||||
}
|
||||
.alert("Delete this flight?", isPresented: $showDeleteConfirm) {
|
||||
Button("Delete", role: .destructive) {
|
||||
@@ -186,23 +190,38 @@ struct HistoryDetailView: View {
|
||||
}
|
||||
|
||||
private func loadTrackIfRecent() async {
|
||||
// OpenSky's anonymous track endpoint goes back roughly 7 days
|
||||
// before they trim history. Older logs get the great-circle
|
||||
// fallback drawn by FlightRouteMap.
|
||||
// OpenSky's anonymous track endpoint trims history after ~7
|
||||
// days. Older logs get the great-circle fallback drawn by
|
||||
// FlightRouteMap.
|
||||
let ageDays = Date().timeIntervalSince(flight.flightDate) / 86400
|
||||
guard ageDays < 7, let icao24 = guessICAO24() else { return }
|
||||
guard ageDays < 7, let icao24 = flight.icao24, !icao24.isEmpty else { return }
|
||||
track = await openSky.track(icao24: icao24)
|
||||
}
|
||||
|
||||
/// We don't store icao24 on the LoggedFlight (we store registration
|
||||
/// instead) — but for track replay we need icao24. Future work: pull
|
||||
/// reg→icao24 mapping from a fresh OpenSky lookup. For now, only the
|
||||
/// most-recently-logged airframe gets a replay attempt.
|
||||
private func guessICAO24() -> String? {
|
||||
// TODO: tie this to a reg→icao24 resolution. For v1 the
|
||||
// track replay only fires when icao24 is in notes or we
|
||||
// resolve via aircraft DB.
|
||||
return nil
|
||||
/// Hit OpenSky's metadata endpoint for first-flight / built dates.
|
||||
/// We persist the result so subsequent views of the same airframe
|
||||
/// don't re-query the network. Best-effort — many newer airframes
|
||||
/// have no metadata yet.
|
||||
private func loadAirframeMetadata() async {
|
||||
guard let reg = flight.registration,
|
||||
!reg.isEmpty,
|
||||
let icao24 = flight.icao24,
|
||||
!icao24.isEmpty
|
||||
else { return }
|
||||
// Skip if we already have a cached entry with at least one date.
|
||||
if let cached = store.airframe(for: reg),
|
||||
cached.firstFlightDate != nil || cached.deliveryDate != nil {
|
||||
metadataLoaded.toggle()
|
||||
return
|
||||
}
|
||||
if let meta = await AirframeMetadataService.shared.metadata(forICAO24: icao24) {
|
||||
store.upsertAirframe(
|
||||
registration: reg,
|
||||
firstFlightDate: meta.firstFlightDate,
|
||||
deliveryDate: meta.built
|
||||
)
|
||||
metadataLoaded.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Aircraft card
|
||||
|
||||
Reference in New Issue
Block a user