web-shop-data-simo
Vodafone UK · Data SIM Only (SIMO) e-commerce journey · Consumer & Business segments
This is the front-facing web application that powers Vodafone's Data-SIM-Only (SIMO — SIM Only, data variant) purchase journey. It lets customers and business users browse data plans, pick add-ons, and complete checkout for a SIM card that provides mobile data with no voice calls included. The app supports both brand-new customers (acquisition) and existing Vodafone subscribers upgrading or migrating their tariff.
Guided Tour — Start Here
If you're new to this repo, read these sections in order. Each one builds on the last.
Who Are You?
Jump straight to what you need most.
Tech Stack
TL;DR: React 17 with TypeScript on the frontend, Express.js for server-side rendering, MobX for state, Vite for bundling, Contentful for content, and a suite of Vodafone-internal (@vfuk) and Vodafone design system (@source-web) packages.
| Category | Technology | Version | Purpose |
|---|---|---|---|
| Frontend Framework | React | 17.0.2 | Component rendering, UI interactions |
| Language | TypeScript | 4.9.5 | Type safety across client and server |
| State Management | MobX + mobx-react | 6.13.7 | Observable purchase journey state |
| Build Tool | Vite | 3.2.11 | Dev server, bundling, code splitting |
| Server | Express.js | 4.21.2 | SSR, API proxying, middleware pipeline |
| CMS | Contentful | — | Marketing copy, page content, signposting |
| HTTP Client | Axios | 1.16.0 | API calls (wrapped via @vfuk middleware) |
| Routing | React Router DOM | 5.3.4 | Client-side page navigation |
| CSS | SCSS + Styled Components | 6.1.16 | Component and global styling |
| Internationalisation | i18next + react-i18next | — | String translations |
| Document Head | react-helmet | 5.2.1 | SEO meta tags, page titles |
| Analytics | Tealium (internal) | — | User journey event tracking |
| Feature Flags | Launch Darkly | — | Progressive feature rollout |
| Monitoring | Datadog RUM | — | Real-user performance monitoring |
| Unit Tests | Jest + Enzyme | 29.7.0 / 3.11.0 | Component and service unit tests |
| E2E Tests | Cypress | 10.11.0 | Full journey browser tests |
| Code Quality | ESLint + SonarQube | — | Linting and static analysis |
| Design System | @source-web | various | Vodafone UI component library (240+ components) |
| Internal Packages | @vfuk/* | various | Vodafone middleware, auth, proxy, logging |
Notable Internal Packages
| Package | What it does |
|---|---|
@vfuk/web-middleware-dxl-proxy | DXL (Digital Experience Layer) — the API gateway proxy that adds auth headers and routes backend calls |
@vfuk/web-middleware-idm | IDM (Identity Management) middleware — handles session token validation |
@vfuk/web-middleware-login-redirect | Intercepts unauthenticated requests and redirects to the login page |
@vfuk/lib-web-feature-flagging | Launch Darkly integration for feature flags |
@vfuk/lib-web-fe-logger | Frontend logging utility (wraps console + Datadog) |
@vfuk/utils-middleware-content-api-transformer | Transforms Contentful API responses into a consistent internal format |
@vfuk/lib-web-analytics | Tealium analytics event dispatcher |
Architecture Diagram
TL;DR: The browser talks to an Express server, which proxies API calls through DXL (the Vodafone API gateway) to backend microservices. Content is fetched from Contentful. All routing back to services uses AWS API Gateway.
Walking Through the Diagram
- ① Page request — A user visits
/data-only-sim. Their browser sends an HTTP GET to the Express server. - ② Render HTML — The Express server renders React to HTML server-side (SSR), returning a fully-formed page to the browser. This makes the first paint fast and SEO-friendly.
- ③ API calls — Once the browser loads the JavaScript bundle, React hydrates and the purchase journey begins making API calls as the user interacts (selecting a plan, adding extras, etc.).
- ④ Authenticated request — All API calls go through the DXL Proxy middleware, which attaches authentication headers (session token, API key) before forwarding to AWS API Gateway.
- ⑤ Purchase APIs — AWS Gateway routes to either the new SIMO-Purchase service (v2) or the legacy Data-SIMO-Purchase service (v1) depending on the journey type and feature flags.
- ⑥ Auth — The Auth Service (IDM — Identity Management) handles login, session validation, and two-factor authentication (2FA).
- ⑦ Content — The Content Service fetches marketing copy, plan descriptions, and signposting text from Contentful, transforming it into a format the frontend can render directly.
Purchase Journey — Request Flow
TL;DR: A purchase journey is a server-managed state machine. The frontend creates a journey, fetches available plans, the user selects one, extras are optionally added, and the journey concludes. State transitions are driven by HATEOAS links (see HATEOAS section) not hardcoded URLs.
Journey State Transitions
The journey progresses through these states. Each state is driven by an action call to the backend:
| Step | Action | HATEOAS Link Key |
|---|---|---|
| Identify customer type | POST actions/new-or-existing-customer | select-existing-customer / select-new-customer |
| Select a plan | POST /package | select-plan |
| Accept upsell offer | POST /package | select-plan-upsell |
| Add an extra | POST /package/extras | select-extra |
| Remove an extra | DELETE /package/extras/{id} | remove-extra |
| Keep existing package | POST actions/keep-or-replace-package | select-keep-package |
| Replace existing package | POST actions/keep-or-replace-package | select-replace-package |
| Switch customer segment | POST actions/keep-or-change-segment | sync-jrny-seg-to-bskt |
| Empty basket | GET /empty-basket | empty-basket |
Server-Side Rendering (SSR)
TL;DR: The Express server renders the initial React tree to HTML on first load. After that, the browser takes over and React updates the page dynamically without full reloads.
- Entry point:
src/server/production/server.ts— Express application with middleware pipeline - The server mounts at path prefix
/data-only-sim - A dedicated pre-render service is also referenced for crawler/SEO pre-rendering
- MobX stores are hydrated server-side with initial data, then serialised into the HTML and re-used client-side to avoid double-fetching
- Vite handles the build in both SSR mode (for the server bundle) and client mode (for the browser bundle)
Key Server Entry Points
| File | Role |
|---|---|
src/server/production/server.ts | Express server bootstrap and middleware wiring |
src/server/common/config/core.config.ts | Core server configuration (ports, paths, env vars) |
src/server/common/config/dxlProxy.config.ts | DXL proxy configuration — API gateway URL, API key, allowed endpoints |
src/server/common/config/whitelist.config.ts | Endpoint allowlist — only listed paths can be proxied to the backend |
src/server/common/config/contentAPITransformer.config.ts | Contentful transformer configuration — space names and transformation rules |
src/client/index.tsx | React client bootstrap — hydrates SSR HTML |
CMS — Contentful Integration
TL;DR: Yes — this app is heavily CMS-driven. Almost all user-visible text, images, signposting panels, and marketing content is fetched from Contentful (a headless CMS). The app never hardcodes product copy directly in React components.
Contentful Dependencies
@contentful/rich-text-html-rendererv16.6.10 — converts Contentful rich-text JSON to HTML@contentful/rich-text-typesv16.8.5 — TypeScript types for Contentful document nodes@source-web/contentful-rich-textv11.5.2 — Vodafone design-system wrapper for rendering rich text@vfuk/utils-middleware-content-api-transformerv2.0.3 — Express middleware that transforms Contentful API responses into a consistent shape
Content API Endpoint
GET /api/content-service/v2/content
?contentEntryKey=dataonlysim_consumer
&contentType=marketingContent
&spaceName=consumer
The contentEntryKey identifies the specific Contentful entry. The spaceName is either consumer or business, selecting the correct Contentful space.
Contentful Content Keys
The following keys are used to fetch content entries. Each key maps to a Contentful entry that an editor manages:
| Key | What it controls | Segment |
|---|---|---|
dataonlysim_consumer | Main consumer page content — hero, intro, plan descriptions | Consumer |
dataonlysim_business | Main business page content | Business |
contactUsFlyout | Contact Us side panel content | Both |
signposting_consumer | Signposting panels — directional banners pointing users to related products | Consumer |
signposting_consumer_secondline | Signposting for secondary line (existing customer adding a second SIM) | Consumer |
dataonlysim_essentials | "What's included" / essentials section copy | Both |
dataonlysim_charges_list_1 | Additional charges list (e.g. roaming, overage) | Both |
dataonlysim_needtoknow_list_1 | "Need to know" information panel | Both |
devicedetailsmbb_about_speed | About mobile broadband speeds section | Both |
dataonlysim_plans | Plan listing content / descriptions | Both |
dataonlysim_bestdealslist | Best deals list copy | Both |
dataonlysim_bestdeals | Best deals section heading/intro | Both |
dataonlysim_breadcrumb | Breadcrumb navigation labels | Both |
dataonlysim_mid_contract_rise | Mid-contract price rise notice (legal/regulatory) | Both |
mcpr_price_increase | MCPR (Mid-Contract Price Rise) pricing increase content | Both |
CONTENTFUL_KEY_CONFIGContentful Layer Types
Content is organised into layers. Each layer type controls a different region of the page:
| Layer | Purpose |
|---|---|
marketingContent | Main page sections — heroes, banners, plan copy |
postBodyMarketingContent | Content displayed after the main body content |
channelApp | Channel-specific configuration and overrides (e.g. eShop vs. app) |
pageSeo | Page title, meta description, Open Graph tags |
Content Store
Fetched Contentful content is stored in the MobX DataSimoContentStore. This store holds all CMS content for the current page session and makes it available to any component that needs it.
Content Key → Component Mapping
TL;DR: Every content key ends up on screen somewhere inside the DataSimoTemplate tree (or a store/helper it feeds). Some keys have a dedicated store getter; others are queried ad hoc by the component that needs them.
| Content Entry Key | Store Property / Getter | Consuming Component(s) | File Path | What it renders |
|---|---|---|---|---|
dataonlysim_consumer / dataonlysim_business | content (via consumerContent / businessContent) | Root payload — every component below reads from this | n/a | Base page content object, switched by isBusiness |
contactUsFlyout | consumerContent.contactUs / businessContent.contactUs | ContactUs | src/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:106-109 | "Get in touch" flyout panel content |
signposting_consumer + signposting_consumer_secondline | signpostingContent | SimpleNotification + RawHtmlWrapper (inline in template) | src/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:54-60 | Second-line SIM signposting banner — only when journey type is secondline and segment is consumer |
dataonlysim_mid_contract_rise | priceRiseContent | MidContractPriceRise | src/client/templates/DataSimoPlansTemplate/DataSimoPlansTemplate.tsx:56 | Mid-contract price rise disclosure above the plan list (gated by feature flag) |
mcpr_price_increase | mcprContent | PlanCardList → PlanCard | src/client/components/molecules/PlanCardList/PlanCardList.tsx:32 | MCPR (Mid-Contract Price Rise) title shown per plan card |
dataonlysim_breadcrumb_{segment} | breadcrumbContent | DataSimoBreadcrumbs | src/client/templates/DataSimoTemplate/components/DataSimBreadcrumbs/DataSimoBreadcrumbs.tsx:20,31 | JSON-LD BreadcrumbList schema + visual breadcrumb trail |
dataonlysim_essentials | PlanDetailsStore.getContentForPlan('whatsincluded') | PlanDetailsOverlayModal (via PlanCardDetailsModal) | src/client/components/molecules/PlanDetailsOverlayModal/PlanDetailsOverlayModal.tsx:20-40 | "What's included" tab in the plan details modal |
dataonlysim_charges_list_1 | PlanDetailsStore.getContentForPlan('additionalcharges') | PlanDetailsOverlayModal | src/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:62-66 | "Additional Charges" tab |
dataonlysim_needtoknow_list_1 | PlanDetailsStore.getContentForPlan('whatyouneedtoknow') | PlanDetailsOverlayModal | src/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:67-71 | "What you need to know" tab |
devicedetailsmbb_about_speed | PlanDetailsStore.getContentForPlan('aboutspeed') | PlanDetailsOverlayModal | src/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:72-76 | "About speed" tab |
dataonlysim_plans | PlanDetailsStore.getFields() (container lookup only) | — no direct render — | src/client/stores/PlanDetailsStore/PlanDetailsStore.ts:71 | Pure indirection: the entry whose nested list holds the four keys above |
dataonlysim_bestdealslist_{segment}_{journeyType} | Ad hoc lookup via contentStore.content.marketingContent | HeroBanner → IconSnippetList | src/client/components/organisms/HeroBanner/HeroBanner.tsx:58-73 | Icon/heading/text snippet list under the hero banner |
dataonlysim_bestdeals_{segment}_{journeyType}_skinnybanner | Ad hoc lookup via contentStore.content.marketingContent | HeroBanner | src/client/components/organisms/HeroBanner/HeroBanner.tsx:37-93 | Hero banner background image + heading |
marketingContent layer (slot1 tag) | getMarketingContent() | MarketingComponent | src/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:73 | Marketing slot above the fold |
postBodyMarketingContent layer (slot3 tag) | getPostBodyMarketingContent() | MarketingComponent | src/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:123 | Marketing slot at the bottom of the page |
pageSeo layer | seoData | DataSimoSeoTags → HeadTags | src/client/templates/DataSimoTemplate/components/DataSimoSeoTags/DataSimoSeoTags.tsx | <head> SEO / OpenGraph / Twitter meta tags |
.tracking (raw field, not in key config) | consumerContent.tracking / businessContent.tracking | Not a component — formatTrackingProps helper | src/client/stores/helpers/formatTrackingProps.ts:7 | Builds the Tealium analytics tracking payload |
.filterOptionsContent (raw field) | content.filterOptionsContent | generateSecureNetBenefitItem → BenefitItems | src/client/components/molecules/CardBenefitsSlot/components/BenefitItems/helpers/generateSecureNetBenefitItem/generateSecureNetBenefitItem.ts:6-10 | "Secure Net" benefit item on a plan card |
.planCardEntertainmentContent (raw field) | content.planCardEntertainmentContent | PlanCardAccordionModal | src/client/components/molecules/PlanCardAccordionModal/PlanCardAccordionModal.tsx:50-51 | Entertainment benefit modal — header + accordion |
content (root, generic) | store.contentStore.content | SubscriptionSummary → renderInclusiveProducts | src/client/components/molecules/SubscriptionSummary/SubscriptionSummary.tsx:32,72,80 | Inclusive products/benefits list in the post-purchase summary |
CONTENTFUL_KEY_CONFIG keys have a dedicated getter on DataSimoContentStore (consumerPage, businessPage, contactUsFlyout, signposting_consumer, signposting_consumer_secondline, midContractPriceRise, mcprContent, dataonlysim_breadcrumb). The remaining 7 keys (dataonlysim_essentials, dataonlysim_charges_list_1, dataonlysim_needtoknow_list_1, devicedetailsmbb_about_speed, dataonlysim_plans, dataonlysim_bestdealslist, dataonlysim_bestdeals) are queried ad hoc by PlanDetailsStore and HeroBanner, which call findContentByEntryKey directly against contentStore.content rather than going through a store getter. Keep this in mind when adding a new key — there's no single consistent pattern to follow.
src/client/stores/SimoStore/SimoStore.ts:284 calls this.contentStore.loadPlanDetailsContent(), but DataSimoContentStore has no method by that name (confirmed by a full-repo search — the only references are in SimoStore.ts and its test file). Unless this is mocked away in tests, it will throw TypeError: ... is not a function at runtime. Worth a bug ticket before relying on this code path.Backend Endpoints
TL;DR: The app calls four distinct backend service groups — Content, SIMO-Purchase (new), Data-SIMO-Purchase (legacy), and Auth/Authorization. All calls are proxied through the DXL proxy, which adds authentication headers and routes to AWS API Gateway.
src/server/common/config/whitelist.config.ts) can be proxied. Any call to an unlisted path will be blocked by the DXL proxy. If you add a new backend endpoint, you must add it to the whitelist.
contentEntryKey, contentType, spaceName{segment} path param (consumer/business)HATEOAS Navigation
TL;DR: HATEOAS (Hypermedia As The Engine Of Application State) means the backend tells the frontend what actions are available at each step. The frontend never hardcodes "what to call next" — it reads the links from the last API response and follows them.
How it works in code
Each API response from the purchase service includes a links array. Each link has a rel (relation type) that tells the frontend what that link means. The frontend extracts the link by its rel and calls it to progress the journey.
// Example API response structure
{
"journeyId": "abc-123",
"status": "PLAN_SELECTED",
"links": [
{ "rel": "get-extras", "href": "/simo-purchase/.../plans/xyz/extras", "method": "GET" },
{ "rel": "empty-basket", "href": "/simo-purchase/.../empty-basket", "method": "GET" },
{ "rel": "get-subscription-summary", "href": "/simo-purchase/.../subscription-summary", "method": "GET" }
]
}
The frontend looks up the link by rel and calls the href — it never constructs these URLs itself.
HATEOAS Link Reference
Link rel | What it means |
|---|---|
get-plans | Load the available plan listing |
select-plan | Confirm the user's plan choice |
select-plan-upsell | Accept an upsell offer on top of the selected plan |
reject-plan-upsell | Decline the upsell offer and keep the original plan |
select-existing-customer | Declare this user is an existing Vodafone customer |
select-new-customer | Declare this user is a new customer |
get-offers-and-buy-options | Load OTB (personalised offers) for existing customers |
set-journey-type | Set the journey type (acquisition, upgrade, etc.) |
get-subscription-summary | Load the order summary before checkout |
get-extras | Load available add-ons for the selected plan |
get-business-apps | Load available business app add-ons |
select-extra | Add an extra to the basket |
remove-extra | Remove an extra from the basket |
select-business-app | Add a business app to the basket |
remove-business-app | Remove a business app from the basket |
select-keep-package | Existing customer keeps their current package |
select-replace-package | Existing customer replaces their current package |
reset-journey | Reset the journey back to the beginning |
set-segment-type | Set consumer vs. business segment |
sync-jrny-seg-to-bskt | Keep journey segment (sync journey → basket direction) |
sync-bskt-seg-to-jrny | Change journey segment (sync basket → journey direction) |
empty-basket | Clear the entire basket |
switch-account | Switch to a different Vodafone account |
sign-in | Trigger the login flow |
State Management — MobX
TL;DR: All application state is managed with MobX 6. Stores hold journey state, plan selections, content data, auth state, and UI state. Components observe stores and re-render automatically when state changes.
Key Stores
| Store | What it holds | Location |
|---|---|---|
DataSimoContentStore | All Contentful CMS content for the current page — marketing copy, signposting, SEO data | src/client/stores/DataSimoContentStore/ |
| Journey Store | Current purchase journey state — selected plan, extras, journey ID, HATEOAS links | src/client/stores/ |
| Auth Store | User authentication state — logged in/out, session token, account details | src/client/stores/ |
MobX Pattern Used
The codebase uses MobX with mobx-react (the React bindings). Components are wrapped with observer() to make them reactive to store changes. Stores use makeAutoObservable or makeObservable for declaring which properties are observable.
// Typical pattern — component reacts to store changes automatically
import { observer } from 'mobx-react'
const PlanSelector = observer(({ store }) => {
// Re-renders whenever store.selectedPlan changes
return <div>{store.selectedPlan?.name}</div>
})
Store Access Pattern
Stores are typically injected via React Context or passed as props from the top-level page component. There is no global singleton store — stores are created per-request on the server side to avoid state leaking between users.
Auth & Identity
TL;DR: Authentication is handled by IDM (Identity Management) — a Vodafone internal service. The frontend uses middleware packages (@vfuk/web-middleware-idm, @vfuk/web-middleware-login-redirect) to validate sessions and redirect unauthenticated users.
Auth Flow
- User lands on the purchase page
- The
@vfuk/web-middleware-idmmiddleware checks for a valid session cookie - If no valid session:
@vfuk/web-middleware-login-redirectredirects to the Vodafone login page - After login, IDM issues a session token (stored in a cookie)
- All subsequent API calls are made with this token in the headers (added by the DXL proxy)
- For high-security actions, a 2FA (Two-Factor Authentication) challenge is triggered via
POST /auth/2fa/actions/sendOtp
Auth Endpoints Summary
/auth and /authorization. The /auth service handles session and login. The /authorization service handles token issuance for API access. Both may be called during a single user session.
Key Auth Middleware Packages
| Package | Role |
|---|---|
@vfuk/web-middleware-idm | Validates the user's session token on every request |
@vfuk/web-middleware-login-redirect | Redirects unauthenticated users to the login page |
@vfuk/utils-middlewares-auth-shop-polyfills | Auth polyfills specific to eShop flows |
@vfuk/web-middleware-dxl-proxy | Attaches auth token to all outbound API calls |
cookie-parser | Parses session cookies from incoming requests |
Routing
TL;DR: React Router 5 manages client-side navigation. The app has two base paths — /data-only-sim for consumer and /business/data-only-sim for business. Routes are defined in src/client/routes.tsx.
| Path | Segment | Purpose |
|---|---|---|
/data-only-sim | Consumer | Consumer data SIM entry page |
/business/data-only-sim | Business | Business data SIM entry page |
The server is mounted at the path prefix /data-only-sim. The channel ID used in API calls is eShop-data-simo.
Plan Filtering & Sorting
TL;DR: The data model (FilterStore) supports six filter dimensions and the API supports a sortBy parameter, but only one filter — commitment period (contract length) — actually has a UI control. Everything else is defined in the types and analytics config but has no rendered control anywhere in the app today.
Filter dimensions defined in IFilters
FilterStore.types.ts defines the full shape of filters the backend journey response can carry:
| Filter key | UI Control? | Component | Notes |
|---|---|---|---|
duration (commitment period) | Yes — active | PlanFilterWrapper (desktop, radio buttons) / PlanFilterWrapperMobile (mobile, select menu) | The only filter with a working UI. Drives filterStore.selectedCommitmentPeriodId and the commitment-period tabs on the plans template. |
dataAllowance | No UI | — none — | Type defined, and an analytics key exists (dataAllowance → 'data' in filterConfigForAnalytics.ts), but no component reads or renders it. |
entertainment | No UI | — none — | Analytics key entertainmentPromotion exists, but this is distinct from the "Entertainment benefit" shown on plan cards (that's static plan content, not a filter toggle). |
monthlyPrice | No UI | — none — | Analytics buckets exist (0-15, 16-25, 26-30, 31-40) in filterConfigForAnalytics.ts, suggesting a price-range filter was planned, but nothing renders it. |
readyWith5g | No UI | — none — | Type-only. No analytics key, no component reference found anywhere in src/client. |
salesDeals | No UI | — none — | Analytics key salesPromotion → 'sale/deals' exists in filterConfigForAnalytics.ts, but no rendered toggle. |
IFilters interface), src/client/components/molecules/PlanFilterWrapper/PlanFilterWrapper.tsxIFilters is the full contract the backend can send back on a journey, and filterConfigForAnalytics.ts maps several of these to Tealium tracking keys as if selection were possible — but PlanFilterWrapper / PlanFilterWrapperMobile only ever read filterStore.commitmentPeriodRadioList (built from filters.duration). If you want to add a working "sort by data allowance" or "5G only" toggle, the type and analytics plumbing already exist — you'd be adding the missing UI control and wiring it to filterStore.setFilters / a new action, not inventing the filter dimension from scratch.
Sorting — sortBy
TL;DR: Sorting is not a UI control either. It only works as a deep-link query parameter — if a URL like ?sortBy=monthlyPrice.asc is used to land on the page, the value is forwarded to the backend's HATEOAS link query parameters. There is no on-page sort dropdown.
// src/client/services/dataSimOnlyService/helpers/deeplinkingQueryOverride/deeplinkingQueryOverride.ts
if (paramKey === 'sortBy') {
const isDefault = ['default', 'recommended'].includes(deepLinkQueryParams[paramKey])
hateoasLink.queryParameters[paramKey] = isDefault ? '' : deepLinkQueryParams[paramKey]
}
Any sortBy value present in the page's URL query string is copied onto the outgoing HATEOAS link before it's called — default and recommended are normalised to an empty string (the backend's default ordering).
Deep-link sortBy value | Analytics label | Meaning |
|---|---|---|
recommended | — | Default/recommended order (normalised to empty) |
monthlyPrice.asc | monthly_price:low_high | Cheapest plans first |
monthlyPrice.desc | monthly_price:high_low | Most expensive plans first |
dataAllowance.asc | data:low_high | Smallest data allowance first |
dataAllowance.desc | data:high_low | Largest data allowance first |
SORT_BY_VALUESduration (commitment period) has a working UI. sortBy works today too, but only via URL query string, not a visible control. The other five filter dimensions and any on-page sort dropdown would need new frontend components wired to the existing FilterStore / generateFilterPayload plumbing — the backend contract and analytics tracking already anticipate them.
Module & Component Inventory
TL;DR: The app is split into client-side React components/pages, server-side Express middleware, shared services, and MobX stores. Most UI components come from the @source-web design system.
Client-Side Structure
| Directory | Contents |
|---|---|
src/client/pages/DataSimo/ | Top-level page components (consumer and business variants) |
src/client/templates/HeaderFooterTemplate/ | Page shell template — wraps pages with header and footer |
src/client/components/ | Shared React components used across pages |
src/client/stores/ | MobX stores — all application state |
src/client/services/ | API service functions — HTTP calls to backend |
src/client/helpers/ | Pure utility functions and request helpers |
src/client/constants/constants.ts | All app-wide constants — content keys, HATEOAS links, route paths |
src/client/routes.tsx | React Router route configuration |
src/client/index.tsx | Browser entry point — React hydration |
Server-Side Structure
| Directory | Contents |
|---|---|
src/server/production/server.ts | Express server entry point and middleware pipeline |
src/server/common/config/ | All server configuration — proxy, whitelist, content transformer, core |
src/server/common/config/whitelist.config.ts | Endpoint allowlist for the DXL proxy |
src/server/common/config/dxlProxy.config.ts | DXL proxy settings — AWS gateway URL, API key |
src/server/common/config/contentAPITransformer.config.ts | Contentful transformer config |
Key Services
| File | Purpose |
|---|---|
src/client/services/dataSimOnlyService/dataSimOnlyService.ts | Main purchase API service — all journey, plan, and package calls |
src/client/services/contentServiceV2/getAssetModelV2.ts | Contentful content fetching service |
src/client/helpers/makeAPIRequest/makeAPIRequest.ts | Low-level API request helper — wraps Axios with consistent error handling |
Environment Config Files
Environment-specific configuration is in the .env/ directory:
.env/common.env— shared settings across all environments.env/local-prod.env— local dev pointing at production backends.env/int1.env— integration environment 1.env/qc1.env— QC (Quality Control) environment 1
Key environment variables:
| Variable | Purpose |
|---|---|
AWS_GATEWAY_DAL_URL | AWS API Gateway base URL (e.g. https://m3zqs725ki.execute-api.eu-west-1.amazonaws.com) |
DAL_URL | DAL (Data Abstraction Layer) direct URL for non-gateway calls |
AWS_GATEWAY_DAL_API_KEY | API key sent with every call to AWS Gateway |
Getting Started
TL;DR: Clone, install with npm, set up your .env file, and run npm run dev. You'll need Node.js and access to the Vodafone internal npm registry for @vfuk and @source-web packages.
@vfuk/* and @source-web/* packages are hosted on Vodafone's internal npm registry (Artifactory). You must be on the Vodafone network or VPN and have registry credentials configured before running npm install.
Prerequisites
- Node.js (check
.nvmrcorpackage.json enginesfor the required version) - npm (comes with Node)
- VPN / internal network access for Vodafone npm registry
- Access to an environment's backend services (or a local mock)
Install & Run
# Install dependencies (requires VPN + Vodafone registry credentials)
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Start production server (after build)
npm start
Testing
# Run unit tests (Jest + Enzyme)
npm test
# Run tests in watch mode
npm run test:watch
# Run E2E tests (Cypress)
npm run cypress:open # Interactive browser mode
npm run cypress:run # Headless CI mode
Linting & Type Checking
# ESLint
npm run lint
# TypeScript type check (no emit)
npm run type-check
# Stylelint (CSS/SCSS)
npm run stylelint
Local Environment Setup
Copy the appropriate .env/ file for your target environment. For local development against a real environment, use .env/local-prod.env or .env/int1.env and ensure your AWS_GATEWAY_DAL_URL and AWS_GATEWAY_DAL_API_KEY are set.
Feature Flags
TL;DR: Feature flags are managed via Launch Darkly, accessed through the @vfuk/lib-web-feature-flagging package. Flags gate features behind toggles that can be turned on or off per environment without a code deploy.
Feature flags are particularly relevant to controlling which purchase API version is used — the v1 legacy service vs. v2 SIMO-Purchase service. Before changing any flow, check whether it's gated behind a feature flag.
Observability & Logging
TL;DR: Logs go to Datadog via @vfuk/lib-web-fe-logger. Real-user monitoring (RUM) runs via Datadog RUM. Analytics events go to Tealium.
| Tool | Purpose | Package |
|---|---|---|
| Datadog RUM | Browser performance monitoring — page load times, JS errors, user sessions | Datadog SDK |
| @vfuk/lib-web-fe-logger | Structured logging — errors and events are sent to Datadog log management | @vfuk/lib-web-fe-logger |
| Tealium | User journey analytics — button clicks, page views, purchase funnel events | @vfuk/lib-web-analytics |
| SonarQube | Static code analysis — code quality and security issue reporting | CI/CD integration |
Health Check Endpoints
Both backend services expose health endpoints. These are used by load balancers and monitoring systems to check if the service is alive:
GET /simo-purchase/actuator/health— basic health checkGET /simo-purchase/actuator/vhealth— extended health including dependency statusGET /data-simo-purchase/actuator/health— basic health check (legacy service)GET /data-simo-purchase/actuator/vhealth— extended health (legacy service)
Testing
TL;DR: Unit tests use Jest + Enzyme. End-to-end tests use Cypress 10. SonarQube provides static analysis in CI/CD.
| Type | Tool | Version | Location |
|---|---|---|---|
| Unit / Component | Jest + Enzyme | 29.7.0 / 3.11.0 | Co-located with source files (*.test.tsx, *.spec.ts) |
| End-to-End | Cypress | 10.11.0 | cypress/ directory |
| Static Analysis | SonarQube | — | Runs in CI pipeline |
| Type Checking | TypeScript compiler | 4.9.5 | npm run type-check |
enzyme-adapter-react-17. If you encounter test failures with hooks or functional components, this incompatibility may be the cause. React Testing Library would be the modern replacement.
Gotchas & Traps
Non-obvious behaviours and known limitations. Read before shipping.
links in each response. Do not construct journey action URLs manually. If you hardcode a path like /journeys/{id}/actions/new-or-existing-customer, it will work in testing but silently break when the backend changes its URL structure. Always extract the URL from the relevant rel link in the last API response.src/server/common/config/whitelist.config.ts. If you add a new backend endpoint and forget to add it to this allowlist, the proxy will silently block the call with no useful error message to the client. Always check the whitelist when a new API integration is not working.data-simo-purchase (v1) service and the newer simo-purchase (v2) service. Feature flags in Launch Darkly determine which is active per environment. A bug fix in one version may not apply to the other. Before debugging a journey issue, confirm which API version is active in the target environment.dataonlysim_consumer. If a key doesn't match exactly what's in Contentful, the API returns an empty result with no error — the page section just renders blank. There's no compile-time validation of content keys. When adding or renaming a Contentful entry, verify the key matches exactly with the constants in src/client/constants/constants.ts.enzyme-adapter-react-17, which is a community adapter not officially supported by the Enzyme team (who halted React 17/18 support). Tests involving hooks, useEffect, or useState can behave unexpectedly. If a unit test fails for no obvious reason, try converting it to use React Testing Library (@testing-library/react) which has full React 17 support.data-simo-purchase API includes {segment} (consumer/business) as a URL path parameter: /data-simo-purchase/{sessionId}/{segment}/paym/journeys. The new v2 simo-purchase API does not — segment is determined from the journey context instead. If you're building a service layer that supports both, handle this difference explicitly rather than sharing URL-construction logic.