Airline integration work: AirlineLoadService updates, docs, JSX scripts
- AirlineLoadService: pass airport DB for timezone-aware date strings, add browser-shaped headers for United, expand JetBlue/Alaska/Emirates signatures to take origin, log/parse fixes for Korean Air. - FlightsApp: build AirlineLoadService with the airport DB and inject it. - JSX: continued WebView-based fetcher work plus updated JSX_NOTES. - Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md, add api_docs/ (StaffTraveler reverse-engineering captures + findings). - Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX protocol exploration. - .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-80
@@ -14,20 +14,25 @@ hardware against the live production site in April 2026.
|
||||
2. You **cannot** call that endpoint from outside a loaded `jsx.com` page —
|
||||
Akamai will reject the request with an HTTP/2 protocol error based on the
|
||||
TLS fingerprint of whatever's making the call.
|
||||
3. You **can** call it from inside a loaded `jsx.com` page, using the
|
||||
browser's own `fetch()` with the anonymous token the SPA stashes in
|
||||
`sessionStorage["navitaire.digital.token"]`. Same-origin + browser TLS +
|
||||
browser cookies means Akamai sees it as a normal user request.
|
||||
4. Driving the on-page UI to submit the search form is a dead end in
|
||||
**WKWebView** (but works in Playwright) because of a subtle trusted-event
|
||||
issue described below.
|
||||
3. Real Chrome succeeds when you actually drive the full one-way flow:
|
||||
select `One Way`, fill origin and destination, pick the depart date,
|
||||
click `DONE`, then click `FIND FLIGHTS`. That fires
|
||||
`POST /api/nsk/v4/availability/search/simple` and lands on
|
||||
`/booking/select`.
|
||||
4. In WKWebView, the safest runtime strategy is layered: drive the same UI
|
||||
flow first, try the real `FIND FLIGHTS` button, then fall back to the
|
||||
component's `search()` method, then finally to a direct in-page
|
||||
`fetch()` if page state still does not materialize.
|
||||
|
||||
The working pattern is therefore:
|
||||
|
||||
Navigate browser → jsx.com (real page load)
|
||||
Wait for SPA bootstrap (station buttons exist, token call finishes)
|
||||
Read token from sessionStorage
|
||||
POST /api/nsk/v4/availability/search/simple ← from inside the page
|
||||
Select One Way / route / date
|
||||
Click DONE
|
||||
Click FIND FLIGHTS
|
||||
Wait for booking/select or Angular availability state
|
||||
Fallback: component.search() or direct in-page POST if needed
|
||||
Parse the response
|
||||
|
||||
---
|
||||
@@ -281,70 +286,40 @@ won't surface any error to the DOM.
|
||||
|
||||
---
|
||||
|
||||
## WKWebView vs Playwright — the trusted-events trap
|
||||
## Browser validation and WKWebView strategy
|
||||
|
||||
This is the single most important finding from this session.
|
||||
The earlier "WKWebView cannot submit the form" theory was too strong. The
|
||||
browser probe that only failed `POST /search/simple` had not actually driven
|
||||
the same successful user flow Chrome uses.
|
||||
|
||||
### The symptom
|
||||
### What is confirmed
|
||||
|
||||
In WKWebView, after driving the entire UI (one-way, origin, destination,
|
||||
day cell, DONE button), the depart-date input visually shows `"Sat, Apr 11"`
|
||||
and the Find Flights button is visually enabled. Clicking it fires the click
|
||||
handler (you can prove it with a capture listener), but no POST to
|
||||
`/availability/search/simple` happens — ever. The only network call that
|
||||
fires after the click is the token refresh that was already scheduled.
|
||||
- Real Chrome does succeed when the automation performs the whole flow:
|
||||
`One Way` -> origin -> destination -> depart date -> `DONE` ->
|
||||
`FIND FLIGHTS`.
|
||||
- That path produces a live `POST /api/nsk/v4/availability/search/simple`
|
||||
with HTTP `200` and advances the page to `https://www.jsx.com/booking/select`.
|
||||
- The response contains per-flight availability counts that the app can map
|
||||
back to `XE` flights.
|
||||
|
||||
### The cause
|
||||
### What remains true in WKWebView
|
||||
|
||||
WKWebView's synthetic DOM events produced by
|
||||
`element.dispatchEvent(new MouseEvent('click', {...}))` have
|
||||
`event.isTrusted === false`. JSX's custom datepicker only commits its
|
||||
day-cell selection into its Angular `FormControl` on a **trusted** user
|
||||
gesture. So the synthetic click on a day cell visually highlights the cell,
|
||||
lets DONE close the picker, and updates the input's display value — but the
|
||||
underlying FormControl stays `null`.
|
||||
Synthetic events can still be brittle. A visually-filled form is not enough;
|
||||
the page may still reject the search if the component's internal state is not
|
||||
fully committed. Because of that, the iOS fetcher should not rely on exactly
|
||||
one trigger path.
|
||||
|
||||
When Find Flights is then clicked, the handler runs `search()`, which does:
|
||||
### Current iOS runtime strategy
|
||||
|
||||
```ts
|
||||
search() {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
this.api.post('/availability/search/simple', this.buildBody());
|
||||
}
|
||||
```
|
||||
`Flights/Services/JSXWebViewFetcher.swift` step 17 now does this:
|
||||
|
||||
Form is invalid because `departDate` FormControl is `null`, so the method
|
||||
returns early. No POST. Silent failure.
|
||||
1. Prime the Angular search component with the target date.
|
||||
2. Click the real `FIND FLIGHTS` button and wait for availability state.
|
||||
3. If that does not materialize data, call `component.search()`.
|
||||
4. If that still fails, issue the direct in-page `fetch()` POST as the last fallback.
|
||||
|
||||
### Why Playwright doesn't hit this
|
||||
|
||||
Playwright's `page.click()` and `locator.click()` don't produce synthetic
|
||||
DOM events — they go through CDP's `Input.dispatchMouseEvent`, which
|
||||
produces events indistinguishable from real user input, including
|
||||
`isTrusted === true`. JSX's picker accepts those and commits the date.
|
||||
|
||||
### Why Angular's `markAllAsTouched` workaround doesn't help
|
||||
|
||||
You can walk `__ngContext__` on every element looking for `FormControl`
|
||||
instances and call `markAllAsTouched()` + `updateValueAndValidity()`. This
|
||||
does trigger revalidation and reveals all the currently-invalid fields, but
|
||||
it doesn't populate the empty date control — it just marks it dirty. The
|
||||
underlying value is still `null` because nothing wrote to it.
|
||||
|
||||
### The WKWebView workaround
|
||||
|
||||
Don't click Find Flights. Instead, call
|
||||
`/api/nsk/v4/availability/search/simple` directly via `fetch()` from the
|
||||
page context after reading the token out of `sessionStorage`. You still do
|
||||
the UI-driving steps (for page warm-up, session establishment, and as a
|
||||
smoke test that the site is functional), but the actual data fetch happens
|
||||
via a direct API call whose request shape matches exactly what
|
||||
`search()` would have posted.
|
||||
|
||||
This is what `Flights/Services/JSXWebViewFetcher.swift` step 17 does.
|
||||
That keeps the app aligned with the proven Chrome behavior without betting the
|
||||
entire integration on one fragile DOM event path.
|
||||
|
||||
---
|
||||
|
||||
@@ -357,8 +332,8 @@ JSX fronts `api.jsx.com` with Akamai Bot Manager. Observed behavior:
|
||||
| Plain `curl`, `fetch` from Node, any external HTTP client | Blocked. Almost all endpoints return HTML challenge page or Akamai error. |
|
||||
| Playwright's built-in `chromium.launch()` (both bundled chromium and `channel: "chrome"`) | GET requests succeed, but `POST /availability/search/simple` specifically returns `ERR_HTTP2_PROTOCOL_ERROR`. Playwright injects enough automation bits for Akamai to flag the TLS/H2 fingerprint. |
|
||||
| Real Chrome spawned as a plain process + Playwright attached via `chromium.connectOverCDP()` | **Works reliably.** Chrome has the expected fingerprint and Playwright is only driving it via CDP, not altering it. |
|
||||
| WKWebView on macOS / iOS | GET requests succeed. Direct `POST /availability/search/simple` from inside a loaded `jsx.com` page via `fetch()` also succeeds. The browser session's cookies and TLS fingerprint are trusted. |
|
||||
| WKWebView with UI-driven Find Flights click | Fails — but for an unrelated reason (the trusted-events trap above). Angular never fires the POST in the first place, so Akamai never sees it. |
|
||||
| WKWebView on macOS / iOS | Same-origin traffic from a real loaded `jsx.com` page can work, but the most robust app strategy is still layered because DOM-driven submission can be sensitive to page state and timing. |
|
||||
| WKWebView with the current app flow | Try real `FIND FLIGHTS` first, then `component.search()`, then direct in-page `fetch()`. This is the runtime path the app now uses. |
|
||||
|
||||
Observations:
|
||||
|
||||
@@ -368,25 +343,20 @@ Observations:
|
||||
|
||||
---
|
||||
|
||||
## Network interceptor pattern
|
||||
## Network observation
|
||||
|
||||
If you want to capture JSX traffic programmatically (e.g. in WKWebView or
|
||||
Playwright), override `window.fetch` and `XMLHttpRequest` to tee every
|
||||
request/response to a probe object. Two things to track separately:
|
||||
For the app runtime, prefer passive observation over wrapping `fetch()` or
|
||||
`XMLHttpRequest`. The current iOS flow uses a `PerformanceObserver` to record
|
||||
`api.jsx.com` resource entries without touching the request pipeline.
|
||||
|
||||
1. **Initiated calls** — every URL passed to `fetch()` or
|
||||
`XMLHttpRequest.send()`, recorded *before* awaiting the response.
|
||||
2. **Completed calls** — every call that returned a response (any status),
|
||||
plus an error marker on network failure.
|
||||
That gives enough signal to answer:
|
||||
|
||||
This lets you tell apart "Angular's `search()` never fired a POST" (nothing
|
||||
in initiated calls) from "Angular fired a POST but the network rejected it"
|
||||
(initiated but not completed, error flag set). This distinction was critical
|
||||
for diagnosing the WKWebView trusted-events issue — without it, both failure
|
||||
modes look like "no searchSimple response seen".
|
||||
1. Did the page attempt `/availability/search/simple` at all?
|
||||
2. Did the page advance to `/booking/select`?
|
||||
3. Did Angular availability state appear after the search trigger?
|
||||
|
||||
See the `window.__jsxProbe` setup in
|
||||
`Flights/Services/JSXWebViewFetcher.swift` step 4 for a working
|
||||
`Flights/Services/JSXWebViewFetcher.swift` step 4 for the current
|
||||
implementation.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user