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.
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.
-
Theme initialisation
Sets
themeWS10base 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. -
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 whenwindowexists — not SSR. -
Federated runtime initialisation
initFederatedRuntime({ debugEnabled, sharedDependencyBaseUrl })sets the shared import-map base URL to/basket/federated/sharedor the CDN equivalent.debugEnabledistruewheneverTEALIUM_ENVIRONMENT !== 'prod'. -
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. -
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. -
React root render
ReactDOM.createRoot(root).render()mounts:SourceProvider(theme + i18n) →HelmetProvider→BrowserRouter→AppRoutes. In non-production,MockSelectoris also injected at root level outside the router.
Provider stack
- SourceProvider — supplies
themeWS10theme 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
/basket→MainTemplate→BasketPage(standard browser)/basket(webview) →WebViewTemplate→BasketPage/customer-transfer/basket→SeamlessMigrationTemplate→BasketPage- 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.
| 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.
Node 22.14.0, Yarn, and Bastion for full backend access. Chrome or Firefox are the intended browser
targets.
yarn in the repo root, then cd cypress && yarn for E2E support.
yarn start then open https://localhost:8000/basket. Use yarn start:client when
you need a lighter local path.
Append ?mock=true to the basket URL or set the basketMock cookie to a fixture path under
cypress/fixtures/basket.
yarn build:prod runs the client and server Vite builds and copies fixtures into the build output.
yarn test, yarn test:cc, and yarn test:watch drive the Jest and Testing Library
suite.
yarn cypress:open, yarn cypress:run, and Percy-backed visual runs live under
cypress/.
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.
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.
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.
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
-
express.json(),express.urlencoded(), andcookieParser()normalize request input. -
initFeatureFlagService()andfeatureFlaggingMiddleware()evaluate LaunchDarkly-backed flags and exposeGET /basket/flags. -
mockingMiddleware()mounts local mock routes for basket, validation, content, portability, and auth session traffic when enabled. -
cleanRouteMiddleware()andcontentAPITransformer()rewrite route and content-service requests into the platform shape. -
authRedirectMiddleware(),loginRedirect(), andidmMiddleware()protect basket routes and map login/session endpoints. dxlProxyMiddleware()forwards basket API traffic through the configured encrypted gateway.- 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.
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
cleanRouteMiddleware()strips the/digital/vXprefix and normalizes basket paths.-
contentAPITransformer()rewrites specific content-service and footer or meganav requests into the payload shape the downstream services expect. -
authRedirectMiddleware(),loginRedirect(), andidmMiddleware()can intercept auth-scoped routes before any business proxy is reached. -
Basket API requests then flow through
dxlProxyMiddleware(), while image and environment-wide requests use the dedicated image or environment proxies instead. -
Local assets,
/basket/federatedbundles, 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.
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.
State management
MobX is the real ownership boundary for most client behavior. There is no Redux store or provider tree hiding the state flow.
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.
-
Browser bootstrap
src/client/index.tsxcallsauthService.session(), bootstraps federated dependencies, and mounts the route tree only after the runtime resolves. -
Route and wrapper selection
AppRouteschoosesMainTemplate,WebViewTemplate, orSeamlessMigrationTemplate, andBasketPagedecides whether the journey itself should be visible. -
Flag and content loading
useBasket()triggersbasketStore.actions.loadBasketContent(), which loads/basket/flagsand the transformedbasket_pagecontent model before normal basket rendering continues. -
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 usingbasketIdfrom query or cookie. -
Transformation into UI state
The raw basket response is passed through
basketTransformer(), written intobasketState, and paired with UI state such as border visibility or disabled CTA flags. -
Validation and edge-case branches
BasketJourneyreacts 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. -
Summary, analytics, and external outputs
AnalyticsUtilfires page-level events,useBasketSummary()updateswindow.VFUK.basketData, and the route settles into the render tree described above.
Query-driven runtime switches
basketId- reuse a real basket.planSkuIdanddeviceSkuId- 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.
| 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/.
Core basket endpointsbasket API
| Method | Path | Purpose | Source util |
|---|---|---|---|
| GET | /basket/api/basket/v2/basket/{basketId} | Fetch current basket | basket.service.ts |
| POST | /basket/api/basket/v2/basket | Create new basket | basket.service.ts |
| POST | /basket/api/basket/v2/basket/{basketId} X-HTTP-Method-Override: PUT | Update basket | basket.service.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/empty | Empty basket | basket.service.ts |
| PATCH | /basket/api/basket/v2/basket/{basketId} | Patch basket (e.g. watch linking) | patchBasket.ts |
Validation endpointsvalidation
| Method | Path | Purpose | Source util |
|---|---|---|---|
| POST | /basket/api/basket/v2/basket/{basketId}/validate | Validate basket before checkout | validateBasket.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/packages/discounts/validation | Check for discount/price changes — triggers pre-checkout discount modal if changes detected | validateBasket.ts |
Package operationspackages
| Method | Path | Purpose | Source util |
|---|---|---|---|
| POST | /basket/api/basket/v2/basket/{basketId}/package | Add package | addPackage.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/package/{packageId} X-HTTP-Method-Override: PUT | Update package | updatePackage.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}/product | Add product (service or hardware) to package | addProduct.ts |
| DELETE | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/productLine/{productLineId} | Remove product line from package | removeProduct.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/contract-options X-HTTP-Method-Override: PUT | Update contract options (tenure, upfront) | basket.service.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/BYODevice | Add or remove BYOD product | addByodProduct.ts / removeByodProduct.ts |
Bundle, portability, and vouchersmutations
| Method | Path | Purpose | Source util |
|---|---|---|---|
| POST | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/{bundleId} X-HTTP-Method-Override: PUT | Switch bundle/tariff | switchBundle.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/portability X-HTTP-Method-Override: PUT | Submit portability (PAC/STAC) info | submitPortabilityInfo.ts |
| DELETE | /basket/api/basket/v2/basket/{basketId}/package/{packageId}/bundle/portability | Remove portability info | removePortabilityInfo.ts |
| POST | /basket/api/basket/v2/basket/{basketId}/voucher/{voucherCode} X-HTTP-Method-Override: PUT | Add voucher | addVoucher.ts |
| DELETE | /basket/api/basket/v2/basket/{basketId}/voucher/{voucherCode} | Remove voucher | removeVoucher.ts |
Broadband, delivery, and non-basket endpointsother
| Method | Path | Purpose | Source |
|---|---|---|---|
| POST | /basket/api/broadband/{broadbandId}/package | Add or update broadband package | addBroadbandPackage.ts |
| GET | /basket/api/basket/v2/basket/{basketId}/deliveryOptions | Fetch available delivery options | getDeliveryOptions.ts |
| GET | /basket/flags | Feature flags payload — fetched before basket loads | getFlagData.ts |
| GET | /basket/api/content-service/v2/content | CMS content for basket page (transformed by contentAPITransformer server-side) | content.service.ts |
| GET | /basket/api/content/asset | CMS asset endpoint | content.service.ts |
| GET | /basket/mockData | Returns available mock fixture scenarios (dev only) | MockSelector.tsx |
| GET/POST | /basket/auth/session | Proxied to /web-shop/login/auth/session by authRedirectMiddleware | authRedirectMiddleware.ts |
| GET | /basket/api/portability/authcode/** | Portability auth code lookup | mocking.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.
What triggers validation
- User clicks Go to Checkout →
handleCheckout()→validateBasket() - After a package removal →
validateBasket(true)(removingPackages flag set) - After an undo operation →
validateBasketAfterUndo()if packages remain ?validate=truequery param inBasketJourney.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. ReturnspackageValidations[{ packageId, status }].
Validation response shape
valid: booleanpackageValidations: [{ packageId, status }]— status is'ADD','REMOVE', or'UPDATE'openLoanCount: number | nullpackages: null | Package[]
Step-by-step validation logicbasket.actions.ts
-
Call
validateCheckout(basketId)POSTs to
/packages/discounts/validation. SetsisValidating: truein store while in-flight. -
No package validations returned + not removing packages
Hides the discount modal (
setShowPreCheckoutDiscountModal(false)) and redirects tobasket.checkoutUrlor falls back to/secure-checkout. -
Package validations present (status ADD / REMOVE / UPDATE)
Sets
validationResponsein state, then shows the pre-checkout discount modal (setShowPreCheckoutDiscountModal(true)). User reviews changes and confirms before redirect continues. -
Error path
Calls
handleSeamlessMigrationErrors(error)— redirects if in the migration journey. Otherwise updatesuiStorestatus 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
basketIdcookie 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.
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
-
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.
-
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 asCREDIT_RISK_WITH_TENURE_AT_MAX. -
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. -
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. -
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.
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.
| 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. |
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.
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
-
Server boot decides whether the middleware exists
mockingMiddleware()only mounts whenconfig.middleware.mockingMiddlewareEnabledis true. -
The browser chooses a scenario
MockSelectorrequests/basket/mockData, renders grouped options, then stores the chosen values in cookies. -
Handlers read cookies, not query strings
Each mock handler calls
isMockingEnabled(req), which is effectively abasketMockcookie check. The chosen cookie value determines the fixture path. -
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.
Initialisation
- Called from
BasketPageon mount viainitialiseAnalytics(reviewMode). - Reads basket state, customer segment, and order details to build the page config.
reviewMode: truesuppresses certain page-view events when embedded in checkout Shadow DOM.- Backed by
@vfuk/lib-web-analyticswith 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 key | Trigger | Key data |
|---|---|---|
emptyBasketCta | User confirms empty basket | basket data, pageError, purchaseType |
continueShoppingCta | Continue shopping modal confirmed | basket data, error |
removePackageCta | Package removal confirmed | basket data, packageId, error |
removeTradeinCta | Trade-in removed | basket data, packageId, error |
undoRemovePackageCta | Undo package removal | basket data, packageId, error, eventName array |
undoRemoveTradeinCta | Undo trade-in removal | basket data, packageId, error |
removeAddonCta | Add-on removed | addonId, basket data, error |
undoRemoveAddonCta | Undo add-on removal | addonId, basket data, error |
promoCodeSuccess | Voucher code applied successfully | basket data, transactionCouponCode |
inlineComponentError | Any action results in an error | eventAction (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 object shape
status: number— HTTP status codedata.errorCode?: string— machine-readable codedata.errorMessage?: string— human-readable messagepageErrorcomputed:"{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
| Key | Shown when | Auto-cleared |
|---|---|---|
couldNotEmptyBasket | Empty basket API call fails | On next successful action |
couldNotRemovePackage | Package removal API call fails; package status reverts to 'present' | On next action |
couldNotAddPackage | Add package fails | On next action |
couldNotAddExtra | Add-on addition fails | On next action |
couldNotUndo | Undo operation fails; package status reverts to 'removed' | On next action |
couldNotAddVoucher | Voucher application fails | On next action |
couldNotRemoveVoucher | Voucher removal fails | On next action |
couldNotRemovePackageTradeIn | Trade-in removal fails | On next action |
couldNotUndoRemovePackageTradeIn | Trade-in undo fails | On next action |
basketIsInvalid | Validation API returns an error | On next action |
tooManyPackages | Connection limit exceeded check in checkBasketForProblems | On next action |
unacceptableRecurringCost | Monthly charge limit exceeded | On next action |
general | Catch-all for unclassified errors | On next action |
clear | Explicitly resets notification and error state | Immediate |
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.
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
-
The preprocessor turns feature files into runnable specs
cypress.config.tswires the Cucumber preprocessor together with the esbuild bundler and fail-fast plugin. -
Global support code bootstraps the browser
support/e2e.tsloads custom commands, accessibility helpers, Percy CSS fixes, and uncaught-exception suppression before the first step runs. -
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. -
The app still runs at the real route
Most scenarios visit
http://localhost:8000/basketand 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.
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:watchyarn cypress:open,yarn cypress:run- Percy visual tests and README-backed local setup
Quality and release
yarn lintruns type checking and ESLint quality gates.yarn analysechains 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.
| 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 hierarchy: BasketJourney → BasketContainer → BasketContents →
BasketPackage / BasketCombiPackage → PackageItemList →
SimPanel · 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.stateanduiStore.stateviauseStore(). - Mounts
BasketCreditCheckConfiguratorwhenISHARDCREDITCHECKENABLEDflag is on and scenario is actionable. - Mounts
EECCModalfor energy/environmental switching confirmations. - Handles
?validate=truequery viahandleQueryChange(). - Shows
PageLoadingspinner whileisLoadingorisValidatingis true. - Falls through to
EmptyBasketwhenbasketStore.state.isEmpty. - Falls through to
BasketContainerfor 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 viareact-shadow, trims promo chrome. - In
showModalsOnly: renders onlyModalContainers— used by other flows embedding basket modals. - Shows
LoginBannerfor 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
thisPackageprop andindex. TriggersonRemovePackage(), rendersPackageHeader+RenderPackageBody→PackageItemList. - 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/removeByodProductendpoints. - Contract configurator link appears when tenure options are available via
hardwareConfiguratorranges.
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.rpiContentwhen the flag is active. - Reads
bundle.portabilityto 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 viaaddProductendpoint with original discounts preserved. - Discount lines shown per service where applicable.
- Multi-line insurance removal tracked in
manuallyRemovedMultiLineInsuranceIdstate 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
BasketHardwaresub-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.disableActionsduring any async operation.
BasketStatusnotification bar
- Reads
uiStore.state.notification(key) anduiStore.state.pageError(formatted string). - Displayed at the top of the basket — scrolls into view automatically on error.
- Fires
inlineComponentErroranalytics 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.
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.
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.