Software Design Description (SDD)
for EV Power Mobile Application
Architectural Blueprints, Component Hierarchies, State Management, and Low-Level Coding Design for the EV Power Mobile Client across Vietnam
1. Introduction & Design Overview
1.1 Purpose & Objectives
This Software Design Description (SDD) establishes the definitive, production-grade architectural and low-level
coding specification for the EV Power Mobile Application (repository evp-app,
application identifier com.evpower.chargingapp). While the companion Software Requirements
Specification (SRS) (docs/SRS.html, IEEE Std 830-1998) details what
the application does from a functional and business perspective, this SDD defines how the software is
architected, decomposed, structured, and implemented in code adhering to IEEE Std 1016-2009.
The objectives of this design document are:
- To formalize the Clean Layered Architecture governing the separation between Presentation, State Management, Domain Hooks, API Transport, and Native Persistence.
- To establish the System Context Viewpoint and external actor boundaries spanning EV Drivers, CPMS Cloud Microservices, Physical EVSE Hardware, VNPay Payment Gateway, and Push Notification Providers.
- To document the Zustand client state (6 modular stores) and TanStack React Query server caching lifecycles, including cache invalidation, background reconciliation, and multi-session keying.
- To specify the Dual-Instance Axios networking pipeline, dynamic microservice path routing,
and the thread-safe 401 Refresh Token Mutex Queue with
rotatedByOtherFlowrace recovery. - To provide low-level coding blueprints for the 5-step charging state machine, including real-time
reanimated car silhouette liquid wave fill (
buildWavePath), hardware stabilization delays, explicit driver CTA confirmation, and the AC charger battery SoC suppression fallback (BR-CHG-06). - To serve as the canonical architectural guide for mobile engineers maintaining, extending, or refactoring the EV Power codebase on Expo SDK 54 and React Native 0.81 New Architecture.
1.2 System Context Viewpoint & External Actor Boundaries
In compliance with IEEE Std 1016-2009 (Context Viewpoint), the EV Power Mobile Application operates as a distributed edge client interacting with multiple external actors and enterprise systems:
| External Actor / System | Interface Protocol | Interaction Boundaries & Data Exchanged |
|---|---|---|
| EV Driver (End User) | React Native UI • Native Gestures | Touch inputs, camera QR scanning, biometrics, live telemetry observation, and swipe-to-stop session control. |
| CPMS Cloud Microservices | HTTPS / REST / JSON • JWT Bearer | Dynamic routing across auth, stations, charging, wallet, invoices, vehicles, notifications. |
| Physical EVSE Fleet (Chargers) | OCPP 1.6J / 2.0.1 (via CPMS) • QR Decal • RFID | Physical plug sensing, contactor relay energization, hardware connector status (Available, Preparing, Charging, Finishing). |
| VNPay Payment Gateway | Embedded <TopupPaymentWebView /> • IPN Webhook |
Driver initiates wallet top-up; app loads secure VNPay checkout URL; backend receives IPN webhook and triggers push alert. |
| Push Notification Providers | Expo Push Service • APNs • FCM | Transmits background push payloads (ORDER_CREATED, CHARGING_STOPPED, TOPUP_SUCCESS) for reactive UI state synchronization. |
| Mobile OS Native Services | Expo Native Modules • iOS / Android SDK | Hardware Keychain / KeyStore (SecureStore AES-256), NetInfo cellular/wifi observer, FileSystem binary PDF caching, and Sharing sheet. |
1.3 Scope of Software Design
The scope of this document encompasses all software modules residing within the evp-app repository:
| Subsystem / Scope Area | Target Directory | Architectural Responsibility |
|---|---|---|
| Routing & Screens | app/, screens/ |
File-based navigation (Expo Router 54), nested tabs, modal sheets, guest onboarding gatekeeper, and route parameters. |
| UI Component System | components/ |
Atomic design presentation components (CarProgressContainer, BatteryStatusCard, CostSummaryCard) styled with NativeWind v4. |
| Client State Management | stores/ |
6 Zustand stores handling auth tokens, multi-session charging, unread counts, profile balances, station filters, and session expired modal. |
| Server State & Caching | queries/, libs/query/ |
TanStack React Query v5 hooks, automatic background refetching, infinite pagination, and domain query key contracts. |
| Domain Logic & Hooks | hooks/, utils/ |
Custom hooks encapsulating state machines, Reanimated 3 UI worklets (~15 FPS), NetInfo observers, and 1 EVP = 1,000 VND math. |
| API & Interceptor Pipeline | api/ |
Dual-instance Axios clients, service routing, JWT Bearer attachment, 401 refresh mutex with rotatedByOtherFlow, and localized error translation. |
| Secure & Local Persistence | storage/ |
Hardware-backed Expo SecureStore (AES-256) for auth secrets and AsyncStorage for preferences and 24h brute-force lockout hashes. |
| Global Contexts | contexts/ |
React Context providers for Theme switching (Dark/Light), i18n localization (VI/EN/CH), and Network reachability (NetInfo). |
1.4 Definitions, Acronyms & Conventions
| Term / Acronym | Full Form | Design Definition in evp-app |
|---|---|---|
| CPMS | Charge Point Management System | Authoritative cloud backend orchestrating EV chargers, transactions, tariffs, and driver billing. |
| EVSE | Electric Vehicle Supply Equipment | The physical charging station pedestal containing one or more power connectors. |
| EVP | EV Power Point Currency | Platform digital billing unit strictly pegged to Vietnamese Dong at 1 EVP = 1,000 VND. |
| SoC | State of Charge | Vehicle traction battery charge level expressed as an integer percentage (0–100%). |
| isAcNoSoc | AC Charger SoC Suppression Flag | Safety invariant hiding battery percentage and rendering a blurred car silhouette when charging via AC posts lacking telemetry. |
| Mutex | Mutual Exclusion Queue | Concurrency control pattern in api/interceptors.ts serializing parallel 401 refresh requests into a single network call. |
| rotatedByOtherFlow | Token Concurrency Recovery Flag | Interceptor branch recovering from 400/401 refresh errors if another parallel asynchronous flow has already successfully rotated tokens. |
| RTM | Requirements Traceability Matrix | Bidirectional mapping linking SRS requirements to concrete code implementation artifacts. |
1.5 Standards Compliance & References
- IEEE Std 1016-2009: IEEE Standard for Information Technology — Systems Design — Software Design Descriptions.
- IEEE Std 830-1998: IEEE Recommended Practice for Software Requirements Specifications (docs/SRS.html).
- OCPP 1.6J / 2.0.1: Open Charge Point Protocol JSON specification over WebSockets (CPMS to EVSE).
- RFC 7519: JSON Web Token (JWT) architecture for stateless, cryptographically signed API authorization.
- React Native New Architecture: Fabric Native Renderer and TurboModules on React Native 0.81 and Expo SDK 54.
1.6 Requirements Traceability Matrix (RTM)
In accordance with IEEE Std 1016-2009 Section 5, this matrix maps all 24 Functional Requirements (FRs)
and 11 Non-Functional Requirements (NFRs) defined in the companion SRS (docs/SRS.html) to concrete
code symbols, screens, stores, hooks, and API clients within the evp-app repository:
1.6.1 Functional Requirements Traceability Matrix
| SRS Requirement ID | Feature Name | Primary Screen / Route | Zustand Store / React Query | API Service Client |
|---|---|---|---|---|
FR-AUTH-01..03 |
Phone OTP, Password Setup & Google Sign-In | app/(auth)/* • screens/auth/* |
useAuthStore (stores/auth.store.ts) |
AuthServices (api/services/auth.ts) |
FR-AUTH-04 |
Driver Profile, Avatar & Debt Tracking | app/(tabs)/profile.tsx • screens/profile/ProfileScreen.tsx |
useProfileStore, useAuthStore |
UserServices (api/services/user.ts) |
FR-STN-01..03 |
Station Map, Cluster Markers & Filter | app/(tabs)/stations.tsx • screens/stations/* |
useStationQuery, useAllStationQuery, useChargingStationStore |
StationServices (api/services/station.ts) |
FR-CHG-01..05 |
5-Step Charging Session Machine | app/charging/connect.tsx • screens/charging/ChargingSessionScreen.tsx |
useChargingStore, useChargingSession, useChargingStep |
ChargingServices (api/services/charging.ts) |
FR-CHG-06 |
AC Battery SoC Suppression (isAcNoSoc) | screens/charging/steps/ChargingStep.tsx |
useChargingStep (derived isAcNoSoc guard) |
ChargingServices.getChargingSessionDetail() |
FR-VEH-01..02 |
EV Garage & Spending Limits | app/vehicle/index.tsx • screens/vehicle/* |
useVehiclesQuery, useVehicleDetailQuery |
VehicleServices (api/services/vehicle.ts) |
FR-CRD-01..03 |
RFID Charge Cards & Scratch PIN | app/charging-card.tsx • screens/profile/MyVouchersScreen.tsx |
useChargeCardList (useMyChargeCardsQuery) |
WalletServices (api/services/wallet.ts) |
FR-WAL-01..04 |
EVP Wallet, VNPay Top-Up & Ledger | app/top-up/index.tsx • screens/wallet/* |
useTopupStatusQuery, useTopupHistoryQuery, useProfileStore |
WalletServices (api/services/wallet.ts) |
FR-INV-01..03 |
VAT Tax Profiles & PDF Invoices | app/invoice/index.tsx • screens/invoice/* |
useInvoiceListQuery, useUserInvoiceProfileQuery |
InvoiceServices • InvoiceProfileServices |
FR-NOT-01..02 |
Push Notifications & REST Feed | app/notifications/index.tsx • screens/notifications/* |
useNotificationStore (stores/notification.store.ts) |
NotificationServices (api/services/notification.ts) |
1.6.2 Non-Functional Requirements Traceability Matrix
| SRS NFR ID | Quality Attribute & Target | Architectural Mechanism | Governing Code Artifacts | SDD Section |
|---|---|---|---|---|
NFR-PRF-01 |
60 FPS Animation & Zero Dropped Frames | Reanimated 3 UI Worklet throttling (65ms / ~15 FPS); CarProgress SVG clipping | hooks/useAnimatedNumber.ts, CarProgressContainer.tsx |
Section 3.5 |
NFR-PRF-02 |
Telemetry Polling Interval ≤10,000 ms | Dynamic interval polling hook with exponential backoff on errors | hooks/charging/useChargingStep.ts, useChargingSession.ts |
Section 6.1 |
NFR-PRF-03 |
Startup Splash ≥2,500 ms Zero Layout Shift | SplashScreen.preventAutoHideAsync() with Space Grotesk pre-loading |
app/_layout.tsx (MIN_SPLASH_DURATION_MS) |
Section 3.2 |
NFR-SAF-01 |
Hardware Relay Interlock on Start | Explicit driver CTA requirement; connector must reach IN_USE + ≥1,800ms stabilization |
ConnectingStep.tsx, useChargingStep.ts |
Section 6.1 |
NFR-SAF-02 |
Emergency Disconnect & Stop Slider | Swipe-to-stop gesture initiating two-phase settlement with <StoppingChargeOverlay /> |
SwipeStopSlider.tsx, StoppingChargeOverlay.tsx |
Section 6.1 |
NFR-SEC-01 |
Hardware-Backed Token Encryption | Expo SecureStore utilizing iOS Keychain and Android Keystore with AES-256-GCM | storage/secure-storage.ts, storage/token-storage.ts |
Section 7.1 / 7.2 |
NFR-SEC-02 |
Bearer Token & PII Redaction in Logs | maskToken and sanitizeAuthPayload utility funnels in error handlers |
api/errors.ts (getApiErrorLog) |
Section 5.5 |
NFR-SEC-03 |
24-Hour Anti-Brute-Force Lockout | 64-bit FNV/Murmur phone number hashing with daily calendar reset guard | storage/app-storage.ts (change_password_lock_${hash}) |
Section 7.2 |
NFR-SEC-04 |
Immediate Session Teardown on Revocation | Synchronous purging of all 6 Zustand store slices and queryClient.clear() |
stores/auth.store.ts (clearSessionState()) |
Section 4.4 / Fig 4.1 |
NFR-QLT-01 |
Zero Broken Tables on A4 Print / PDF | Embedded @media print stylesheet with orphan/widow suppression and page breaks |
docs/SDD.html, docs/SRS.html |
Section 1.4 |
NFR-QLT-02 |
Multi-Language Localization (VI/EN/CH) | Synchronous i18n dictionary lookup with fallback and EXCLUDED_CODES 401 bypass |
contexts/I18nContext.tsx, api/errors.ts |
Section 5.5 |
NFR-QLT-03 |
Offline & Weak Network Detection | NetInfo cellular/wifi observer driving full-screen centered blocking/non-blocking modal | contexts/NetworkContext.tsx, NetworkStatusOverlay.tsx |
Section 5.4 / 8.3 |
2. System Architecture & Design Principles
2.1 Clean Layered Architecture Overview
The EV Power Mobile Application enforces a strict 4-layer Clean Layered Architecture. Dependencies flow strictly downward from Presentation to Infrastructure; higher layers consume domain facades without knowing underlying network or persistence details:
| Layer | Module Responsibilities | Allowed Downward Dependencies |
|---|---|---|
| 1. Presentation Layer | Expo Router 54 file-based routing, screen controllers, atomic UI components, gestures, and NativeWind styling. | Layer 2 (Hooks & Facades), Layer 3 (Zustand Stores & React Query hooks) |
| 2. Domain Hooks & Facades | Custom hooks encapsulating state machines, Reanimated 3 UI worklets, app lifecycle observers, and context facades. | Layer 3 (Zustand & Query Cache), Layer 4 (API Clients & Storage) |
| 3. Client State & Server Cache | Zustand client stores (ephemeral state) and TanStack React Query v5 cache (remote server state & invalidation). | Layer 4 (API Transport & Storage Engines) |
| 4. API Transport & Storage | Dual Axios instances, 401 refresh token mutex queue, SecureStore AES-256 encryption, and AsyncStorage preferences. | Native Mobile OS Platforms (iOS Keychain, Android Keystore, Network Stack) |
2.2 Core Architectural Design Patterns
- Dual-Instance Axios Pattern: Separates public unauthenticated calls (e.g. login, token refresh) from protected Bearer-authenticated calls to prevent circular dependency loops during 401 token refresh.
- Mutex Refresh Queue Pattern with Concurrency Recovery: Suspends concurrent failing requests during token rotation, resolving them upon token exchange, with automatic fallback recovery via
rotatedByOtherFlowif another asynchronous flow already rotated tokens. - Multi-Session Keying Pattern: Indexes active charging sessions by physical coordinates (
chargeBoxCode_connectorNo) in Zustand memory, ensuring concurrent or multi-vehicle fleet charging sessions never overwrite each other. - SoC Suppression Guard Pattern (isAcNoSoc): Safety pattern hiding battery percentage and rendering a blurred car silhouette when charging at AC posts lacking battery telemetry, preventing driver panic from false 0% readouts.
- UI Thread Reanimated Worklets: Bypasses the JavaScript bridge by executing high-frequency telemetry animations on the native UI thread via Reanimated 3 SharedValues, throttled to 65ms (~15 FPS).
3. Component & UI Architecture
3.1 Expo Router File-Based Routing Topology
The application leverages Expo Router v4 (Expo SDK 54), providing type-safe, URL-driven routing modeled after file-system conventions. The routing tree is structured into clear navigation hierarchies:
- Root & Onboarding Gatekeeper (
app/index.tsx): Determines whether the device has completed onboarding, evaluates authentication status viauseAuthStore, and routes either to/(tabs)/home(authenticated),/(auth)/login, or guest screens. - Authentication Stack (
app/(auth)/): Public authentication stack containinglogin.tsx,register.tsx,forgot-password.tsx, andreset-password.tsx. Note that OTP verification is handled inline as an active step withinForgotPasswordScreen.tsxandRegisterScreen.tsxrather than as an isolated standalone route. - Authenticated Driver Tabs (
app/(tabs)/): Authenticated main driver experience featuring a custom persistent floating bottom bar:(tabs)/home.tsx→ Driver dashboard, active charging banner, quick actions.(tabs)/orders/→ Nested directory tree with_layout.tsxcontroller, historical charging sessions, and receipt sub-routes.(tabs)/qr-scan.tsx→ Raised camera QR scanner with physical connector manual entry modal.(tabs)/stations.tsx→ Interactive Leaflet/Map view with cluster markers, search drawer, and connector filters.(tabs)/profile/→ Nested directory tree with_layout.tsxcontroller, managing wallet balance, RFID charge cards, vehicles, invoices, and help center.
- Active Charging State Machine Route (
app/charging/connect.tsx): Mountsscreens/charging/ChargingSessionScreen.tsx, which hosts the core 5-step charging machine (PlugInStep,ConnectingStep,ChargingStep,SuccessScreen,FailedScreen). Note thatapp/charging-detail.tsxre-exportsOrderDetailScreen(order receipt), not the active state machine. - Guest Exploration Subsystem (
screens/guests/): Provides unauthenticated discovery across 6 dedicated screens:GuestHomeScreen,GuestStationScreen,GuestSearchScreen,GuestStationDetailScreen,GuestScanGateScreen, andGuestProfileScreen.
3.2 Root Provider Tree Lifecycle (app/_layout.tsx)
The root layout component (app/_layout.tsx) manages the startup bootstrap lifecycle, font asset loading,
native splash dismissal, and strict context provider nesting:
SplashScreen.preventAutoHideAsync(). A minimum splash duration of 2,500 ms
(MIN_SPLASH_DURATION_MS) is enforced in combination with useFonts pre-loading (Space Grotesk 5 weights)
to ensure zero visual layout shifts or font flashes before the main UI renders.
3.3 Custom Floating Curved SVG Tab Bar Architecture
The bottom navigation bar (app/(tabs)/_layout.tsx) features a proprietary floating SVG notch architecture
with a center-cutout dome accommodating the raised QR Scanner button:
- SVG Path Math: Uses exact Quadratic Bézier (
Q) and Elliptical Arc (A33,33) curves to sculpt the center dome notch dipping downwards to wrap around the 60px circular QR scan button. Zero Cubic Bézier (C) commands are utilized;NOTCH_DEPTH = 22is declared in constants but unused in the geometric path math. - Top Rim Active Indicator: A 4px absolute bar (
Animated.View style={{ height: 4, position: 'absolute', top: 0 }}) glides along the top edge of the tab bar usingAnimated.spring(indicatorPosition, { tension: 65, friction: 10, useNativeDriver: true }). - QR Tab Transparency: When the driver taps the center QR button (tab index 2), the indicator's opacity
smoothly fades to 0 via
Animated.timing(indicatorOpacity, { toValue: 0, duration: 200 }), preventing visual artifacts beneath the elevated camera button.
3.4 Design System, Tokens & Styling Architecture
The design system is constructed on NativeWind v4 (Tailwind CSS preset). Styling tokens are dynamically
harmonized across Dark and Light modes through ThemeContext.tsx:
| Design Token | Light Theme (#F8FAFC bg) |
Dark Theme (#0B0F15 bg) |
Usage & Semantic Role |
|---|---|---|---|
colors.primary |
#088178 (Deep Teal) |
#14B8A6 (Bright Cyan-Teal) |
Primary brand actions, active state highlights, submit buttons. |
colors.card |
#FFFFFF (Pure White) |
#161D26 (Rich Obsidian) |
Card backgrounds, modal surfaces, floating tab bar body. |
colors.cardBorder |
#E2E8F0 (Slate 200) |
rgba(255, 255, 255, 0.08) |
Subtle borders on cards, input fields, and tab bar edges. |
colors.text |
#0F172A (Slate 900) |
#F1F5F9 (Slate 100) |
Primary headings, key data readouts, active labels. |
colors.textMuted |
#94A3B8 (Slate 400) |
#64748B (Slate 500) |
Timestamps, secondary telemetry labels, inactive tab icons. |
colors.warning |
#C9992E (Amber Gold) |
#FACC15 (Amber 400) |
AC Charger SoC unsupported alert, low wallet balance warning. |
3.5 Micro-Interactions & High-Performance Animations
To maintain a fluid 60 FPS experience on mobile devices during intensive live telemetry polling:
- CarProgressContainer Liquid Wave Silhouette: Rather than a generic circular progress ring,
CarProgressContainer.tsxrenders an SVG side-profile car silhouette with an animated liquid wave clipping fill (buildWavePath). Reanimated shared values drive sinusoidal horizontal translation and vertical fill height matchingbatteryPct. - Throttled Reanimated UI Worklet: The
useAnimatedNumberhook runs a Reanimated 3 UI worklet throttled to 65ms (~15 FPS) viauseAnimatedReaction, interpolating numeric metrics (kwh,amountin EVP,batteryPct) over 1,200 ms with cubic easing, completely offloading the JS thread. - Background Battery Conservation: The
useAppActive()hook detects when the mobile OS transitions the app to the background (or another tab). When inactive, UI animations are immediately paused to conserve CPU cycles and battery.
4. State Management & Data Fetching Architecture
4.1 Client-Side State: Zustand Store Architecture
The application uses Zustand 4.5 for client-side state management. State is divided across 6 dedicated, loosely-coupled domain stores:
| Zustand Store | Source File | Primary State & Responsibilities | Persistence Mechanism |
|---|---|---|---|
useAuthStore |
stores/auth.store.ts |
accessToken, user profile, loading, initialized, auth bootstrap, login/register mutations. |
SecureStore (Tokens) + Memory |
useChargingStore |
stores/charging-store.ts |
sessions: Record<string, ChargingSession>, active session keys, duration calculation, server session rehydration. |
In-Memory (Synchronized with CPMS) |
useNotificationStore |
stores/notification.store.ts |
unreadCount: number, notifications: NotificationItem[], read state toggle, resetNotifications() teardown. |
In-Memory (Refetched via REST) |
useProfileStore |
stores/profile.store.ts |
Driver avatar URL, wallet balance in EVP, outstandingPoint debt tracking, vehicle selection. |
In-Memory |
useSessionExpiredStore |
stores/session-expired.store.ts |
isOpen: boolean, controls the global Session Expired modal dialog. |
In-Memory (Single-fire trigger) |
useChargingStationStore |
stores/charging-station-stores.ts |
Selected station, connector filter criteria (power, standard), search keyword, station detail drawer state. | In-Memory |
4.2 Multi-Session Keying & State Isolation
A critical architectural pattern in stores/charging-store.ts is the Multi-Session Keying model.
Instead of storing a single global active session, sessions are stored in an indexed dictionary keyed by physical hardware coordinates:
/** Unique composite session key for physical charger connector */
export function makeSessionKey(chargeBoxCode: string, connectorNo: number): string {
return `${chargeBoxCode}_${connectorNo}`;
}
export interface ChargingSession {
chargeBoxCode: string;
connectorNo: number;
stationName: string;
connectorLabel: string;
startedAt: number; // Epoch millisecond timestamp
elapsedSeconds: number;
kwh: number;
batteryPct: number;
transactionId: number | null;
hasNavigatedOnStop: boolean;
hasCompleted: boolean;
}
Architectural Invariant: The canonical delimiter is underscore (${chargeBoxCode}_${connectorNo}).
For backward compatibility with legacy socket payloads, stores/charging-store.ts:140 also safely falls back
to colon splitting if encountered. This dictionary indexing guarantees that concurrent charging sessions (e.g. multi-vehicle fleet accounts)
never overwrite each other's live metrics.
4.3 Server-Side Cache: TanStack React Query Configuration
Remote server state is managed through TanStack React Query v5 (libs/query/client.ts).
The global client is configured with strict staleness and garbage collection policies tailored for mobile networks:
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // Fail fast to allow light retry interceptor to handle 5xx
staleTime: 30000, // 30 seconds: prevents redundant network requests
gcTime: 300000, // 5 minutes: cache retention in memory
refetchOnReconnect: "always", // Immediate refetch when network comes online
refetchOnMount: true,
refetchOnWindowFocus: false, // Suppressed on React Native mobile clients
},
},
});
4.4 Unified Session Teardown Protocol
When authentication tokens expire or the driver explicitly logs out, a hard session invalidation occurs.
To prevent stale data leakage across driver sessions, stores/auth.store.ts executes a synchronous teardown protocol:
Calling clearSessionState() atomically executes:
useAuthStore.getState().clear()→ Purges user entity and auth status.useNotificationStore.getState().resetNotifications()→ Clears unread counts and cached alerts.useProfileStore.getState().resetProfile()→ Resets balance and personal profiles.useChargingStore.getState().clearAllSessions()→ Wipes all active charging timers and telemetry.queryClient.clear()→ Purges 100% of cached query responses from memory.
5. Networking, API Client & 401 Mutex Architecture
5.1 Dual-Instance Axios Pattern
The networking layer (api/client.ts) instantiates two distinct Axios instances:
- Public Client (
publicClient): Used exclusively for unauthenticated endpoints (login, register, forgot password, and token refresh). It has no Bearer token interceptor, eliminating circular dependency deadlocks. - Protected Client (
protectedClient): InjectsAuthorization: Bearer <token>headers, monitors responses for 401 UNAUTHORIZED status, and interfaces with the token refresh mutex queue.
5.2 Dynamic Microservice Path Construction
Backend services are structured as microservices under unified API gateway prefixes. api/client.ts dynamically
builds service base URLs according to the target domain module:
Service Name (ServiceName) |
Base Path Constructed | Target Domain Operations |
|---|---|---|
"auth" |
/api/v1/auth/* |
Driver login, token refresh, OTP verification, password reset. |
"charging" |
/api/v1/charging/* |
Start/stop session, live telemetry polling, charging orders, transactions. |
"stations" |
/api/v1/stations/* |
Charging station discovery, geographic search, connector availability. |
"wallet" |
/api/v1/wallet/* |
Top-up creation, VNPay verification, RFID card activation (/cards), ledger history. |
"invoices" |
/api/v1/invoices/* |
Invoice history listing, binary PDF streaming (/:id/download). |
"invoice-profiles" |
/api/v1/invoice-profiles/* |
Personal and corporate VAT tax invoice profiles. |
"vehicles" |
/api/v1/vehicles/* |
Electric vehicle garage, model specifications, fleet spending limits (PATCH /:id). |
"notifications" |
/api/v1/notifications/* |
Expo push token registration (/devices), notification inbox. |
"user" |
/api/v1/user/* |
Driver profile details, avatar upload, account metadata. |
5.3 Thread-Safe 401 Refresh Mutex Queue Sequence
When access tokens expire, concurrent protected requests risk triggering a stampede of refresh calls.
api/interceptors.ts enforces a thread-safe Mutex Queue:
The mutex implements 4 critical engineering invariants:
- Single-Flight Refresh: While
isRefreshing = true, subsequent failing 401 calls push their promiseresolve/rejecthandlers intofailedQueue[]. - Stale Response Guard (
_authSessionVersion): If a logout occurs while a refresh call is inflight, the response version is compared againstauthSessionVersion. Stale refresh results are safely discarded. - Single-Fire Session Expired Notification: If token refresh fails with 401/403,
hasNotifiedSessionExpiredensures only one modal dialog is presented, preventing alert stacking. - Concurrent Rotation Recovery (
rotatedByOtherFlow): If the refresh attempt fails with 400/401/403, the interceptor checksSecureStore(api/interceptors.ts:275-285). If another concurrent flow already rotated the refresh token (refreshTokenUsed !== latestRefreshToken), the queue is resolved withlatestAccessTokenand retried without dropping the session.
5.4 Network Resiliency & Weak Network Detection
The networking stack integrates with @react-native-community/netinfo via NetworkContext.tsx:
- Connection State: Distinguishes between completely offline (
isConnected === false) and weak/unstable connectivity (isWeakConnection === true, e.g. 2G or poor cellular handoff). - Idempotent Safe Retries: Read-only HTTP GET requests are automatically retried once on transient
socket timeouts (
ECONNABORTED). State-mutating POST/PATCH operations are never retried automatically to prevent duplicate transactions.
5.5 Localized Error Translation Pipeline & PII Redaction
Backend errors return standardized error codes (e.g. INVALID_CREDENTIALS, WALLET_INSUFFICIENT_BALANCE).
The interceptor transforms these into user-facing localized messages using api/errors.ts:
EXCLUDED_CODESProxy Guard: The code"UNAUTHORIZED"is explicitly excluded from the i18n error code translation proxy. This ensures that 401 errors fall through directly to the token refresh mutex queue rather than prematurely rejecting with a localized error string.- Sensitive Credential & Token Redaction: To satisfy
NFR-SEC-02,maskTokenandsanitizeAuthPayloadredact bearer tokens, refresh tokens, passwords, and PINs from debug console logs (e.g., displayingeyJhbGci...4f8a).
6. Detailed Subsystems Coding Design (LLD)
6.1 Subsystem 1: 5-Step EV Charging Session State Machine
The core business value of EV Power resides in screens/charging/ChargingSessionScreen.tsx
(mounted at app/charging/connect.tsx). The charging lifecycle is governed by a 5-step finite state machine:
The state machine transitions follow strict hardware and safety guards:
| State Machine Step | Component / Hook | Transition Preconditions & Guard Rules | Hardware / API Action |
|---|---|---|---|
1. plug |
PlugInStep.tsx |
Station connector status reaches "PREPARING" (cable inserted). Driver MUST TAP "Start Charging" CTA. |
Dispatches POST /api/v1/charging/sessions/start, initialising transactionId. |
2. connecting |
ConnectingStep.tsx |
Connector status reaches "IN_USE" AND elapsed time ≥ connectingMinDurationMs (1,800 ms). |
Hardware contactor relays energize; transitions to Step 3. |
3. charging |
ChargingStep.tsx |
Active 10-second polling (GET /sessions/{id}). Derives isAcNoSoc guard: if true, hides % badge and blurs car silhouette. |
Telemetry drives Reanimated wave fill (CarProgressContainer). Driver can swipe SwipeStopSlider. |
4. terminating |
StoppingChargeOverlay.tsx |
Driver swipes to stop or server terminates. Displays stopping overlay; awaits CHARGING_STOPPED push alert containing orderId. |
Polls GET /api/v1/charging/orders/{orderId} every 1,000 ms until status is COMPLETED or CANCELED. |
5. completed / error |
SuccessScreen / FailedScreen |
Order status resolves to COMPLETED (success) or CANCELED/fault (error). |
Presents itemized receipt with points billed and VAT invoice option, or cable unlock instructions. |
6.2 Subsystem 2: RFID Charge Card Management (charge-card)
Physical RFID charging card management is mounted at app/charging-card.tsx rendering
screens/profile/MyVouchersScreen.tsx:
- Status Filter Tabs: Tabbed segmented control (
ChargeCardStateTabs.tsx) filters cards by status:ALL,ACTIVE(valid point lots),EXPIRING_SOON(≤7 days remaining),USED_UP(zero balance), andEXPIRED. - Scratch Card PIN Activation: Modal dialog accepts the 16-character serial and scratch PIN,
dispatching
POST /api/v1/wallet/cards/activateviaWalletServices.redeemChargeCard(), immediately invalidating['my-charge-cards']and['profile']caches. - Point Lot Representation: Each card represents a promotional point voucher lot with face value (
facePoint), remaining points (pointRemaining), and expiry timestamp (pointExpireAt).
6.3 Subsystem 3: EVP Digital Wallet & VNPay Integration
Platform transactions strictly observe the currency conversion rate 1 EVP = 1,000 VND:
- Top-Up Creation: Driver selects EVP point package; app dispatches
POST /api/v1/wallet/topupsreceiving VNPay checkout URL (paymentUrl) andtopupId. - Embedded WebView Checkout: Payment is processed inside an in-app
<TopupPaymentWebView />modal, avoiding external browser switches and preserving mobile context. - Reactive Push Settlement: Upon VNPay completion, backend issues a
TOPUP_SUCCESSpush notification. The app invalidates['topup-status', id], refetchesGET /api/v1/wallet/topups/{id}, and refreshesuseProfileStorebalance.
6.4 Subsystem 4: Electronic VAT Invoices & PDF Streaming
Corporate and individual tax invoice management is implemented under app/invoice/:
- Invoice Profiles: Managed via
InvoiceProfileServices(POST /api/v1/invoice-profiles) supporting Tax Code (Mã số thuế), Company Legal Name, and registered Tax Address. - Binary PDF Streaming: Invoice PDF downloads invoke
InvoiceServices.downloadInvoice({ id }), dispatchingGET /api/v1/invoices/{id}/downloadwithresponseType: "arraybuffer"and headerAccept: application/pdf. The raw buffer is cached viaExpo FileSystemand presented viaExpo Sharing.
6.5 Subsystem 5: Push Notifications & Event Reconciliation
Real-time updates are mediated via Expo Push Notifications (api/services/notification.ts):
- Device Registration:
NotificationServices.registerForPushNotifications()obtains the push token and registers device metadata atPOST /api/v1/notifications/devices. - Foreground State Reconciliation: In
app/_layout.tsx, when anORDER_CREATEDnotification arrives, the app invokesChargingServices.getChargingSessionActive()and updateschargingStore.restoreSessions(), reactively displaying active sessions in<ActiveSessionList />onHomeScreen. - Background Notification Route Gap:
NotificationServices.addNotificationResponseReceivedListeneris provided for tapping background notifications; mounting it in root layout is scheduled for future deep-linking routing.
6.6 Subsystem 6: Vehicle Fleet Garage & Spending Limits (vehicles)
Vehicle management is implemented under app/vehicle/ and screens/vehicle/:
- Garage Listing & Details:
useVehiclesQueryfetches user EVs fromGET /api/v1/vehicles. - Per-Session Spending Cap: Corporate fleet accounts can set session spending limits via
VehicleServices.updateVehicle(id, { label, maxPointPerSession })dispatchingPATCH /api/v1/vehicles/{id}, preventing runaway vehicle charging debt.
7. Storage & Security Architecture
7.1 Multi-Tier Storage Architecture
Data persistence in the EV Power mobile app is segregated into three distinct isolation tiers based on data sensitivity and lifecycle characteristics:
- Tier 1: Hardware-Backed Encrypted Storage (
Expo SecureStore): Utilizes hardware keystores (iOS Keychain withkSecAttrAccessibleAfterFirstUnlockand Android Keystore with AES-256-GCM encryption). Only authentication and session-critical secrets are persisted here. - Tier 2: Non-Sensitive Persistent Storage (
AsyncStorage/app-storage.ts): Lightweight, unencrypted key-value storage used for UI theme modes, language selections, dismissible tutorial flags, and brute-force lockout hashes. - Tier 3: In-Memory Volatile State (
Zustand/ React Query Cache): All active charging telemetry, real-time prices, and temporary user inputs reside strictly in RAM and are automatically purged upon application termination or session logout.
7.2 Storage Boundary Catalog
In accordance with IEEE Std 1016 Data Persistence Viewpoint, the complete catalog of persistent storage keys
in the evp-app client is detailed below:
| Storage Key | Storage Engine | Encryption Standard | Payload / Purpose | Clear / Invalidation Trigger |
|---|---|---|---|---|
STORAGE_KEYS.ACCESS_TOKEN |
SecureStore | AES-256 (Hardware Keystore) | Short-lived JWT Bearer token for CPMS REST calls | Logout / 401 Hard / Session Expiry |
STORAGE_KEYS.REFRESH_TOKEN |
SecureStore | AES-256 (Hardware Keystore) | Long-lived refresh token for token exchange | Logout / Explicit Account Reset |
STORAGE_KEYS.RESET_TOKEN |
SecureStore | AES-256 (Hardware Keystore) | Ephemeral token for Forgot Password confirmation | Password change completed (clearResetToken()) |
change_password_lock_${hash} |
AsyncStorage | 64-bit FNV/Murmur Hash | JSON: { failedCount, lockedUntil, lastAttemptAt } |
24h expiry / calendar day change / successful reset |
app_theme_mode |
AsyncStorage | Unencrypted | "system" | "light" | "dark" user preference |
Never (Preserved across logins) |
app_language |
AsyncStorage | Unencrypted | "vi" | "en" | "ch" selected locale code |
Never (Preserved across logins) |
has_seen_location_onboarding |
AsyncStorage | Unencrypted | Boolean string ("true") suppressing location permission prompt |
Never (Device-level flag) |
has_seen_language_onboarding |
AsyncStorage | Unencrypted | Boolean string ("true") suppressing language picker modal |
Never (Device-level flag) |
partner_hero_bg_color |
AsyncStorage | Unencrypted | Hex color string for B2B co-branding hero background | Replaced on partner reload |
8. Error Handling, Resilience & Telemetry Fallback
8.1 Multi-Tier Exception Handling Framework
Errors propagate through a 4-tier funnel designed to prevent unhandled promise rejections and network failures:
ECONNABORTED), 401s, and 5xx server errors, automatically attempting light retries for GET requests.INVALID_OTP, INSUFFICIENT_BALANCE) are translated into localized UI strings via applyErrorCodeMessage before rejecting.retry: 1), and sets error state without throwing fatal JS exceptions.TikTokToast) or inline retry banners (SetupErrorBanner.tsx).app/_layout.tsx currently
lacks a top-level React ErrorBoundary component. Introducing an AppErrorBoundary wrapper around
<Stack /> is strongly recommended to protect against synchronous rendering crashes (e.g. SVG path parse errors
or Reanimated worklet runtime faults) and provide a graceful user recovery UI.
8.2 Live Telemetry Offline Degradation & Mathematical Utility
During active charging sessions, the mobile client must remain resilient when traversing areas with poor or intermittent cellular connectivity (e.g. underground parking basements):
- Actual Runtime Degradation: If telemetry polling fails due to connection loss,
useChargingStep.tscontinues ticking the client-side elapsed timer clock (setInterval(syncElapsedSeconds, 1000)). The instrumentation metrics (energyWh,socPercent,estimatedPoint) remain frozen at their last authoritative server values until connectivity is restored and polling resumes. - Dead-Reckoning Mathematical Model:
stores/charging-store.tsdefines a mathematical estimation helper (getChargingProgress) based on nominal charger output (CHARGER_POWER_KW = 50) and battery capacity (BATTERY_CAPACITY_KWH = 75). This module currently serves as a standalone calculation utility and is slated for active UI integration in a future release with an explicit driver notification badge.
8.3 Global Overlays & Modal Recovery Workflows
The application mounts two global recovery overlays in app/_layout.tsx:
NetworkStatusOverlay.tsx: Renders as a full-screen centered modal dialog with an opaque backdrop (bg-black/40). When!isConnected, it activates a blocking modal dialog with a manual "Retry Now" trigger. WhenisWeakConnection === true, it displays an amber status notice with non-blocking touch interaction (pointerEvents="box-none").SessionExpiredModal.tsx: Controlled bysession-expired.store.ts. When refresh tokens expire or are rejected by the CPMS, the session teardown protocol purges all in-memory state and cache, and this modal prompts the driver to confirm re-login, executingrouter.replace("/(auth)/login").
9. Testing, Verification & Coding Standards
9.1 TypeScript Typing & Architectural Boundaries
- Strict Typing: Zero usage of unconstrained
anyin domain modules. Generic typing is enforced on all API responses (this.api<T>()). - Layered Type Separation: Domain entities reside under
types/<domain>/*.types.ts(e.g.types/charge-card/,types/vehicle/), while remote REST service DTO payloads reside undertypes/services/*.types.ts. - Immutability: State updates in Zustand stores utilize spread patterns or functional updaters, ensuring strict referential equality checks for React render optimizations.
9.2 Unit & Integration Testing Strategy
- Zustand Store Unit Tests: Verify store actions, multi-session key mapping, duration calculations, and atomic session teardown in isolation with mocked storage engines.
- Interceptor Mutex Tests: Test parallel 401 token refresh queue concurrency, ensuring only 1 refresh
call is dispatched, failed requests are replayed with new tokens, and
rotatedByOtherFlowrecovers without session loss. - State Machine Guard Tests: Validate that
useChargingStepstrictly enforces the explicit CTA tap in Step 1, the 1,800 ms hardware delay in Step 2, and theisAcNoSocsuppression guard on AC charging sessions.
9.3 Mocking & Simulation Infrastructure
- Axios Mock Adapter: Simulates REST endpoints for testing offline queuing and token refresh without live backend dependencies.
- NetInfo Mocking: Tests
NetworkStatusOverlaybehaviors across offline, cellular 2G (weak), and high-speed Wi-Fi states.