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 | 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 |
Quotation branch end-screen (non-standard pax) | QuotationConfirmation |
Quotation branch: when the contact step response returns requires_quotation (non-standard pax/room configuration the backend can’t self-serve), ClientContactPage routes to /quotation instead of travelers; the customer returns later via a continuation link. See Checkout API — Quotation Flow.
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. 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]/