From 4a939340a2af7f869017647b6ccacc2789b9c894 Mon Sep 17 00:00:00 2001 From: Trey T Date: Tue, 26 May 2026 14:23:33 -0500 Subject: [PATCH] =?UTF-8?q?Remove=20Spirit=20Airlines=20(defunct=20?= =?UTF-8?q?=E2=80=94=20merged=20into=20Frontier)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spirit ceased operations, so the fetchSpiritStatus path and all NK references are dead code. Pulled out: - AirlineLoadService: drop `case "NK"` from the router, delete fetchSpiritStatus (the GetFlightInfoBI POST that was returning 403 even after our APIM key was accepted). - FlightLoadDetailView: drop the `schedule.airline.iata == "NK"` branch and the spiritUnavailableView placeholder. - FlightLoad model: update the airlineCode comment. - AirlineLoadIntegrationTests: remove test_NK_spirit and drop "NK" from statusOnlyAirlines / knownDailyFlights fallback table. - AIRLINE_INTEGRATION_GUIDE.md: tombstone the Spirit section and remove it from the cheat-sheets and recommendations. Test suite now: 6 airlines passing (AA, AS, B6, EK, KE, UA), 1 skipped (XE — WKWebView host required), 0 failures, runs in ~10s. Co-Authored-By: Claude Opus 4.7 (1M context) --- AIRLINE_INTEGRATION_GUIDE.md | 24 ++------ Flights/Models/FlightLoad.swift | 2 +- Flights/Services/AirlineLoadService.swift | 56 +------------------ Flights/Views/FlightLoadDetailView.swift | 12 ---- .../AirlineLoadIntegrationTests.swift | 20 ++----- 5 files changed, 11 insertions(+), 103 deletions(-) diff --git a/AIRLINE_INTEGRATION_GUIDE.md b/AIRLINE_INTEGRATION_GUIDE.md index fad9fb7..bf12432 100644 --- a/AIRLINE_INTEGRATION_GUIDE.md +++ b/AIRLINE_INTEGRATION_GUIDE.md @@ -12,7 +12,7 @@ Drop-in reference for integrating flight load / seat availability data from 11 a | B6 | ✅ Status-only | Confirms flight exists; no load data without check-in session | | EK | ✅ Status-only | Confirms flight exists; load data requires PNR | | KE | ✅ Working | Returns seat count only (no capacity) | -| NK | ❌ Broken (2026-05-26) | `GetFlightInfoBI` returns HTTP 403 with `{"getFlightInfoBIResult":null}`. APIM key is still accepted (401 without it), but the call itself is now rejected. Likely endpoint deprecation or new auth requirement. Spirit APK in `airlines/com.spirit.*` may have the new shape. | +| ~~NK~~ | Removed | Spirit Airlines ceased operations (merged into Frontier). Removed from `AirlineLoadService` and tests. | | XE | Manual only | WKWebView path; unit tests can't exercise it | --- @@ -208,23 +208,9 @@ Script ready at `scripts/jsx_availability.js`. --- -## 5. Spirit Airlines — PARTIAL (status only, no standby) +## 5. ~~Spirit Airlines~~ — DEFUNCT -**What you get:** Flight status, station/route data. **No standby — Spirit is a ULCC and doesn't run standby lists.** - -**Auth:** Static APIM key (decrypted). Plain curl for GETs; POSTs mostly blocked by Akamai CyberFend sensor. - -**Key:** `c6567af50d544dfbb3bc5dd99c6bb177` - -```bash -curl -X POST "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI" \ - -H "Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177" \ - -H "Content-Type: application/json" \ - -H "Platform: Android" \ - -d '{"departureStation":"FLL","arrivalStation":"ATL","departureDate":"2026-04-08"}' -``` - -Seat maps require JWT + CyberFend sensor data (real device + Frida hook only). +Spirit ceased operations and merged into Frontier. Removed from the codebase entirely. Section retained as a placeholder so the numbering below doesn't shift. --- @@ -356,7 +342,6 @@ Same backend powers Lufthansa, SWISS, Austrian, Brussels. |------------|--------------------|---------| | Alaska | APIM key header | Lowest (curl works) | | Emirates | none | Lowest (curl works) | -| Spirit | APIM key (GET only)| Low (curl works) | | JetBlue | apikey header | Low (curl works) | | Korean Air | `channel` header | Low (Playwright or curl) | | JSX | Playwright → JWT | Medium | @@ -382,7 +367,6 @@ These four are the core of any flight-load product. Alaska is the easiest to int ## Tier 2: Status only (useful, but no seat data) -- **Spirit** — status/routes, no standby (ULCC) - **Emirates** — status, zero auth - **Korean Air** — status; `flightSeatCount` returns 0 far out - **JetBlue** — status + route DB; loads need PNR @@ -399,7 +383,7 @@ These four are the core of any flight-load product. Alaska is the easiest to int 3. Layer in American for the third major US carrier. 4. JSX as a bonus — only route pairs that JSX serves (private terminals). 5. For Delta/JetBlue: show flight status only, note "seat data unavailable" unless you have a PNR. -6. Use Emirates/Korean Air/Spirit for status on international/ULCC routes. +6. Use Emirates/Korean Air for status on international routes. ## Shared integration notes diff --git a/Flights/Models/FlightLoad.swift b/Flights/Models/FlightLoad.swift index 944b0d0..0f2a9de 100644 --- a/Flights/Models/FlightLoad.swift +++ b/Flights/Models/FlightLoad.swift @@ -2,7 +2,7 @@ import Foundation /// Flight load data from airline APIs struct FlightLoad: Sendable { - let airlineCode: String // "UA", "AA", "KE", "NK" + let airlineCode: String // "UA", "AA", "KE", etc. let flightNumber: String // "UA2238" let cabins: [CabinLoad] // Full cabin data (United) let standbyList: [StandbyPassenger] diff --git a/Flights/Services/AirlineLoadService.swift b/Flights/Services/AirlineLoadService.swift index 3b0d683..ab56310 100644 --- a/Flights/Services/AirlineLoadService.swift +++ b/Flights/Services/AirlineLoadService.swift @@ -42,7 +42,6 @@ actor AirlineLoadService { switch code { case "UA": return await fetchUnitedLoad(flightNumber: flightNumber, date: date, origin: origin) case "AA": return await fetchAmericanLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination) - case "NK": return await fetchSpiritStatus(origin: origin, destination: destination, date: date) case "KE": return await fetchKoreanAirLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination) case "B6": return await fetchJetBlueStatus(flightNumber: flightNumber, date: date, origin: origin) case "AS": return await fetchAlaskaLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination) @@ -413,59 +412,6 @@ actor AirlineLoadService { } } - // MARK: - Spirit Airlines - - private func fetchSpiritStatus(origin: String, destination: String, date: Date) async -> FlightLoad? { - guard let url = URL(string: "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI") else { - print("[NK] Invalid URL") - return nil - } - - let dateStr = dayString(from: date, originIATA: origin) - let body: [String: String] = [ - "departureStation": origin.uppercased(), - "arrivalStation": destination.uppercased(), - "departureDate": dateStr - ] - - print("[NK] POST \(url) body: \(body)") - - do { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("c6567af50d544dfbb3bc5dd99c6bb177", forHTTPHeaderField: "Ocp-Apim-Subscription-Key") - request.setValue("Android", forHTTPHeaderField: "Platform") - request.httpBody = try JSONSerialization.data(withJSONObject: body) - - let (data, response) = try await session.data(for: request) - let http = response as? HTTPURLResponse - print("[NK] HTTP status: \(http?.statusCode ?? -1)") - - if let bodyStr = String(data: data, encoding: .utf8) { - print("[NK] Response body: \(bodyStr.prefix(500))") - } - - guard http?.statusCode == 200 else { - print("[NK] Non-200 response") - return nil - } - - // Spirit is a ULCC with no standby program. - return FlightLoad( - airlineCode: "NK", - flightNumber: "NK", - cabins: [], - standbyList: [], - upgradeList: [], - seatAvailability: [] - ) - } catch { - print("[NK] Error: \(error)") - return nil - } - } - // MARK: - Korean Air private func fetchKoreanAirLoad( @@ -1069,7 +1015,7 @@ actor AirlineLoadService { return result.isEmpty ? trimmed : result } - /// "yyyy-MM-dd" formatter for United, American, Spirit. + /// "yyyy-MM-dd" formatter for United and American. /// NOTE: this is UTC-pinned and will cross the day boundary for users in /// non-UTC timezones. Prefer `dayString(from:originIATA:)` which resolves /// the departure airport's approximate local timezone. diff --git a/Flights/Views/FlightLoadDetailView.swift b/Flights/Views/FlightLoadDetailView.swift index 6c423ce..1807e90 100644 --- a/Flights/Views/FlightLoadDetailView.swift +++ b/Flights/Views/FlightLoadDetailView.swift @@ -25,8 +25,6 @@ struct FlightLoadDetailView: View { .tint(FlightTheme.accent) } else if let error { errorView(error) - } else if schedule.airline.iata.uppercased() == "NK" { - spiritUnavailableView } else if let load { loadContent(load) } else { @@ -114,16 +112,6 @@ struct FlightLoadDetailView: View { } } - // MARK: - Spirit Unavailable - - private var spiritUnavailableView: some View { - ContentUnavailableView { - Label("Not Available", systemImage: "info.circle") - } description: { - Text("Spirit Airlines does not provide standby or load data.") - } - } - // MARK: - Unsupported Airline /// Shown when fetchLoad returns nil. That can be either: diff --git a/FlightsTests/AirlineLoadIntegrationTests.swift b/FlightsTests/AirlineLoadIntegrationTests.swift index 89e7aad..6e2ce2a 100644 --- a/FlightsTests/AirlineLoadIntegrationTests.swift +++ b/FlightsTests/AirlineLoadIntegrationTests.swift @@ -19,8 +19,6 @@ import XCTest /// Pre-existing limitations (NOT bugs in these tests): /// - JSX (XE) uses a WKWebView path and can't run from unit tests on the /// simulator without a host scene. Skipped with a `XCTSkip`. -/// - Spirit (NK) intentionally returns an empty FlightLoad — they have no -/// standby/load program. Test just asserts non-nil. /// - Some carriers (notably AA, AS waitlist) only open the load endpoint /// close to departure. Tests prefer flights leaving < 24h out and skip /// with a helpful message if nothing's findable. @@ -39,16 +37,15 @@ final class AirlineLoadIntegrationTests: XCTestCase { /// Airlines whose load endpoint deliberately returns only flight /// status (no seat/standby data). We assert non-nil for these and /// stop short of the "must have data" check. - private static let statusOnlyAirlines: Set = ["NK", "B6", "EK"] + private static let statusOnlyAirlines: Set = ["B6", "EK"] /// Hardcoded daily flights used as fallbacks when route-explorer's /// `/departures` data doesn't include the carrier we're looking for - /// (notably ULCCs like NK and some international carriers like EK/KE - /// that aren't in route-explorer's schedule feed). Each entry is a - /// well-known daily operation that's been stable over time; if any - /// of these stop operating, update the entry. + /// (notably some international carriers like EK/KE that aren't in + /// route-explorer's schedule feed). Each entry is a well-known daily + /// operation that's been stable over time; if any of these stop + /// operating, update the entry. private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [ - "NK": ("401", "LAS", "FLL"), // Spirit Las Vegas → Fort Lauderdale, daily "EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship "KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily ] @@ -83,13 +80,6 @@ final class AirlineLoadIntegrationTests: XCTestCase { ) } - func test_NK_spirit() async throws { - try await runAirlineLoadTest( - carrier: "NK", - hubs: ["FLL", "MCO", "LAS", "DTW", "ORD"] - ) - } - func test_KE_koreanAir() async throws { try await runAirlineLoadTest( carrier: "KE",