Checkout Flow (Frontend)
The frontend checkout lives in frontend/src/features/checkout/, rendered by the Astro pages under frontend/src/pages/[market]/checkout/[offerId]/. Each step is a React island; navigation between steps uses Astro View Transitions (client-side) so shared state survives the hop.
When to Use
Section titled “When to Use”- Changing any checkout step UI, selection persistence, or the payment form
- Understanding why the frontend never computes a price (it doesn’t — see Price Display)
One Astro page per step; the numbered titles come from the pages’ step config:
Route (/[market]/checkout/[offerId]/…) |
Step | Component |
|---|---|---|
/loading |
Pre-step: “Preparando tu viaje” — live economy re-pricing while the session starts | CheckoutLoadingScreen (feature checkout-loading) |
| (index) | 1. Vuelos de ida y vuelta — economy selection + business upgrade search | CheckoutPage |
/hotels |
2. Alojamiento — optional hotel upgrades. Tier tabs carry a one-line explanation of each tier and every card badges the hotel’s own category (5★ Incluido / 5★ Exclusivo / 5★ Gran Lujo, via frontend/src/shared/utils/hotelCategory.ts) |
HotelSelectionPage |
/activities |
3. Experiencias — optional activities | ActivitySelectionPage |
/transfers |
4. Otros — transfers, luxury transfer, travel insurance | TransferSelectionPage + InsuranceSelector |
/contact |
5. Contacto — booking contact | ClientContactPage |
/travelers |
6. Datos de los viajeros — per-passenger data | TravelerDataPage |
/summary |
7. Resumen final y pago | SummaryPaymentPage |
/quotation |
Orphaned end-screen of the retired quotation branch — see below | QuotationConfirmation |
Every occupancy reaches payment (#2292): ClientContactPage always navigates to /travelers. It used to route non-standard occupancy (pax != 2 or a non-default room configuration) to /quotation when the contact response carried requires_quotation; that field no longer exists on the backend payload and the page no longer reads it, so a stray flag from an old backend cannot resurrect the redirect (ClientContactPage.test.tsx guards this). The /quotation page and QuotationConfirmation component still exist but nothing in the frontend links to them — the route is only reachable by typing the URL. See Checkout API — Quotation Flow.
Continuation link: /travelers?resume=<token> is a real entry point — TravelerDataPage calls resumeQuotationCheckout() to rehydrate an agent-confirmed quotation before rendering.
Quote link: /loading?quote=<token> is the entry point for an agent’s quote (Checkout API — Quote Links). loading.astro passes the token to CheckoutLoadingScreen and strips it from the redirect, so reloading the flights step never restarts the quote. The screen calls startQuoteCheckout() before the economy search; if the backend refuses the quote, its message replaces the generic unavailable text and the customer does not continue. The session then carries quote_prefill:
- Flight:
CheckoutPagepre-selects the live-search option whose flight numbers and departure dates match the quote (utils/quotePrefill.ts→findQuotedFlight), marked as a manual pick so bound-flight reconciliation keeps it. Times are not compared, because schedules can shift. When nothing matches, the usual default stays. Business-cabin quotes are not pre-selected. - Insurance:
InsuranceSelectorreceivesquotedPolicyIdand waits for that policy’s quote before auto-applying anything: a valid quote applies it once instead of the included base, and a failed one falls back to the base.TransferSelectionPagerenders the insurance section only once the session has loaded, so the base cannot auto-apply before the quoted policy is known.
Edit mode: from the summary, each section links back to its step with ?edit=summary (utils/editMode.ts); the step then shows CANCELAR/GUARDAR and returns to the summary on save.
Step tracking: every step’s Astro frontmatter calls trackCheckoutStepSSR() (features/checkout/server/), which PATCHes the funnel step to the backend using the booking id stored in Astro.session — reliable, no client JS involved. GA4 events are fired client-side (see GA4 Ecommerce Tracking).
State Management
Section titled “State Management”- Server is the source of truth. Every selection change PUTs to the checkout API (
api/checkoutApi.ts) and each step re-hydrates fromGET /checkouton mount. There is no client-side cart. This includes the flight step’s automatic re-anchors: when the sort or stops filter moves the selection to another flight,CheckoutPage.handleFlightSelectstill commits it (so the header total follows), but flags itsilentto skip the “Vuelo seleccionado” toast and theselect_flightevent. store/checkoutTotalStore.ts— the one zustand store: it caches the last backend-committedtotal_price+price_per_person(always set together so they never drift) and the pax count (display copy only). Because steps navigate viagoToStep()(utils/checkoutNavigate.ts, Astro’snavigate()), the JS module graph survives and the header shows the committed price instantly instead of flashing the offer’s advertised 2-pax price. Genuine exits (home, payment gateway, confirmation) stay full page loads.- Per-step selection state is local React
useReducer/useState, seeded from the session response. - Business-flight search results are cached in
localStoragefor 25 minutes (CheckoutPage).
Price Display Rules
Section titled “Price Display Rules”The frontend never computes prices. Pricing is a single backend responsibility (Checkout API): every price on screen is printed from an API response — the header total and “/persona” figure come from total_price / price_per_person committed together into the store; per-person is never divided on the client. The former client-side delta helpers were deliberately removed (utils/priceCalculations.ts now only contains a flight type guard). Checkout amounts are whole-euro per person (per-person × pax === total), rounded by the backend (Offer::roundToWholeUnit). The only client multiplication is a GA4 analytics value — never a displayed price.
Participant Count for Activities (#2209)
Section titled “Participant Count for Activities (#2209)”Activities are priced per person, but customers can book them for a subset of the party. ActivitySelectionPage keeps a Map<activityOptionId, participants>; selecting an activity defaults participants to the full pax count, and a stepper in ActivitySelector/DaySection adjusts it, clamped to [MIN_ACTIVITY_PARTICIPANTS, actual_pax_count]. Each change persists {activity_id, day_number, participants} to the API and the backend reprices — nothing is computed on the client. Selections restored from sessions saved before this feature fall back to the full pax count.
Payment (Summary Step)
Section titled “Payment (Summary Step)”SummaryPaymentPage drives payment through api/paymentApi.ts:
createSetupIntent()creates the booking on the backend and returns either a Stripe SetupIntent or a Redsys redirect form, depending on the gateway the backend chose.- Stripe:
Summary/StripePaymentForm.tsxlazily loads Stripe.js (@stripe/react-stripe-jsElements +PaymentElement, module-level singleton) inside the sharedPaymentModulecard UI; onconfirmSetupsuccess,confirmPayment()finalizes on the backend, handling a possible 3DSaction_urlredirect. - Redsys:
Summary/RedsysRedirectForm.tsxrenders the standard hidden auto-submitting form (signature version, merchant parameters, signature) to the gateway URL; pending-payment state is stashed in the Astro session so the middleware can recover the confirmation if the user closes the tab, and the GA4 purchase payload is parked insessionStorageto fire after the redirect. - Success exits to
/[market]/confirmation/[reference]with a full page load.
The Stripe publishable key arrives as an SSR prop (STRIPE_PUBLISHABLE_KEY env, see frontend/astro.config.mjs); a missing key renders an error alert instead of the form.
Business Rules
Section titled “Business Rules”- All prices displayed are backend-stored values; the client renders, never calculates
- Selections persist server-side on every change, so back-navigation and edit mode never lose choices
- Inter-step navigation must use
goToStep(); exits must be full navigations - Activity participants are clamped to the party size and repriced by the backend
Related
Section titled “Related”- Checkout API — the backend contract for every step
- GA4 Ecommerce Tracking — funnel events fired from these components
- Payment Service — gateway selection and finalization
- Source:
frontend/src/features/checkout/(components,api/,store/,utils/,server/) - Source:
frontend/src/pages/[market]/checkout/[offerId]/