Software Design Description (SDD) for EV Power Mobile Application EV Power Mobile Application SDD
← Portal View SRS →
Official Engineering Design Document • IEEE Std 1016-2009

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 rotatedByOtherFlow race 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 57 (v57.0.22) and React Native 0.86.3 Pure Bridgeless Architecture (Fabric Renderer + TurboModules).

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:

EV DRIVER (User) iOS / Android Device Touch • Camera • Bio Auth PHYSICAL EVSE & RFID Physical Charging Post RFID Tap • QR Code Decal Connector Relay Closure SYSTEM BOUNDARY: EVP MOBILE APP com.evpower.chargingapp • Expo SDK 57 • React Native 0.86 (Bridgeless Fabric) Auth & Biometrics SecureStore AES-256 Station Map & Search Leaflet Web • Location Charging Orchestrator 5-Step State Machine Multi-Session Keying EVP Wallet & Ledger 1 EVP = 1,000 VND Embedded VNPay WebView RFID Point Vouchers WalletServices /cards VAT Invoice & Fleet Binary PDF /download NETWORKING & SECURITY INFRASTRUCTURE Dual-Instance Axios • 401 Refresh Mutex • rotatedByOtherFlow NetInfo Reachability • TanStack Query Cache • Session Expired Guard CPMS CLOUD PLATFORM RESTful Microservices auth, stations, charging, wallet, invoices, vehicles, notifications PAYMENT GATEWAY VNPay Sandbox / Prod In-App WebView • IPN Webhook PUSH NOTIFICATION SERVICE Expo Push Service / APNs / FCM POST /api/v1/notifications/devices HARDWARE CHARGER FLEET OCPP 1.6J / 2.0.1 Protocol Managed by CPMS Cloud QR Scan HTTPS/REST WebView Push APNs OCPP 1.6J
Fig 1.1: IEEE Std 1016 System Context Viewpoint & External Actor Boundary Interactions
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/ Decoupled file-based navigation (Expo Router ~57.0.21), nested tabs, modal sheets, guest onboarding gatekeeper, and route parameters.
UI Component System components/ Atomic design presentation components (CarProgressContainer, BatteryStatusCard, CostSummaryCard, ProfileUserCard, VirtualEvCard) 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 4.5.1 + react-native-worklets 0.10.1 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.
Fabric Fabric UI Renderer React Native C++ UI rendering engine replacing legacy Paper; synchronous layout calculations and 120Hz animations.
TurboModules Turbo Native Modules Direct C++ JavaScript Interface (JSI) bindings for native APIs, eliminating asynchronous JSON bridge serialization.
Bridgeless Bridgeless Native Runtime Pure modern architecture in RN 0.86 / Expo 57 where the legacy JavaScript-to-native Bridge is completely removed.
Hermes v1 Hermes v1 JavaScript Engine Universal default JS engine with static bytecode compilation, fixed worklet memory management, and EAS bytecode diffing.
CNG Continuous Native Generation Automated native project synthesis (npx expo prebuild --clean) compiling iOS and Android projects directly from config plugins.

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 Pure Bridgeless Architecture: Fabric Native Renderer and TurboModules on React Native 0.86.3, React 19.2.3, and Expo SDK 57.0.22; legacy bridge and Paper renderer permanently eliminated.

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.tsxscreens/profile/ProfileScreen.tsx (ProfileUserCard, VirtualEvCard) useProfileStore, useAuthStore UserServices (api/services/user.ts)
FR-STN-01..03 Station Map, Cluster Markers & Filter app/(tabs)/stations.tsxscreens/stations/* useStationQuery, useAllStationQuery, useChargingStationStore StationServices (api/services/station.ts)
FR-CHG-01..05 5-Step Charging Session Machine app/charging/connect.tsxscreens/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.tsxscreens/vehicle/* useVehiclesQuery, useVehicleDetailQuery VehicleServices (api/services/vehicle.ts)
FR-CRD-01..03 RFID Charge Cards & Scratch PIN app/charging-card.tsxscreens/profile/MyVouchersScreen.tsx useChargeCardList (useMyChargeCardsQuery) WalletServices (api/services/wallet.ts)
FR-WAL-01..04 EVP Wallet, VNPay Top-Up & Ledger app/top-up/index.tsxscreens/wallet/* useTopupStatusQuery, useTopupHistoryQuery, useProfileStore WalletServices (api/services/wallet.ts)
FR-INV-01..03 VAT Tax Profiles & PDF Invoices app/invoice/index.tsxscreens/invoice/* useInvoiceListQuery, useUserInvoiceProfileQuery InvoiceServicesInvoiceProfileServices
FR-NOT-01..02 Push Notifications & REST Feed app/notifications/index.tsxscreens/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 4.5.1 + react-native-worklets 0.10.1 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 and synchronous SplashScreen.hide() 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 /> StopChargingSlider.tsx, StoppingChargeOverlay.tsx Section 6.1, Section 6.1.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 1: PRESENTATION & ROUTING LAYER (Expo Router 57 & React Native 0.86 Bridgeless) File-Based Routes (app/) _layout, (tabs), charging/connect, auth Screen Controllers (screens/) ChargingSession, Setup, MyVouchers, Guests Atomic UI Components (components/) CarProgressContainer, BatteryStatusCard LAYER 2: DOMAIN HOOKS & INTERACTIVE FACADE (Stateful Orchestration) useChargingSession & useChargingStep State Machine, Live Polling, AC SoC Guard & Reanimated Worklets Context Facades & Hardware Observers NetworkContext, ThemeContext, I18nContext, useAppActive LAYER 3: CLIENT STATE (Zustand) & SERVER CACHE (TanStack React Query) Client Stores (stores/) auth, charging, notification, profile, session-expired, charging-station Query Cache & Keys (queries/) queryClient: 30s stale, 5m gc, stations, orders, wallet, vehicles API TRANSPORT & MUTEX (api/) Axios Dual-Instance, 401 Mutex, rotatedByOtherFlow, Masking PERSISTENCE STORAGE (storage/) SecureStore (AES-256 Tokens), AsyncStorage (Preferences & Lockout)
Fig 2.1: Clean Layered Architecture & Data Flow of the EV Power Mobile Application
Layer Module Responsibilities Allowed Downward Dependencies
1. Presentation Layer Expo Router ~57.0.21 decoupled file-based routing, screen controllers, atomic UI components, gestures, and NativeWind v4 styling. Layer 2 (Hooks & Facades), Layer 3 (Zustand Stores & React Query hooks)
2. Domain Hooks & Facades Custom hooks encapsulating state machines, Reanimated 4.5.1 + react-native-worklets 0.10.1 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 rotatedByOtherFlow if 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 (Bridgeless Fabric): Bypasses JavaScript event queues by executing high-frequency telemetry calculations directly on the native UI thread via Reanimated 4.5.1 and standalone react-native-worklets 0.10.1 C++ runtime, throttled to 65ms (~15 FPS).
  • Pure Bridgeless Runtime Pattern: Eliminates legacy bridge JSON serialization entirely; all native calls execute synchronously or via TurboModules JSI bindings. Config flag newArchEnabled: true is eliminated.
  • Decoupled Navigation Pattern: Expo Router ~57.0.21 is completely decoupled from upstream @react-navigation/*, ensuring route states and navigation dispatchers remain synchronized without external hook leaking.

3. Component & UI Architecture

3.1 Expo Router File-Based Routing Topology & Decoupled Navigation

The application leverages Expo Router ~57.0.21 (Expo SDK 57), providing type-safe, URL-driven routing modeled after file-system conventions with a completely decoupled navigation architecture independent of external @react-navigation/* packages:

  • Root & Onboarding Gatekeeper (app/index.tsx): Determines whether the device has completed onboarding, evaluates authentication status via useAuthStore, and routes either to /(tabs)/home (authenticated), /(auth)/login, or guest screens.
  • Authentication Stack (app/(auth)/): Public authentication stack containing login.tsx, register.tsx, forgot-password.tsx, and reset-password.tsx. Note that OTP verification is handled inline as an active step within ForgotPasswordScreen.tsx and RegisterScreen.tsx rather 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.tsx controller, 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.tsx controller, ProfileUserCard responsive identity header with modular VirtualEvCard (VirtualCard), managing wallet balance, RFID charge cards, vehicles, invoices, and help center.
  • Active Charging State Machine Route (app/charging/connect.tsx): Mounts screens/charging/ChargingSessionScreen.tsx, which hosts the core 5-step charging machine (PlugInStep, ConnectingStep, ChargingStep, SuccessScreen, FailedScreen). Note that app/charging-detail.tsx re-exports OrderDetailScreen (order receipt), not the active state machine.
  • Guest Exploration Subsystem (screens/guests/): Provides unauthenticated discovery across 6 dedicated screens: GuestHomeScreen, GuestStationScreen, GuestSearchScreen, GuestStationDetailScreen, GuestScanGateScreen, and GuestProfileScreen.
Decoupled Navigation Architecture (SDK 57 Invariant): In Expo SDK 57, Expo Router decoupled its internal state machine from upstream @react-navigation/* packages. Direct imports of useIsFocused from @react-navigation/native trigger route desynchronization and stale focus states. All 10 application locations have been refactored to import directly from expo-router: screens/charging/ChargingSessionScreen.tsx, screens/charging/SetupScreen.tsx, screens/qr/QRScanScreen.tsx, screens/stations/StationMapScreen.tsx, hooks/charging/useActiveSessionItem.ts, hooks/useChargingSession.ts, hooks/useAppActive.ts, components/home/map-station/HomeStationMap.tsx, hooks/map-station/useStationMap.tsx, and components/stations/detail/StationLocationMap.tsx.

3.2 Root Provider Tree Lifecycle (app/_layout.tsx)

The root layout component (app/_layout.tsx) manages the startup bootstrap lifecycle, Space Grotesk font asset loading, synchronous splash dismissal via expo-splash-screen ~57.0.9 (SplashScreen.hide()), Android 15 mandatory edge-to-edge windowing insets via react-native-safe-area-context ~5.7.0, and strict context provider nesting:

GestureHandlerRootView QueryProvider (queryClient) I18nProvider (vi, en, ch) ThemeProvider (Light / Dark) NetworkProvider (NetInfo) <Stack /> (Expo Router) <NetworkStatusOverlay /> <TikTokToast /> (Notifications) <SessionExpiredModal />
Fig 3.1: Complete Root Provider Hierarchy & Global UI Overlays Tree (app/_layout.tsx)
Lifecycle Timing Invariant: Native splash screen auto-hiding is prevented via 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 = 22 is 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 using Animated.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 (nativewind@^4.2.7) backed by react-native-css-interop@0.2.7 and tailwindcss@3.4.19. This toolchain is certified against React 19.2.3 and React Native 0.86.3 Fabric without requiring NativeWind v5 breaking syntax modifications. 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.tsx renders 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 matching batteryPct.
  • Throttled Reanimated UI Worklet (Fabric Engine): The useAnimatedNumber hook runs a React Native Reanimated 4.5.1 worklet powered by the standalone react-native-worklets 0.10.1 C++ engine. It is throttled to 65ms (~15 FPS) via useAnimatedReaction, interpolating numeric metrics (kwh, amount in EVP, batteryPct) over 1,200 ms with cubic easing, completely offloading the JS thread. Babel AST transformations are automatically injected by babel-preset-expo@57.0.11; manual worklet plugin registration is prohibited.
  • 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.

3.6 Modular Profile Identity & VirtualCard Architecture (ProfileUserCard & VirtualEvCard)

In accordance with the Single Responsibility Principle (SRP) and component modularity standards, the driver profile header presentation was refactored to cleanly decouple user account metadata from the digital wallet card presentation:

Component Source File Architectural Scope & Visual Responsibilities State & Event Handlers
ProfileUserCard components/profile/ProfileUserCard.tsx Driver identity container: Smooth entrance slide-up / fade-in animation (320ms, Easing.out(Easing.ease)); UserAvatar with border glow; driver full name (auto-truncated); masked phone number via formatMaskedPhone(phone); and LiquidGlassButton edit profile action. Receives username, phone, avatarUrl, isTablet, onEditPress; embeds <VirtualEvCard />.
VirtualEvCard (VirtualCard) components/profile/VirtualEvCard.tsx Modular virtual EV pass card: 3-stop golden LinearGradient background with gold drop shadow (#ca8a04); metallic gold EMV chip with micro-circuit divider paths; 90°-rotated contactless Wi-Fi waves; brand EV icon; auto-scaling wallet balance (adjustsFontSizeToFit, evpToVnd); conditional amber debt strip (outstandingPoint); and full-width top-up CTA button. Receives balance, outstandingPoint, isTablet, and onTopUpPress; routes to /profile/top-up via throttled navigation.
Architectural Benefit of Decoupled VirtualCard: Separating VirtualEvCard from ProfileUserCard isolates financial formatting logic (formatCurrency, evpToVnd) and top-up navigation dispatching from personal account identity rendering. This enables VirtualEvCard to be independently tested, re-used within checkout/payment drawers, or embedded into future Apple Wallet / Google Wallet pass provisioning workflows.

4. State Management & Data Fetching Architecture

4.1 Client-Side State: Zustand Store Architecture

The application uses Zustand 5 (v5.0.15) 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:

LOGOUT / 401 HARD Session Invalidation clearSessionState() authSessionVersion += 1 processQueue(Error, null) tokenStorage.clearTokens() useAuthStore.getState().clear() useNotificationStore.getState().resetNotifications() useProfileStore.getState().resetProfile() useChargingStore.getState().clearAllSessions() queryClient.clear() (Purges all server cache)
Fig 4.1: Unified State Teardown & Cache Purge Protocol upon Session Invalidation

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:

  1. 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.
  2. Protected Client (protectedClient): Injects Authorization: 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:

Caller A (GET /charging/orders) Caller B (GET /user/profile) Axios Interceptor (Mutex) Auth Backend API 1. Request fails with 401 UNAUTHORIZED LOCK isRefreshing = true 2. Request B fails with 401 Push to failedQueue[] 3. POST /api/v1/auth/refresh (refreshToken) 4. 200 OK: newAccessToken & newRefreshToken Save new tokens to SecureStore & AuthStore (or recover via rotatedByOtherFlow) 5. Retry Request A with new Bearer Token → 200 OK 6. Drain failedQueue: Retry Request B → 200 OK FREE isRefreshing = false
Fig 5.1: Thread-Safe 401 Refresh Token Mutex Queue & Request Replay Sequence

The mutex implements 4 critical engineering invariants:

  1. Single-Flight Refresh: While isRefreshing = true, subsequent failing 401 calls push their promise resolve/reject handlers into failedQueue[].
  2. Stale Response Guard (_authSessionVersion): If a logout occurs while a refresh call is inflight, the response version is compared against authSessionVersion. Stale refresh results are safely discarded.
  3. Single-Fire Session Expired Notification: If token refresh fails with 401/403, hasNotifiedSessionExpired ensures only one modal dialog is presented, preventing alert stacking.
  4. Concurrent Rotation Recovery (rotatedByOtherFlow): If the refresh attempt fails with 400/401/403, the interceptor checks SecureStore (api/interceptors.ts:275-285). If another concurrent flow already rotated the refresh token (refreshTokenUsed !== latestRefreshToken), the queue is resolved with latestAccessToken and 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_CODES Proxy 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, maskToken and sanitizeAuthPayload redact bearer tokens, refresh tokens, passwords, and PINs from debug console logs (e.g., displaying eyJhbGci...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:

1. PLUG STEP PlugInStep.tsx Polls status (5,000 ms) Awaits PREPARING DRIVER TAP handleStartCharge 2. CONNECTING STEP ConnectingStep.tsx Awaits IN_USE status + Min Delay ≥1,800ms Tx Active 3. ACTIVE CHARGING (ChargingStep.tsx) 10-Second Telemetry Polling: GET /sessions/{id} • CarProgress wave clip DC Fast / Valid SoC socPercent > 0 Car Silhouette Fill + % Badge AC Charger Fallback isAcNoSoc === true Suppress % Badge + Blur Car Active Instrumentation & Swipe-to-Stop Duration (clockTimer) • Energy (kWh) • Power (kW) • EVP/VND Cost useAnimatedNumber worklet (~15 FPS) • SwipeStopSlider gesture Driver swipes → sets step: "terminating" → POST /stop 4. TERMINATION & SETTLEMENT <StoppingChargeOverlay /> Awaits CHARGING_STOPPED push Polls GET /orders/{id} @ 1000ms Resolves to COMPLETED / CANCELED 5A. SUCCESS (Normal) SuccessScreen.tsx orderId, totalEnergyWh, points Invoice profile & PDF receipt 5B. FAILED / CANCELED FailedScreen.tsx CABLE_DISCONNECTED / ABORT Cable unlock instruction
Fig 6.1: 5-Step EV Charging State Machine, Telemetry Polling, AC Fallback, and Two-Phase Settlement

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 in-tree full-screen blur overlay (BlurView on Fabric) with centered dialog card and blocked touch events; 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.
SDK 57 Fabric Stabilization Details:
  • In-Tree StoppingChargeOverlay Architecture: Legacy React Native <Modal> spawned an isolated native Window, which broke Android Fabric blur (RenderEffect) and miscalculated vertical screen centering. The overlay was refactored into an in-tree full-screen view (StyleSheet.absoluteFillObject, zIndex: 9999) backed by expo-blur, dead-centered dialog card, and blocked pointer events.
  • Android Map Marker Fabric Stabilization: Custom marker views in components/map-station/StationMarker.tsx explicitly declare collapsable={false}. Under Fabric, Android View Flattening optimized away container Views without explicit bounds, causing Google Maps software canvas snapshots to capture empty 0x0 bitmaps and rendering invisible station pins.

6.1.1 StoppingChargeOverlay Component Architecture & Fabric In-Tree Overlay Pattern

In Step 4 (terminating) of the charging lifecycle, the application executes a high-stakes, two-phase settlement sequence. During this interval, the hardware contactor is opening, the CPMS is calculating final energy meters, and premature driver navigation or concurrent touch interactions could produce an orphaned session or corrupted billing record. To guarantee uncompromising reliability and visual excellence on modern mobile operating systems, StoppingChargeOverlay.tsx was re-engineered from a legacy native modal into an In-Tree Full-Screen Absolute Viewport Overlay.

STOPPING CHARGE OVERLAY: IN-TREE MULTI-TIER FABRIC ARCHITECTURE 1. Root View Hierarchy screens/charging/ChargingSessionScreen.tsx <View className="flex-1"> Root Full Viewport Container (Edge-to-Edge) <SafeAreaView edges={["top","bottom"]}> ChargingSessionHeader onBack = isStopping ? () => {} : handleBack ChargingStep Telemetry Display Live kWh, SoC, CarProgress wave clip ChargingSessionBottomAction UNMOUNTED: !isStoppingCharge <StoppingChargeOverlay /> Mounted as Sibling to SafeAreaView visible={isStoppingCharge && !isComplete} Bypasses Native <Modal> Window Boundary 2. Layered Visual Stack In-Tree Z-Index & Elevation Compositing Layer A: Full-Screen Viewport Shell StyleSheet.absoluteFillObject • zIndex: 9999 • elevation: 9999 alignItems: "center" • justifyContent: "center" Layer B: Native BlurView (expo-blur) blurMethod="dimezisBlurView" • intensity={iOS: 20, Android: 30} tint="systemUltraThinMaterial" • Fabric RenderEffect Safe Layer C: Anti-Fog Contrast Scrim pointerEvents="none" • absoluteFillObject isDark ? rgba(0,0,0,0.35) : rgba(0,0,0,0.08) (Eliminates Milky Fog) Layer D: Dead-Centered Dialog Card minWidth: 220, maxWidth: 240 • rounded-3xl • px-8 py-6 shadowOffset: {0,12} • shadowRadius: 24 • elevation: 12 Lottie t("charging.stoppingTitle") & t("...Message") accessibilityRole="alert" • assertive live region 3. Event & Gesture Lockdown Hardware Back & Touch Guardrails Touch Responder Trapping pointerEvents="auto" onStartShouldSetResponder: true onMoveShouldSetResponder: true onStart/MoveShouldSetResponderCapture: true onResponderTerminationRequest: false Android Hardware Back Press BackHandler.addEventListener() () => true (Unconditionally Blocked) Navigation Stack Protection navigation.setOptions({ gestureEnabled: false }) navigation.addListener("beforeRemove", e.preventDefault) Blocks iOS swipe-to-dismiss & programmatic pop Unmount / Dismissal Teardown Restores gestureEnabled: true • Removes listeners Automatic cleanup on step change to success/canceled
Fig 6.2: StoppingChargeOverlay In-Tree Multi-Tier Rendering Stack & Lockdown Protocol
Architectural Dimension Legacy React Native <Modal> Defect SDK 57 Fabric In-Tree StoppingChargeOverlay Solution Engineering Impact & OS Compliance
Window Hierarchy & Insets Spawns secondary native window (android.app.Dialog / UIWindow), divorcing status bar and navigation bar insets from host view. Direct child of root screen <View className="flex-1">, rendered as sibling to <SafeAreaView> with StyleSheet.absoluteFillObject. Zero vertical offset or header jumping on Android 15 edge-to-edge displays and iOS dynamic islands.
Hardware Blur Rendering Fails to capture or blur underlying host activity window pixels; causes black canvas drops, flicker, or invalid context crashes under expo-blur. Employs <BlurView blurMethod="dimezisBlurView" intensity={20/30}> directly within the Fabric render tree. Seamless hardware-accelerated RenderEffect blur directly over active charging meters and car silhouette.
Optical Scrim & Anti-Fog Relies on default native modal dimming or single-layer blur, creating an over-exposed, washed-out milky haze on OLED screens. Implements dual-tier backdrop: Base BlurView plus dedicated non-blocking dimming scrim (rgba(0,0,0,0.35) dark / rgba(0,0,0,0.08) light). Deep contrast, rich glassmorphic texture, and high text legibility meeting WCAG AAA contrast ratios in light/dark themes.
Gesture & Touch Isolation Touch event dispatch can leak across window edges or drop ongoing pan gestures unexpectedly, allowing underlying button clicks. Strict responder trap: pointerEvents="auto", onStartShouldSetResponder, onMoveShouldSetResponder, and onResponderTerminationRequest={() => false}. Absolute touch capture; prevents duplicate slider drags or background screen taps during API in-flight requests.
Hardware Back & Navigation Lock Android back button dismissal handled via separate modal callbacks; iOS edge-swipe back navigation remains active in navigation stack. Triple lockdown: BackHandler suppression, navigation.setOptions({ gestureEnabled: false }), and beforeRemove interception. Guarantees atomic settlement state; driver cannot pop screen or abort navigation until server returns COMPLETED / CANCELED.
Screen Bottom Action Coordination Bottom action slider remains mounted beneath modal, risking visual bleed-through or re-render collisions. In ChargingSessionScreen.tsx, ChargingSessionBottomAction is conditionally unmounted when isStoppingCharge === true; header back action is no-op'd. Eliminates DOM weight, avoids overlapping drop-shadows, and guarantees zero phantom interaction.
Accessibility (A11y) Conformance: StoppingChargeOverlay satisfies WAI-ARIA modal dialog criteria on both mobile accessibility trees: it marks the host container with accessibilityViewIsModal={true}, aria-modal={true}, accessibilityRole="alert", and accessibilityLiveRegion="assertive". The dialog card announces a concatenated, localized status string via accessibilityLabel={`${t("charging.stoppingTitle")}. ${t("charging.stoppingMessage")}`}, while the embedded Lottie animation is explicitly declared with accessible={false} and importantForAccessibility="no" to eliminate redundant screen reader chatter.

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), and EXPIRED.
  • Scratch Card PIN Activation: Modal dialog accepts the 16-character serial and scratch PIN, dispatching POST /api/v1/wallet/cards/activate via WalletServices.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:

  • Wallet Entrypoint via VirtualEvCard: Driver views real-time EVP balance and outstanding debt points on the modular <VirtualEvCard /> in ProfileUserCard. Tapping the full-width "Nạp tiền" (Top Up) CTA navigates directly to app/(tabs)/profile/top-up.tsx.
  • Top-Up Creation: Driver selects EVP point package; app dispatches POST /api/v1/wallet/topups receiving VNPay checkout URL (paymentUrl) and topupId.
  • 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_SUCCESS push notification. The app invalidates ['topup-status', id], refetches GET /api/v1/wallet/topups/{id}, and refreshes useProfileStore balance.

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 }), dispatching GET /api/v1/invoices/{id}/download with responseType: "arraybuffer" and header Accept: application/pdf. The raw buffer is cached via expo-file-system ~57.0.7 using the modern Paths.cache.uri API and presented via Expo Sharing ~57.0.19.

6.5 Subsystem 5: Push Notifications & Event Reconciliation

Real-time updates are mediated via Expo Push Notifications (api/services/notification.ts):

  • Config Plugin & Device Registration: Handled by expo-notifications ~57.0.18 declared as a native plugin in app.json (legacy root notification configuration dropped). NotificationServices.registerForPushNotifications() obtains the push token, registers metadata at POST /api/v1/notifications/devices, and cleans up listeners using sub.remove().
  • Foreground State Reconciliation: In app/_layout.tsx, when an ORDER_CREATED notification arrives, the app invokes ChargingServices.getChargingSessionActive() and updates chargingStore.restoreSessions(), reactively displaying active sessions in <ActiveSessionList /> on HomeScreen.
  • Background Notification Route Gap: NotificationServices.addNotificationResponseReceivedListener is 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: useVehiclesQuery fetches user EVs from GET /api/v1/vehicles.
  • Per-Session Spending Cap: Corporate fleet accounts can set session spending limits via VehicleServices.updateVehicle(id, { label, maxPointPerSession }) dispatching PATCH /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:

  1. Tier 1: Hardware-Backed Encrypted Storage (Expo SecureStore): Utilizes hardware keystores (iOS Keychain with kSecAttrAccessibleAfterFirstUnlock and Android Keystore with AES-256-GCM encryption). Only authentication and session-critical secrets are persisted here.
  2. 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.
  3. 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:

Tier 1: Network
Axios interceptors trap network timeouts (ECONNABORTED), 401s, and 5xx server errors, automatically attempting light retries for GET requests.
Tier 2: Localization
Backend error codes (INVALID_OTP, INSUFFICIENT_BALANCE) are translated into localized UI strings via applyErrorCodeMessage before rejecting.
Tier 3: React Query
React Query catches rejected promises, controls retry limits (retry: 1), and sets error state without throwing fatal JS exceptions.
Tier 4: UI Toast / Banner
Components observe error states and present animated toasts (TikTokToast) or inline retry banners (SetupErrorBanner.tsx).
Architectural Reliability & Hermes Engine Stabilization:
  • Hermes v1 Engine Memory Stabilization: Early SDK 57 preview builds exhibited a memory leak regression when exchanging high-frequency telemetry objects with react-native-worklets on the UI thread. In expo@~57.0.22 and React Native 0.86.3, this regression is fully resolved, providing Hermetic bytecode compilation and EAS Update bytecode diffing.
  • Root Error Boundary: The root provider tree in 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 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.ts continues 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.ts defines 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 EV Power architecture distinguishes between two specialized overlay tiers based on lifecycle scope, hardware interaction depth, and rendering performance requirements:

Overlay Component Mounting Scope Architectural Scope & Purpose Modality & Touch Trapping Backdrop & Blur Mechanics
NetworkStatusOverlay.tsx Root Provider Tree (app/_layout.tsx) Global network connectivity observer (useNetInfo). Displays blocking recovery modal when offline (!isConnected) with manual retry trigger; displays non-blocking amber badge when connection is degraded (isWeakConnection). Full-screen blocking when disconnected (pointerEvents="auto"); non-blocking pass-through when weak (pointerEvents="box-none"). Opaque translucent scrim (bg-black/40) with centered modal alert card.
SessionExpiredModal.tsx Root Provider Tree (app/_layout.tsx) Global authentication invalidation & token teardown orchestrator (useSessionExpiredStore). When CPMS refresh fails or returns 401 unrecoverable, prompts re-login and redirects to /(auth)/login. Strict blocking modal; intercepts all user touch interactions until re-login confirmation is pressed. Standard themed modal scrim with centered warning dialog card and primary action CTA.
StoppingChargeOverlay.tsx Screen Root (ChargingSessionScreen.tsx) Session-critical termination & settlement overlay. Shields active session while contactor opens, meters calculate, and order completes. Enforces atomic transaction safety. In-tree responder trap (pointerEvents="auto", responder capture, BackHandler lock, gestureEnabled: false, beforeRemove interception). Fabric-native BlurView (dimezisBlurView, intensity 20 iOS / 30 Android) plus layered anti-fog scrim (rgba(0,0,0,0.35) dark / rgba(0,0,0,0.08) light).
Architectural Rationale for In-Tree Screen-Scoped Overlays: While NetworkStatusOverlay and SessionExpiredModal are mounted globally at app/_layout.tsx because their triggers transcend any single screen route, StoppingChargeOverlay is intentionally mounted locally at the root of ChargingSessionScreen.tsx outside <SafeAreaView>. This architectural decision provides four critical advantages:
  1. State & Hook Lifecycle Co-location: Stopping state is intimately bound to the local useChargingSession hook instance, its transactionId, and the 5-step state machine. Global mounting would require unnecessary state hoisting into a global Zustand store.
  2. Route-Scoped Navigation Lockdown: Calling navigation.setOptions({ gestureEnabled: false }) and intercepting beforeRemove operates precisely on the active charging screen stack entry, preventing route teardown without freezing navigation transitions across the rest of the application.
  3. Coordinated Viewport Hierarchy: When isStoppingCharge is active, ChargingSessionScreen synchronously unmounts ChargingSessionBottomAction and neutralizes ChargingSessionHeader.onBack, preventing duplicate stop slider interactions and phantom touch handling.
  4. GPU & Fabric Compositor Efficiency: Keeping expo-blur and the Lottie animation lifecycle scoped to the active screen prevents continuous GPU surface allocation in the background when the user is simply browsing stations or managing their wallet.

9. Testing, Verification & Coding Standards

9.1 TypeScript Typing & Architectural Boundaries

  • Strict Typing: Zero usage of unconstrained any in 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 under types/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 rotatedByOtherFlow recovers without session loss.
  • State Machine Guard Tests: Validate that useChargingStep strictly enforces the explicit CTA tap in Step 1, the 1,800 ms hardware delay in Step 2, and the isAcNoSoc suppression 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 NetworkStatusOverlay behaviors across offline, cellular 2G (weak), and high-speed Wi-Fi states.

9.4 Continuous Native Generation & Multi-Gate Verification Protocol

Following the Expo SDK 57 upgrade, native project directories (ios/, android/) are governed by Continuous Native Generation (CNG). Every release and refactor must satisfy a strict 5-gate verification sequence prior to merge:

Verification Gate Execution Command Validation Criteria & Target Standard
Gate 1: Expo Dependency Health npx expo install --check Exits 0 with Dependencies are up to date; zero version mismatches across 58 packages.
Gate 2: Expo Doctor Diagnostic npx expo-doctor 18/18 checks pass with 0 warnings, validating pure Bridgeless New Architecture configurations.
Gate 3: TypeScript Typecheck npm run typecheck tsc --noEmit exits 0 with zero diagnostics across all domain models and service types.
Gate 4: Clean CNG Prebuild npx expo prebuild --clean Successfully regenerates native iOS (CocoaPods/Xcode 26+) and Android (Gradle 8.x / API 35) projects without plugin errors.
Gate 5: ESLint Verification npm run lint ESLint 9 flat config exits 0 with zero syntax or rule violations.

10. Expo SDK 57 Master Dependency Matrix & Architectural Evolution

10.1 Master Dependency Version Matrix (58 Packages)

The EV Power Mobile Application was migrated from Expo SDK 54 (React Native 0.81.5, React 19.1.0) to Expo SDK 57 (v57.0.22, React Native 0.86.3, React 19.2.3). All 58 production and development dependencies are synchronized according to bundledNativeModules.json and peer-dependency satisfaction:

Package Name Baseline (SDK 54) Target (SDK 57) Category Architectural & Migration Role
expo ~54.0.29 ~57.0.22 Core Runtime Pure Bridgeless mode default; Fabric Renderer + TurboModules; Hermes v1.
react 19.1.0 19.2.3 Core Engine Introduces React 19 concurrent features, Activity primitives, and useEffectEvent.
react-dom (Missing) 19.2.3 Core Engine (Peer) Mandatory peer dependency for Expo 57 and Expo Router web primitives.
react-native 0.81.5 0.86.3 Native Runtime Fabric C++ shadow tree; drops Paper & Bridge; Android 15 edge-to-edge support.
expo-router ~6.0.19 ~57.0.21 Navigation Engine Decoupled from @react-navigation/*; unified SDK versioning; native tab transitions.
react-native-reanimated 4.1.1 4.5.1 UI Animation Fabric-only; co-installed with standalone worklets engine; Babel auto-injected.
react-native-worklets 0.5.1 0.10.1 Worklet Engine Standalone C++ worklet runtime; executes telemetry animations on UI thread.
nativewind ^4.2.1 ^4.2.7 Styling Framework Preserves Tailwind v3 pipeline; paired with react-native-css-interop@0.2.7.
react-native-css-interop ^0.2.5 ^0.2.7 Styling Engine Certified against React 19.2.3 and RN 0.86.3 Fabric.
react-native-maps 1.20.1 1.27.2 Mapping Engine Fabric codegen (RNMapsSpecs); requires collapsable={false} for Android markers.
expo-camera ~17.0.10 ~57.0.5 Hardware Vision Native MLKit barcode plugin; audio recording permission decoupled from Android manifest.
expo-notifications ~0.32.17 ~57.0.18 Push Engine Registered via config plugin; deprecated root notification dropped; sub.remove().
expo-splash-screen ~31.0.12 ~57.0.9 App Lifecycle Plugin relocated to module; synchronous SplashScreen.hide() supported.
expo-file-system ~19.0.22 ~57.0.7 File I/O Modern FileSystem API; replaces deprecated cacheDirectory with Paths.cache.uri.
expo-secure-store ~15.0.8 ~57.0.4 Cryptography Keychain / KeyStore AES-256 encrypted storage TurboModule.
zustand ^5.0.9 ^5.0.15 State Management Compatible with React 19.2.3 concurrency and pure Bridgeless execution.
@tanstack/react-query ^5.100.14 ^5.102.8 Server Cache Stale-while-revalidate caching; zero TypeScript diagnostics under TS 5.9+.
eslint-config-expo ~10.0.0 ~57.0.2 Tooling Aligned to Expo SDK 57 flat configuration rules.

10.2 Architectural Breaking Changes & Concrete Refactoring

The SDK 57 upgrade addressed several critical runtime and compilation breaking changes across the codebase:

  • Decoupled Expo Router & useIsFocused (10 Code Locations): In SDK 57, Expo Router decoupled itself from @react-navigation/*. Direct imports of useIsFocused from @react-navigation/native bypassed Expo Router's route state, causing desynchronization. All 10 application files (ChargingSessionScreen, SetupScreen, QRScanScreen, StationMapScreen, useActiveSessionItem, useChargingSession, useAppActive, HomeStationMap, useStationMap, StationLocationMap) were repointed to import { useIsFocused } from "expo-router".
  • Android View Flattening & Map Marker Stabilization: On Android Fabric, Google Maps captures canvas software snapshots of child views inside <Marker>. Fabric's View Flattening aggressively optimized away intermediate <View> wrappers, collapsing them to 0x0 pixels. Adding collapsable={false} to StationMarker.tsx forces Android view generation, restoring charging pins on Google Maps.
  • StoppingChargeOverlay Modal Restructure & In-Tree Fabric Port: Native React Native <Modal> spawns an isolated secondary native Window (android.app.Dialog on Android, UIWindow on iOS), breaking Android 12+ Fabric expo-blur (RenderEffect / dimezisBlurView) due to cross-window surface isolation, and producing vertical offset errors under Android 15 edge-to-edge insets. StoppingChargeOverlay was refactored into an in-tree full-screen absolute viewport overlay (StyleSheet.absoluteFillObject, zIndex: 9999, elevation: 9999) mounted as a sibling to <SafeAreaView> in ChargingSessionScreen.tsx. It integrates dual-layer anti-fog contrast scrimming, dead-centered card geometry (minWidth: 220, maxWidth: 240, rounded-3xl, Lottie spinner), full pointer event trapping, and atomic navigation lockdown (BackHandler, gestureEnabled: false, beforeRemove interception).
  • Modern FileSystem Migration: Refactored (FileSystem as any).cacheDirectory in SettingsScreen.tsx to the official Paths.cache.uri API from expo-file-system ~57.0.7.
  • TypeScript Compilation Integrity: Restored missing const unitPrice = unitPriceStr ? Number(unitPriceStr) : undefined; at hooks/useChargingSession.ts:59, ensuring npm run typecheck exits 0 without errors.

10.3 Mobile OS Deployment Baselines & Rollback Protocol

Operating System Baselines:
  • iOS Baseline: iOS 16.4+ (drops iOS 15.x support); requires macOS 13.4+ and Xcode 26+.
  • Android Baseline: Android 15 (API Level 35) with mandatory edge-to-edge window drawing. Status bar background setters are no-ops; safe areas are managed via react-native-safe-area-context.

In the event of an unforeseen production blocker on physical hardware, rollback to the SDK 54 baseline is achieved via:

# 1. Revert package configs and working tree
git checkout main -- package.json pnpm-lock.yaml app.json app.config.ts
git checkout main -- components/ hooks/ screens/

# 2. Reinstall frozen lockfile
pnpm install --frozen-lockfile

# 3. Clean Continuous Native Generation
npx expo prebuild --clean

# 4. Diagnostic verification
npx expo-doctor && npm run typecheck