A cross-platform (iOS / Android / web) point-of-sale app for electronics-retail staff to ring up sales, manage stock, and process returns, with role-based access spanning cashier through super-admin.
X-POS is the front-end of a point-of-sale system for electronics-retail staff. It runs on iOS, Android, and the web from one Expo/React Native codebase, with file-based routing, an 8-slice Redux Toolkit store, a typed service layer with JWT auth interceptors, and a swappable mock backend that lets the UI be built and demoed before the API exists.
Role & Context
Sole front-end author. Built the entire client: file-based navigation, an 8-slice Redux state layer, a typed API/service layer with auth interceptors, RBAC, and a swappable mock backend for backend-independent development. The backend is wired-for but not in this repo (EXPO_PUBLIC_API_URL is a placeholder; the app runs against an in-process mock adapter).
Problem
Electronics-retail checkout needs cashiers to ring up multi-item sales with line-level discounts, deduct stock atomically, and handle returns, while owners restrict who can discount, refund, or manage staff. The team also needed to build and demo the full app before the backend existed.
Approach & Architecture
Loading diagram…
Screens gate on RBAC and dispatch thunks through a typed service layer; one axios instance attaches the JWT and runs single-flight refresh; a mock adapter stands in for the pending backend.
Best Practices Followed
Layered separation of concerns: atomic-design components, per-domain service modules, and Redux slices — screens never call axios directly
RBAC from a single source of truth: a 5-role × 14-permission matrix with role normalization, reused by a can() gate across UI and route guards
Resilient auth: a response interceptor refreshes the JWT with a single-flight flag and a failed-request queue, replaying parked 401s after one refresh
Circular-dependency-safe wiring: injectStore() plus runtime dynamic import() of thunks inside the interceptor to break the api → store → slices → services → api cycle
Disciplined persistence: redux-persist whitelist limited to auth tokens + theme, so stale server data and PII are not persisted
API/domain decoupling with strict TypeScript and explicit DTO → domain mappers with defensive type coercion
Challenges & Resolution
ChallengeCircular import deadlock — the axios layer needs the store for the token, but the store imports slices that import the services that import axios.
FixLazy injectStore(store) called once at bootstrap, plus runtime dynamic imports of the refreshToken/logoutUser thunks inside the interceptor instead of at module load.
ChallengeParallel requests all 401-ing at once would each trigger a separate token refresh (a refresh stampede).
FixA single-flight refresh guarded by an isRefreshing flag; other failed requests park in a queue, resolve with the new token, and retry automatically.