Docs Hub Basket route

Overview

web-shop-basket is a React 18 basket SPA served under /basket by an Express edge. Route ownership is intentionally shallow, but render ownership below BasketJourney is deep, stateful, and heavily influenced by transformed basket payloads, CMS content, and LaunchDarkly-backed flags.

#overview

Client runtime

React 18, React Router 6, MobX singletons, Source Web chrome, and federated runtime bootstrap.

Server edge

Express plus vfukServer or vfukDevServer, DXL proxying, auth redirects, mock routes, and Vite dev middleware.

Primary journeys

/basket, /customer-transfer/basket, review-mode embeds, and modal-only query branches.

Runtime drivers

Basket API payloads, content-service assets, LaunchDarkly flags, auth cookies, and deeplink query parameters.

The fastest way to understand ownership is: src/client/index.tsx -> src/client/routes.tsx -> BasketPage -> BasketJourney -> BasketContainer -> package owners such as BasketPackage, BasketCombiPackage, and PackageItemList.

App bootstrap

src/client/index.tsx is the real entry point. It runs a strict ordered sequence — theme, auth, federated runtime, React root — before the first component mounts.

#app-bootstrap
  1. Theme initialisation

    Sets themeWS10 base asset URL and icon URL from the CDN environment variable. Must run before any Source Web component renders so the correct asset paths are available globally.

  2. Auth session check

    authService.session() (from @vfuk/utils-shop-auth-service) runs synchronously. It validates the existing session and silently re-authenticates if needed. Only fires when window exists — not SSR.

  3. Federated runtime initialisation

    initFederatedRuntime({ debugEnabled, sharedDependencyBaseUrl }) sets the shared import-map base URL to /basket/federated/shared or the CDN equivalent. debugEnabled is true whenever TEALIUM_ENVIRONMENT !== 'prod'.

  4. Runtime bootstrap (async)

    runtime.bootstrap() resolves the federated dependency graph. React does not mount until this promise settles — this guarantees shared React/MobX libraries come from the import map, not a bundled copy inside each entry.

  5. Dynamic import preload override

    window.__dynamicImportPreload__ is patched to prefix all federated preload paths with /basket (or CDN domain). Without this, lazy-loaded federated chunks would resolve against the wrong base URL.

  6. React root render

    ReactDOM.createRoot(root).render() mounts: SourceProvider (theme + i18n) → HelmetProviderBrowserRouterAppRoutes. In non-production, MockSelector is also injected at root level outside the router.

Provider stack

  • SourceProvider — supplies themeWS10 theme and locale strings to all Source Web components.
  • HelmetProvider — manages <head> tags (page title, meta) across route changes.
  • BrowserRouter — enables React Router 6 client-side routing under /basket.
  • MockSelector — dev-only overlay injected at root, survives route changes, not inside the router tree.

Routes wired in routes.tsx

  • /basketMainTemplateBasketPage (standard browser)
  • /basket (webview) → WebViewTemplateBasketPage
  • /customer-transfer/basketSeamlessMigrationTemplateBasketPage
  • Template choice is resolved by isWebView() and route path matching — not by a flag.

Tech stack

The repo is effectively a UI shell, a service facade layer, and an Express edge with shared platform middleware.

#tech-stack
Layer Primary tech Where it shows up Why it matters
Browser app React 18, React Router 6, StrictMode src/client/index.tsx, src/client/routes.tsx Mounts the SPA, owns the only client-side route split, and hosts the provider stack.
State model MobX singletons src/client/store/root.store.ts, basket.state.ts Most UI branches read directly from global basketStore and uiStore state instead of context or Redux.
Design system Source Web MainTemplate.tsx, BasketJourney.tsx Provides the page shell, overlays, headings, notifications, grids, and most primitive UI controls.
Federation @vf/federated-core, react-shadow index.tsx, BasketContainer.tsx Bootstraps shared dependencies and enables review-mode rendering inside a Shadow DOM root.
Server edge Express, vfukServer, vfukDevServer, Vite src/server/development/server.ts, src/server/production/server.ts Owns auth, feature flags, mocks, proxying, content transformation, and asset delivery.
Flag and content delivery LaunchDarkly-backed service plus content-service featureFlagging.middleware.ts, content.service.ts Flags and CMS content are loaded before the basket body is considered fully renderable.
Testing and delivery Jest, Cypress, Percy, SonarQube, Azure Pipelines package.json, cypress/, cicd/ Local development, PR validation, visual testing, and release automation all live in-repo.

Getting started

README-backed commands and local modes that matter when you are trying to reproduce basket behavior quickly.

#getting-started
Prerequisites

Node 22.14.0, Yarn, and Bastion for full backend access. Chrome or Firefox are the intended browser targets.

Install

yarn in the repo root, then cd cypress && yarn for E2E support.

Run the app

yarn start then open https://localhost:8000/basket. Use yarn start:client when you need a lighter local path.

Mock mode

Append ?mock=true to the basket URL or set the basketMock cookie to a fixture path under cypress/fixtures/basket.

Build

yarn build:prod runs the client and server Vite builds and copies fixtures into the build output.

Unit tests

yarn test, yarn test:cc, and yarn test:watch drive the Jest and Testing Library suite.

E2E and visual tests

yarn cypress:open, yarn cypress:run, and Percy-backed visual runs live under cypress/.

Useful local deep links

README examples cover SIMO, broadband, EVO, tablet, watch, and other deeplink basket creation scenarios.

High-level architecture

The app is easier to reason about as three ownership layers: browser shell, basket domain, and Express edge.

#architecture

1. Browser shell

src/client/index.tsx starts auth, bootstraps federated dependencies, and mounts the router and Source Web providers.

2. Basket domain

BasketPage, BasketJourney, BasketContainer, and the package pipeline decide what the customer can see and change.

3. Express edge

The server owns feature flag evaluation, mocks, login redirect, DXL proxying, and index routing before the browser ever receives the SPA shell.

Browser-side ownership client

The client route tree is intentionally small: AppRoutes picks a wrapper, BasketPage decides whether to show the journey or a skip spinner, and BasketJourney hands off to either EmptyBasket or BasketContainer. The real complexity starts after that handoff because package rows, banners, add-ons, modals, and totals all branch from transformed basket payloads and flags.

Server-side ownership server

Development and production servers share the same core responsibilities: load config, enable feature flags, optionally mount mocks, rewrite or redirect auth endpoints, proxy basket traffic, and finally serve the SPA shell. Development adds Vite middleware and a local HTTPS server; production switches to vfukServer, static asset serving, and Datadog tracing.

External systems that drive behavior integrations

The basket depends on the basket API, content-service, LaunchDarkly, IDM login, DXL proxying, analytics, and Azure pipeline or release infrastructure. Those systems are not just delivery details: their payloads and availability decide which routes, banners, CTAs, and deep child components the customer actually sees.

Federated runtime

In this repo, federated means a small set of basket surfaces are built as separately addressable runtime modules and loaded through import maps under /basket/federated; it does not mean every child component is remote.

#federated-runtime

Browser bootstrap

src/client/index.tsx calls initFederatedRuntime({ sharedDependencyBaseUrl }), waits for runtime.bootstrap(), and only then renders React. Shared React libraries come from the shared import map instead of being bundled into every entry.

Build output

tools/vite/federated/compiler.ts walks src/client/federated/modules/<area>/<entry>, builds each folder in Vite library mode, externalizes React packages, and generates entries-import-map.json.

Server delivery

federatedImportMaps.middleware.ts serves the shared and entry import maps, while getUpdatedImportMap() rewrites /basket URLs to the configured CDN domain when CDN delivery is enabled.

What federated actually buys this repo runtime contract

The basket codebase still ships as one main SPA, but federation gives the platform smaller runtime contracts it can load independently: a full journey shell, a non-empty basket container, and a shared basket service. Those surfaces export host-friendly entry points such as mount(), unmount(), update(), and validateProps() instead of only exporting React components for local imports.

Shared dependency and import-map flow SystemJS

Shared runtime dependencies live under src/client/federated/shared/import-map.json. The client bootstrap sets sharedDependencyBaseUrl to /basket/federated/shared or to the CDN equivalent, and window.__dynamicImportPreload__ prefixes runtime preloads with /basket. That is why hosts can share React instead of each federated entry bundling its own copy.

Server runtime

The Express edge is not a thin static server; it is the feature flag, auth, mocking, proxy, and index-routing layer for the basket SPA.

#server-runtime

Development server

src/server/development/server.ts creates an HTTPS Express app, enables CORS for import-map-overrides support, mounts Vite middleware, opens the local basket URL automatically, and serves build assets from =build/client.

Production server

src/server/production/server.ts loads config from .env, mounts static assets from the production client build, and starts with vfukServer.startServer({ syncKeepAliveWithAlb: true }).

Common middleware responsibilities middleware stack
  1. express.json(), express.urlencoded(), and cookieParser() normalize request input.
  2. initFeatureFlagService() and featureFlaggingMiddleware() evaluate LaunchDarkly-backed flags and expose GET /basket/flags.
  3. mockingMiddleware() mounts local mock routes for basket, validation, content, portability, and auth session traffic when enabled.
  4. cleanRouteMiddleware() and contentAPITransformer() rewrite route and content-service requests into the platform shape.
  5. authRedirectMiddleware(), loginRedirect(), and idmMiddleware() protect basket routes and map login/session endpoints.
  6. dxlProxyMiddleware() forwards basket API traffic through the configured encrypted gateway.
  7. Index routing and static asset serving finally return the SPA shell and federated bundles.
Mocking and local fixture behavior local dev

mocking.middleware.ts mounts a dedicated local API surface under /basket, including /basket/mockData, /basket/auth/session, /basket/api/content/asset, /basket/api/content-service/v2/content, and a range of basket validation and mutation endpoints. That is why mock mode can drive meaningful local journeys instead of just returning static HTML.

Feature flag and config bootstrap config

getGlobalConfig() loads environment variables, parses FEATURE_FLAGS into typed config objects, and decides which middleware and proxies are enabled. pathPrefix is consistently /basket, and route prefixes come from src/common/config/route.ts.

Server proxies

The server-side request path is layered. Some requests are rewritten locally, some are served from local files, and some are forwarded into DXL or the wider shop estate.

#server-proxies

The important distinction is that assetProxy and federated import-map routes keep traffic local, dxlProxyMiddleware() owns basket API traffic, and envProxy() is the escape hatch for non-basket requests that should continue into the broader shop stack.

How the proxy stack is layered in practice request flow
  1. cleanRouteMiddleware() strips the /digital/vX prefix and normalizes basket paths.
  2. contentAPITransformer() rewrites specific content-service and footer or meganav requests into the payload shape the downstream services expect.
  3. authRedirectMiddleware(), loginRedirect(), and idmMiddleware() can intercept auth-scoped routes before any business proxy is reached.
  4. Basket API requests then flow through dxlProxyMiddleware(), while image and environment-wide requests use the dedicated image or environment proxies instead.
  5. Local assets, /basket/federated bundles, and the import-map JSON never leave the basket server at all.

Route and runtime map

The route model is simple on paper, but the wrappers and query branches change ownership more than the raw route count suggests.

#route-runtime-map
flowchart TD A["src/client/index.tsx
federated bootstrap + BrowserRouter"] --> B["src/client/routes.tsx"] B --> C["/basket -> MainTemplate"] B --> D["/basket in webview -> WebViewTemplate"] B --> E["/customer-transfer/basket -> SeamlessMigrationTemplate"] C --> F["BasketPage"] D --> F E --> G["BasketPage
isSeamlessMigrationRoute"] F --> H["BasketJourney"] G --> H H --> I["BasketStatus + CTA strip + global modals"] H --> J["EmptyBasket"] H --> K["BasketContainer"] K --> L["Title and banners"] K --> M["BasketContents"] K --> N["Discounts + totals + RPI"] M --> O["BasketPackage / BasketCombiPackage"] O --> P["PackageHeader / RenderPackageBody"] P --> Q["PackageItemList"] Q --> R["BasketHardware / BasketBundle / BasketExtra / SimPanel / portability children"] M --> S["Premium delivery BasketItem"]
Route or switch Owner Key branch conditions Why it exists
/basket MainTemplate -> BasketPage Standard browser mode, consumer or business CMS header and footer, protected by login redirect. Normal basket experience with full page chrome.
/basket in webview WebViewTemplate -> BasketPage isWebView() removes StandardPageTemplate chrome and enables the skipBasket branch. Embedded or app-style basket surface.
/customer-transfer/basket SeamlessMigrationTemplate -> BasketPage Header links filtered by upgrade eligibility; migration-only child components appear inside package bodies. Dedicated customer transfer journey.
?skipBasket=true BasketPage Only hides the basket in webview mode and is overridden by error or invalid-basket state. Supports skip-through behavior from embedded contexts.
?validate=true BasketJourney.handleQueryChange() Triggers basketService.validateBasket() after load; with mock=true it can fall back to validateCheckout(). Allows explicit validation runs through query-driven journeys.
?reviewMode BasketContainer Non-production query branch that moves rendering into a Shadow DOM host and trims standard promo chrome. Supports embedded checkout-style rendering.
?showModalsOnly BasketContainer Short-circuits normal content and returns only ModalContainers. Lets other flows reuse basket modal surfaces without the full basket body.
Deep link basket creation useBasket() plus deepLinking.createDeeplinkBasket() planSkuId, deviceSkuId, primaryDeviceId, contractDuration, upfrontPrice, packageLinkIdentifier, or basketId. Allows local and journey-specific basket creation without starting from the standard storefront.

Render breakdown

This is the runtime ownership map, not a flat component inventory. Each accordion traces who renders whom, what drives it, and what the component relies on.

#render-breakdown

State management

MobX is the real ownership boundary for most client behavior. There is no Redux store or provider tree hiding the state flow.

#state-management
Root store model MobX

useStore() returns two singletons: basketStore and uiStore. Most components import the hook directly instead of receiving state through props or context. That keeps shared state simple, but it also means render ownership and state ownership are tightly coupled.

basketState domain state

basket.state.ts holds the transformed basket, computed route helpers such as isSeamlessMigrationJourney, the parsed queryMap, page content, validation response, flags, and convenience getters like isEmpty, hasSmartWatch, and rpiContent.

uiState ui state

ui.state.ts owns modal visibility, notifications, CTA disablement, border styling, and computed pageError. This is why top-level shells such as BasketPage and BasketJourney can change behavior without re-plumbing props through intermediate components.

Action and hydration flow bootstrap

basket.actions.ts is the real data loader. loadBasketContent() fetches /basket/flags through getFlagData(), then loads transformed content-service data through contentService.getAssetModel(). Only after that does the client load or create the basket and transform it into the model consumed by the UI.

Federation side channel integration

basketService emits basket events through eventService, and useBasketSummary() publishes a summary to window.VFUK.basketData. That gives external hosts and federated consumers a stable way to observe basket state without owning the whole MobX store.

Request lifecycle

The basket journey is effectively a two-phase bootstrap: load configuration and content first, then load or create the basket payload that drives the UI tree.

#request-lifecycle
  1. Browser bootstrap

    src/client/index.tsx calls authService.session(), bootstraps federated dependencies, and mounts the route tree only after the runtime resolves.

  2. Route and wrapper selection

    AppRoutes chooses MainTemplate, WebViewTemplate, or SeamlessMigrationTemplate, and BasketPage decides whether the journey itself should be visible.

  3. Flag and content loading

    useBasket() triggers basketStore.actions.loadBasketContent(), which loads /basket/flags and the transformed basket_page content model before normal basket rendering continues.

  4. Basket load or deep-link creation

    If query parameters are present, deepLinking.createDeeplinkBasket() can create or reshape the basket. Otherwise the client loads an existing basket using basketId from query or cookie.

  5. Transformation into UI state

    The raw basket response is passed through basketTransformer(), written into basketState, and paired with UI state such as border visibility or disabled CTA flags.

  6. Validation and edge-case branches

    BasketJourney reacts to ?validate=true, handles seamless-migration errors, can surface hard-stop modals, and may show a credit-check configurator or EECC switching modal depending on flags and payload state.

  7. Summary, analytics, and external outputs

    AnalyticsUtil fires page-level events, useBasketSummary() updates window.VFUK.basketData, and the route settles into the render tree described above.

Query-driven runtime switches

  • basketId - reuse a real basket.
  • planSkuId and deviceSkuId - create deeplink baskets.
  • mock=true - use local mock responses and dev surfaces.
  • validate=true - trigger validation after load.

Embed-oriented switches

  • skipBasket=true - webview-only skip behavior.
  • reviewMode - Shadow DOM embed branch in non-prod.
  • showModalsOnly - render only modal containers.
  • packageLinkIdentifier - link a watch package to a primary package in deep-link scenarios.

Service and API layer

The client service layer is small but important: most visible behavior ultimately traces back to these files.

#service-layer
Surface Main file What it owns Why it matters in runtime terms
makeApiCall() src/client/services/api/api.service.ts Axios-based request builder with DAL headers, query helpers, timeout handling, and normalized errors. Nearly every client-side network call flows through this utility, so it defines the request contract shape for basket and content calls.
basketService basket.service.ts Basket reads, writes, validation, voucher flows, portability, delivery, and contract options. Most package actions and global journey mutations eventually end up here.
contentService.getAssetModel() content.service.ts Loads and transforms basket_page content-service payloads. The basket body and many notifications depend on this content before they can render correctly.
fetchCMSData() fetchCMSData.ts Loads header and footer CMS assets used by route wrappers. The page chrome changes by segment and journey, even before basket data loads.
getFlagData() getFlagData.ts Fetches the serialized feature-flag payload from /basket/flags. Flags must exist before the client can decide which deep branches to render.
authService.session() src/client/index.tsx Client-side session initialization. Runs before the route tree mounts, so auth state is part of bootstrap rather than a late child effect.

API routes reference

Every client-side network call flows through makeApiCall() in api.service.ts. The individual endpoint utilities live under src/client/federated/modules/services/basket-service/utils/.

#api-routes
Core basket endpointsbasket API
MethodPathPurposeSource util
GET/basket/api/basket/v2/basket/{basketId}Fetch current basketbasket.service.ts
POST/basket/api/basket/v2/basketCreate new basketbasket.service.ts
POST/basket/api/basket/v2/basket/{basketId} X-HTTP-Method-Override: PUTUpdate basketbasket.service.ts
POST/basket/api/basket/v2/basket/{basketId}/emptyEmpty basketbasket.service.ts
PATCH/basket/api/basket/v2/basket/{basketId}Patch basket (e.g. watch linking)patchBasket.ts
Validation endpointsvalidation
MethodPathPurposeSource util
POST/basket/api/basket/v2/basket/{basketId}/validateValidate basket before checkoutvalidateBasket.ts
POST/basket/api/basket/v2/basket/{basketId}/packages/discounts/validationCheck for discount/price changes — triggers pre-checkout discount modal if changes detectedvalidateBasket.ts
Package operationspackages
MethodPathPurposeSource util
POST/basket/api/basket/v2/basket/{basketId}/packageAdd packageaddPackage.ts
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId} X-HTTP-Method-Override: PUTUpdate packageupdatePackage.ts
DELETE/basket/api/basket/v2/basket/{basketId}/package/{packageId}Remove package. Accepts ?deleteLinkedPackages=true for combi/watch removal.removePackage.ts
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId}/productAdd product (service or hardware) to packageaddProduct.ts
DELETE/basket/api/basket/v2/basket/{basketId}/package/{packageId}/productLine/{productLineId}Remove product line from packageremoveProduct.ts
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId}/contract-options X-HTTP-Method-Override: PUTUpdate contract options (tenure, upfront)basket.service.ts
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId}/BYODeviceAdd or remove BYOD productaddByodProduct.ts / removeByodProduct.ts
Bundle, portability, and vouchersmutations
MethodPathPurposeSource util
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/{bundleId} X-HTTP-Method-Override: PUTSwitch bundle/tariffswitchBundle.ts
POST/basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/portability X-HTTP-Method-Override: PUTSubmit portability (PAC/STAC) infosubmitPortabilityInfo.ts
DELETE/basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/portabilityRemove portability inforemovePortabilityInfo.ts
POST/basket/api/basket/v2/basket/{basketId}/voucher/{voucherCode} X-HTTP-Method-Override: PUTAdd voucheraddVoucher.ts
DELETE/basket/api/basket/v2/basket/{basketId}/voucher/{voucherCode}Remove voucherremoveVoucher.ts
Broadband, delivery, and non-basket endpointsother
MethodPathPurposeSource
POST/basket/api/broadband/{broadbandId}/packageAdd or update broadband packageaddBroadbandPackage.ts
GET/basket/api/basket/v2/basket/{basketId}/deliveryOptionsFetch available delivery optionsgetDeliveryOptions.ts
GET/basket/flagsFeature flags payload — fetched before basket loadsgetFlagData.ts
GET/basket/api/content-service/v2/contentCMS content for basket page (transformed by contentAPITransformer server-side)content.service.ts
GET/basket/api/content/assetCMS asset endpointcontent.service.ts
GET/basket/mockDataReturns available mock fixture scenarios (dev only)MockSelector.tsx
GET/POST/basket/auth/sessionProxied to /web-shop/login/auth/session by authRedirectMiddlewareauthRedirectMiddleware.ts
GET/basket/api/portability/authcode/**Portability auth code lookupmocking.middleware.ts

Note: The basket API does not support native PUT/DELETE from the browser. Mutations use POST with an X-HTTP-Method-Override header, and deletions use the Axios delete() method.

Basket validation flow

Validation is a two-endpoint sequence gated inside basketStore.actions.validateBasket(). It guards checkout, detects price changes, and can surface a pre-checkout discount modal before the user is redirected.

#basket-validation

What triggers validation

  • User clicks Go to CheckouthandleCheckout()validateBasket()
  • After a package removal → validateBasket(true) (removingPackages flag set)
  • After an undo operation → validateBasketAfterUndo() if packages remain
  • ?validate=true query param in BasketJourney.handleQueryChange()

Two validation calls

  • POST /validate — basic pre-checkout validation, checks basket integrity.
  • POST /packages/discounts/validation — checks for price/discount changes since the basket was built. Returns packageValidations[{ packageId, status }].

Validation response shape

  • valid: boolean
  • packageValidations: [{ packageId, status }] — status is 'ADD', 'REMOVE', or 'UPDATE'
  • openLoanCount: number | null
  • packages: null | Package[]
Step-by-step validation logicbasket.actions.ts
  1. Call validateCheckout(basketId)

    POSTs to /packages/discounts/validation. Sets isValidating: true in store while in-flight.

  2. No package validations returned + not removing packages

    Hides the discount modal (setShowPreCheckoutDiscountModal(false)) and redirects to basket.checkoutUrl or falls back to /secure-checkout.

  3. Package validations present (status ADD / REMOVE / UPDATE)

    Sets validationResponse in state, then shows the pre-checkout discount modal (setShowPreCheckoutDiscountModal(true)). User reviews changes and confirms before redirect continues.

  4. Error path

    Calls handleSeamlessMigrationErrors(error) — redirects if in the migration journey. Otherwise updates uiStore status to 'basketIsInvalid' and fires an analytics inline-component-error event.

Hard blockers checked before validation (checkBasketForProblems)pre-flight
  • Credit vet declined (creditVetId === 'D') — redirects to /en/checkout-credit-fail.html, no validation call made.
  • Too many packages — sets notification 'tooManyPackages', blocks checkout.
  • Unacceptable recurring charge — sets notification 'unacceptableRecurringCost', blocks checkout.
  • Abandoned basket — syncs basketId cookie to match server state before proceeding.

Credit check configurator flow

The hard-credit-check tray is gated in BasketJourney, classified by creditCheckScenario.ts, and then rendered through a small subtree whose controls change by scenario.

#credit-check-flow

Input sources

getCreditCheckConfiguratorData.ts combines the first qualifying handset package, its hardwareConfigurator ranges, contract-options tenure, and vetOutcome values such as recurring-charge limit, suggested tenure, and suggested upfront value.

BasketJourney gate

The tray only mounts when ISHARDCREDITCHECKENABLED is true, the helper returns actionable data, shouldShowCreditCheckConfigurator() says the scenario is not NO_CREDIT_CHECK, and the initial total monthly payment is calculable.

Exact evaluation order inside creditCheckScenario.ts branching
  1. Early exit to NO_CREDIT_CHECK

    Missing scenario data or a missing monthly-spend limit immediately suppresses the configurator. BasketJourney never renders the tray in this state.

  2. Credit risk with tenure already maxed out

    If there is a minimum-upfront rule, the selected tenure is already 36, the monthly limit is still exceeded, and the backend is no longer recommending a higher tenure, the helper classifies the case as CREDIT_RISK_WITH_TENURE_AT_MAX.

  3. Credit risk with a usable tenure increase

    If there is a minimum-upfront rule and the basket can move to the suggested tenure while the monthly limit is still exceeded, the helper returns CREDIT_RISK_WITH_TENURE.

  4. Credit risk upfront only

    If the previous two branches fail but the basket still has a credit-risk upfront rule, the helper falls back to CREDIT_RISK_UPFRONT_ONLY.

  5. Affordability only

    If there is no upfront rule but the monthly limit is exceeded and the tenure can be increased, the helper returns LESS_AFFORDABILITY.

Feature flags and config

The flag system is real runtime input, not just release plumbing. Top-level chrome and deep package children both change based on these values.

#feature-flags

Server source of truth

FEATURE_FLAGS_CONSTS plus initFeatureFlagService() define the available flag keys and their server-side defaults.

Client hydration path

basket.actions.loadFlagData() calls getFlagData(), then setFlagData() writes the final values into basketStore.state.flags.

Override model

Config can come from env, LaunchDarkly, and helper-level overrides such as getFeatureFlags() or local storage for non-prod experimentation.

Auth and session model

The basket is a protected route tree. Auth affects routing, middleware, and some journey-specific banners before it affects the visible basket rows.

#auth-session
Surface Main file Behavior Implication for docs readers
Client session bootstrap src/client/index.tsx authService.session() runs before the app renders. Session establishment is part of browser bootstrap, not a child page effect.
Protected routes loginRedirect.config.ts /basket and /customer-transfer/basket require assurance level 3 unless disabled by env. Both primary journeys assume authenticated access at the edge.
Session redirect shims authRedirectMiddleware.ts Redirects /basket/auth/session to /web-shop/login/auth/session and rewrites logout to the login service. The basket does not own login endpoints directly even though it exposes basket-scoped session URLs.
IDM integration idm.config.ts Defines cookie prefix, authority, redirect callbacks, encryption config, and allowed return URLs. Auth cookies and callback behavior are configured server-side, not in client route logic.
Logged-in customer branches BasketContainer.tsx LoginBanner appears for seamless migration customers when getState().isLoggedIn is false. Auth state affects deep child rendering as well as route protection.

Cookies and session storage

The basket uses cookies for three distinct purposes: basket identity, auth session state, and mock/flag overrides in non-production. Most reads happen at store initialisation; most writes happen as side effects of API calls.

#cookies
Cookie Purpose Set by Read by Notes
basketId Identifies the current basket across page loads basket.actions.ts, deepLinking.ts, MockSelector.tsx (sets to mock-basket-id) getBasketId.ts, basket.state.ts, useBasket.ts Deleted with setCookie('basketId', '', -1) on session expiry or empty-basket flow. Synced to URL query param if present.
customerSegment Business or Consumer customer type — drives CMS header/footer and some banner logic useBasket.ts, deepLinking.ts getIsBusiness.ts, isBusinessCustomer.ts Written when basket is loaded or created. Controls which route wrapper fetches which CMS header.
basketMock Selects the fixture scenario for GET basket responses in mock mode MockSelector.tsx Mock middleware handlers via isMockingEnabled(req) Value is a path under cypress/fixtures/basket/. e.g. handset-consumer.
basketMockValidate Selects the fixture scenario for validation POST responses — can differ from basketMock to test mixed success/failure paths MockSelector.tsx Mock validate handler Allows testing discount-change modal without changing the basket fixture.
isCypress Signals that Cypress is driving the browser — disables certain timers and animations Cypress support code AddMoreButton.tsx, WaiverNotification.tsx, EmptyBasketContent.tsx, BasketStatus helpers Used to prevent flaky test behaviour from animation delays.
eShop-auth-{env}_p_id_token Principal ID token for authenticated sessions Auth service / IDM callback MockSelector.tsx (reads and toggles for dev), idm.config.ts Environment suffix: int1, sit2, etc. — determined by getEnvironmentCookiePrefix().
Feature flag override cookies Per-flag overrides that take precedence over LaunchDarkly values MockSelector.tsx flag toggles getFlagValue.ts, isFlagEnabled.ts Checked before LD value in resolution order: cookie → query param → LD payload.
Adobe Target / A/B test cookies A/B test variant assignment for active experiments MockSelector.tsx A/B toggles getABTestFeatureValue.tsx — falls back to cookie if window.vfukTnt is absent Defined in adobeTargetTests array. Production assignment comes from Adobe Target via Tealium.
Cookie resolution order for flags and mocksoverride precedence
  1. Cookie override

    If a flag-named cookie exists (set via MockSelector or manually), its value wins unconditionally.

  2. URL query parameter

    ?flagName=true is checked next — useful for shareable test URLs without touching cookies.

  3. LaunchDarkly payload

    The serialised flag array from /basket/flags is the production source of truth.

  4. Default

    Hardcoded fallback values in FEATURE_FLAGS_CONSTS — always false unless explicitly defaulted otherwise.

Mocking and fixtures

The repo has a real local mock runtime, not just ad-hoc Cypress stubs. Browser query params, cookies, Express middleware, and fixture folders all work together to drive basket journeys locally.

#mocks

How mock mode starts

Visiting /basket?mock=true in non-production shows MockSelector. That UI fetches grouped mock options from /basket/mockData and writes cookies that the server middleware checks on every request.

Why fixtures live under Cypress

Development mock responses are read from cypress/fixtures, so the same JSON scenarios can power local browsing and Cypress intercepts. Build scripts then copy those fixtures into =build/server/fixtures for standalone runs.

Two important cookies

basketMock chooses the basket response fixture and basketMockValidate chooses the validation response fixture. They can be different so success and failure paths can be mixed intentionally.

End-to-end mock runtime server + client
  1. Server boot decides whether the middleware exists

    mockingMiddleware() only mounts when config.middleware.mockingMiddlewareEnabled is true.

  2. The browser chooses a scenario

    MockSelector requests /basket/mockData, renders grouped options, then stores the chosen values in cookies.

  3. Handlers read cookies, not query strings

    Each mock handler calls isMockingEnabled(req), which is effectively a basketMock cookie check. The chosen cookie value determines the fixture path.

  4. Specific handlers serve basket, content, validation, and mutation flows

    Basket GET, validation POST, content assets, auth session, portability, and generic basket mutations each have a dedicated handler path under the mock middleware.

Analytics and events

Analytics are centralised through Analytics.ts. Components never fire raw analytics calls — they call store actions which delegate to AnalyticsUtil.

#analytics

Initialisation

  • Called from BasketPage on mount via initialiseAnalytics(reviewMode).
  • Reads basket state, customer segment, and order details to build the page config.
  • reviewMode: true suppresses certain page-view events when embedded in checkout Shadow DOM.
  • Backed by @vfuk/lib-web-analytics with Tealium as the tag container.

Data sent on every event

  • Basket contents, packages, and pricing
  • Customer segment (business / consumer)
  • Current error code and message (pageError)
  • Feature flag states and A/B test variant assignments
  • Transaction product list with SKUs and upfront costs
Tracked trackLink eventsuser actions
Event keyTriggerKey data
emptyBasketCtaUser confirms empty basketbasket data, pageError, purchaseType
continueShoppingCtaContinue shopping modal confirmedbasket data, error
removePackageCtaPackage removal confirmedbasket data, packageId, error
removeTradeinCtaTrade-in removedbasket data, packageId, error
undoRemovePackageCtaUndo package removalbasket data, packageId, error, eventName array
undoRemoveTradeinCtaUndo trade-in removalbasket data, packageId, error
removeAddonCtaAdd-on removedaddonId, basket data, error
undoRemoveAddonCtaUndo add-on removaladdonId, basket data, error
promoCodeSuccessVoucher code applied successfullybasket data, transactionCouponCode
inlineComponentErrorAny action results in an erroreventAction (error code), eventLabel, basket data, pageError
Federation side channel: window.VFUK.basketDataexternal consumers

useBasketSummary() publishes a basket summary to window.VFUK.basketData after every basket update. External hosts and other federated modules can observe this without owning the MobX store. basketService also emits basket-change events through eventService for the same reason.

Error handling

Errors flow through uiStore as notification keys and error objects. Components read uiStore.state.notification and uiStore.state.pageError — they never catch API errors directly.

#error-handling

Error object shape

  • status: number — HTTP status code
  • data.errorCode?: string — machine-readable code
  • data.errorMessage?: string — human-readable message
  • pageError computed: "{status} {errorMessage}" — sent in analytics events

HTTP error normalisation (api.service.ts)

  • Response errors (4xx/5xx) → { status, data }
  • Network timeouts → returns raw request object
  • Setup errors → { statusCode: 500, data: message }
  • All errors are caught by action-level try/catch and written to uiStore — never bubbled to components.
Notification key referenceuiStore
KeyShown whenAuto-cleared
couldNotEmptyBasketEmpty basket API call failsOn next successful action
couldNotRemovePackagePackage removal API call fails; package status reverts to 'present'On next action
couldNotAddPackageAdd package failsOn next action
couldNotAddExtraAdd-on addition failsOn next action
couldNotUndoUndo operation fails; package status reverts to 'removed'On next action
couldNotAddVoucherVoucher application failsOn next action
couldNotRemoveVoucherVoucher removal failsOn next action
couldNotRemovePackageTradeInTrade-in removal failsOn next action
couldNotUndoRemovePackageTradeInTrade-in undo failsOn next action
basketIsInvalidValidation API returns an errorOn next action
tooManyPackagesConnection limit exceeded check in checkBasketForProblemsOn next action
unacceptableRecurringCostMonthly charge limit exceededOn next action
generalCatch-all for unclassified errorsOn next action
clearExplicitly resets notification and error stateImmediate
Seamless migration error handlingmigration journey

handleSeamlessMigrationErrors(error) is called before standard error handling in validation and mutation actions. If the current journey is a seamless migration route, it redirects to a journey-specific error URL instead of showing an inline notification. This prevents the generic error banner from appearing in a flow that has its own error pages.

Cypress and BDD test runtime

Cypress in this repo is a Cucumber-style journey runner backed by a large intercept and fixture library, not just a set of flat spec files.

#cypress

Feature-first structure

cypress/integration/features/*.feature contains Gherkin scenarios, while cypress/integration/steps/*.steps.js binds those Given/When/Then steps to the real basket route and intercept setup.

Intercept library

cypress/support/commands.ts is the real test DSL. It centralizes auth-session mocks, basket mocks, validation mocks, analytics assertions, portability flows, accessibility hooks, and Percy screenshot setup.

Shared fixture language

Cypress and the local mock middleware reuse the same fixture library. That keeps local browsing and automated tests in sync because both are speaking the same scenario names and payload shapes.

How a Cypress scenario runs execution
  1. The preprocessor turns feature files into runnable specs

    cypress.config.ts wires the Cucumber preprocessor together with the esbuild bundler and fail-fast plugin.

  2. Global support code bootstraps the browser

    support/e2e.ts loads custom commands, accessibility helpers, Percy CSS fixes, and uncaught-exception suppression before the first step runs.

  3. Steps call shared commands instead of duplicating raw intercepts

    Step files use commands like setScenarioMocks(), loadPage(), and analytics helpers so each feature focuses on business behavior instead of setup plumbing.

  4. The app still runs at the real route

    Most scenarios visit http://localhost:8000/basket and let the client runtime execute normally, with network responses controlled by intercepts or shared fixtures.

Tooling, testing, and observability

The repo ships with a full delivery stack: Vite builds, Jest, Cypress, Percy, SonarQube, Azure pipelines, and Datadog hooks.

#tooling-testing

Build and start

  • yarn start - compile federated assets and start the client runtime.
  • yarn start:client - local client path.
  • yarn build:prod - build client and server output.

Tests

  • yarn test, yarn test:cc, yarn test:watch
  • yarn cypress:open, yarn cypress:run
  • Percy visual tests and README-backed local setup

Quality and release

  • yarn lint runs type checking and ESLint quality gates.
  • yarn analyse chains coverage and SonarQube.
  • cicd/ contains PR, build, and release pipeline definitions.
Observability hooks ops

Production tracing starts in src/server/production/tracer.ts. Server logging uses the shared Node logger, and health checks are mounted through the platform health-check package during server bootstrap. In the client, analytics flow through AnalyticsUtil rather than being emitted ad hoc from random components.

File structure and aliases

The repo is broad, but most basket ownership lives in a small set of folders and path aliases.

#file-structure
Path What lives there
src/client/ Browser entry, routes, pages, templates, components, stores, hooks, services, and utilities.
src/client/federated/modules/ Basket journey, container, and service surfaces that participate in the federated runtime.
src/server/ Development and production bootstraps plus common middleware, config, helpers, and proxies.
src/shared/ Shared helpers, transformers, logging, and type surfaces used by both client and server.
cypress/ E2E config, fixtures, support code, Percy config, and Cypress-specific package dependencies.
tools/ Vite config, code generation, mocks, postcompile tasks, and maintenance scripts.
Alias Resolves to Why it matters
@components/* src/client/components/* Most visible UI ownership flows through this alias.
@federated/* src/client/federated/modules/* Journeys, containers, and basket services live here.
@store / @stores/* src/client/store/... Direct path into MobX singleton state.
@server/* src/server/* Client code occasionally references server constants or shared runtime settings.
@shared/* src/shared/* Transformers and shared types cross the client or server boundary here.
@services/* src/client/services/* Shared API and content primitives live under this alias.

Component panels

The basket body is built from a fixed set of panel components. Each panel reads from basketStore and delegates mutations back to basketStore.actions — no direct API calls from components.

#component-panels

Component hierarchy: BasketJourneyBasketContainerBasketContentsBasketPackage / BasketCombiPackagePackageItemListSimPanel · BasketHardware · BasketBundle · BasketExtra · BasketAccessory

BasketJourneyjourney orchestrator

BasketJourney.tsx — the top-level journey shell. Owns the conditional rendering tree for every major journey branch.

  • Reads basketStore.state and uiStore.state via useStore().
  • Mounts BasketCreditCheckConfigurator when ISHARDCREDITCHECKENABLED flag is on and scenario is actionable.
  • Mounts EECCModal for energy/environmental switching confirmations.
  • Handles ?validate=true query via handleQueryChange().
  • Shows PageLoading spinner while isLoading or isValidating is true.
  • Falls through to EmptyBasket when basketStore.state.isEmpty.
  • Falls through to BasketContainer for the normal journey.
BasketContainerbasket shell

BasketContainer.tsx — wraps the basket body. Handles ?reviewMode (Shadow DOM embed) and ?showModalsOnly branches.

  • In reviewMode: renders into a Shadow DOM root via react-shadow, trims promo chrome.
  • In showModalsOnly: renders only ModalContainers — used by other flows embedding basket modals.
  • Shows LoginBanner for unauthenticated seamless-migration customers (getState().isLoggedIn === false).
  • Renders: page title and banners → BasketContents → discounts, totals, RPI → BasketPageButtons.
BasketContentspackage list

Renders the list of packages. Reads basket.combiPackages and basket.singlePackages from store.

  • Combi packages → BasketCombiPackage (linked handset + watch or HBB + SMOS).
  • Single packages → BasketPackage.
  • Premium delivery item rendered separately if deliveryType === 'PREMIUM'.
  • No direct API calls — all mutations delegated to store actions.
BasketPackage / BasketCombiPackagepackage row
  • BasketPackage — single package row. Receives thisPackage prop and index. Triggers onRemovePackage(), renders PackageHeader + RenderPackageBodyPackageItemList.
  • BasketCombiPackage — wraps two or more linked packages (e.g. handset + watch). Removal shows the 'removeCombi' modal which deletes both packages atomically via ?deleteLinkedPackages=true.
  • Package header shows status: 'present' (normal), 'removing' (optimistic removal in-flight), 'removed' (undo state).
  • Undo timer starts after removal — calls onUndoRemovePackage() if user acts before timeout.
SimPanelSIM card panel

Displays SIM card details for a package's services. Source: src/client/components/ area.

  • Shows activation method (physical SIM / eSIM) and phone number if available.
  • Links to portability info — clicking opens the portability modal.
  • Portability PAC/STAC codes displayed when present in bundle.portability.
  • Triggers submitPortabilityInfo() / removePortabilityInfo() API calls via store actions.
BasketHardwaredevice panel
  • Displays device name, storage, colour, and contract term.
  • Shows upfront and monthly price. Includes RPI messaging when applicable.
  • Trade-in credit shown as a line item — removable with undo support via onRemovePackageTradeIn().
  • BYOD (Bring Your Own Device) handled separately: addByodProduct / removeByodProduct endpoints.
  • Contract configurator link appears when tenure options are available via hardwareConfigurator ranges.
BasketBundleplan / tariff panel
  • Shows plan name, data allowance, contract length, and monthly cost.
  • Bundle switch link calls switchBundle() → PUT /bundle/{bundleId}.
  • RPI pricing message rendered from basketStore.state.rpiContent when the flag is active.
  • Reads bundle.portability to determine whether portability status is shown here or in SimPanel.
BasketExtraservices / add-ons panel
  • Renders optional services: insurance, roaming add-ons, handset protection, etc.
  • Each service has an individual remove action (onRemoveAddOn()) with undo support.
  • Undo calls onUndoRemoveAddOn() → re-adds via addProduct endpoint with original discounts preserved.
  • Discount lines shown per service where applicable.
  • Multi-line insurance removal tracked in manuallyRemovedMultiLineInsuranceId state to prevent re-offering.
BasketAccessoryaccessories panel
  • Renders accessory hardware items (cases, cables, etc.) separately from the main device.
  • Individual removal supported. If all accessories removed, package-level removal is triggered.
  • Shares the same BasketHardware sub-components for price display.
BasketPageButtonsCTA bar
  • Go to Checkout — calls basketStore.actions.handleCheckout()validateBasket() → redirect.
  • Continue Shopping — disabled state controlled by uiStore.state.isContinueShoppingDisabled.
  • Empty Basket — calls handleEmptyBasket(), fires analytics, shows confirmation modal first.
  • All three buttons read disabled state from uiStore.state.disableActions during any async operation.
BasketStatusnotification bar
  • Reads uiStore.state.notification (key) and uiStore.state.pageError (formatted string).
  • Displayed at the top of the basket — scrolls into view automatically on error.
  • Fires inlineComponentError analytics event when an error is shown.
  • Dismissible via a close link which calls uiStore.actions.updateStatus('clear', null).

Component inventory

This view is broader than the render breakdown: it groups important runtime surfaces by their job, file ownership, and dependency profile.

#component-inventory

Unresolved areas

These are the places where the codebase exposes a clear integration point but the full runtime behavior is not completely inferable from the repo alone.

#gaps

Federated import maps and CDN runtime

The repo shows the bootstrap and middleware hooks for federated assets, but the full production import-map resolution path depends on external platform behavior and environment configuration.

AB test variants outside the basket code

RenderABComponents and several flag-controlled branches reference experiments whose allocation rules are not fully visible from this repo alone.

Checkout-linked flags

Some flags in the shared constants map are clearly checkout-adjacent. Their full downstream behavior is not owned by this repository even though the constants are shared here.

Production-only platform details

Datadog APM, ALB keepalive behavior, and some proxy characteristics are only partially represented in code because the rest of the behavior lives in runtime infrastructure and platform packages.