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 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:
| 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.tsx • screens/profile/ProfileScreen.tsx (ProfileUserCard, VirtualEvCard) |
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 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 | 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
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 (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.1C++ 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: trueis 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 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,ProfileUserCardresponsive identity header with modularVirtualEvCard(VirtualCard), 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.
@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:
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 (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.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 (Fabric Engine): The
useAnimatedNumberhook runs a React Native Reanimated 4.5.1 worklet powered by the standalonereact-native-worklets 0.10.1C++ engine. It is 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. Babel AST transformations are automatically injected bybabel-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. |
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:
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 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. |
- 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 byexpo-blur, dead-centered dialog card, and blocked pointer events. - Android Map Marker Fabric Stabilization: Custom marker views in
components/map-station/StationMarker.tsxexplicitly declarecollapsable={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.
| 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. |
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), 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:
- Wallet Entrypoint via VirtualEvCard: Driver views real-time EVP balance and outstanding debt points
on the modular
<VirtualEvCard />inProfileUserCard. Tapping the full-width "Nạp tiền" (Top Up) CTA navigates directly toapp/(tabs)/profile/top-up.tsx. - 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-file-system ~57.0.7using the modernPaths.cache.uriAPI and presented viaExpo 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.18declared as a native plugin inapp.json(legacy rootnotificationconfiguration dropped).NotificationServices.registerForPushNotifications()obtains the push token, registers metadata atPOST /api/v1/notifications/devices, and cleans up listeners usingsub.remove(). - 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).- Hermes v1 Engine Memory Stabilization: Early SDK 57 preview builds exhibited a memory leak regression
when exchanging high-frequency telemetry objects with
react-native-workletson the UI thread. Inexpo@~57.0.22and 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.tsxcurrently lacks a top-level ReactErrorBoundarycomponent. Introducing anAppErrorBoundarywrapper 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.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 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). |
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:
- State & Hook Lifecycle Co-location: Stopping state is intimately bound to the local
useChargingSessionhook instance, itstransactionId, and the 5-step state machine. Global mounting would require unnecessary state hoisting into a global Zustand store. - Route-Scoped Navigation Lockdown: Calling
navigation.setOptions({ gestureEnabled: false })and interceptingbeforeRemoveoperates precisely on the active charging screen stack entry, preventing route teardown without freezing navigation transitions across the rest of the application. - Coordinated Viewport Hierarchy: When
isStoppingChargeis active,ChargingSessionScreensynchronously unmountsChargingSessionBottomActionand neutralizesChargingSessionHeader.onBack, preventing duplicate stop slider interactions and phantom touch handling. - GPU & Fabric Compositor Efficiency: Keeping
expo-blurand 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
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.
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 ofuseIsFocusedfrom@react-navigation/nativebypassed Expo Router's route state, causing desynchronization. All 10 application files (ChargingSessionScreen,SetupScreen,QRScanScreen,StationMapScreen,useActiveSessionItem,useChargingSession,useAppActive,HomeStationMap,useStationMap,StationLocationMap) were repointed toimport { 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. Addingcollapsable={false}toStationMarker.tsxforces 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.Dialogon Android,UIWindowon iOS), breaking Android 12+ Fabricexpo-blur(RenderEffect/dimezisBlurView) due to cross-window surface isolation, and producing vertical offset errors under Android 15 edge-to-edge insets.StoppingChargeOverlaywas refactored into an in-tree full-screen absolute viewport overlay (StyleSheet.absoluteFillObject,zIndex: 9999,elevation: 9999) mounted as a sibling to<SafeAreaView>inChargingSessionScreen.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,beforeRemoveinterception). - Modern FileSystem Migration: Refactored
(FileSystem as any).cacheDirectoryinSettingsScreen.tsxto the officialPaths.cache.uriAPI fromexpo-file-system ~57.0.7. - TypeScript Compilation Integrity: Restored missing
const unitPrice = unitPriceStr ? Number(unitPriceStr) : undefined;athooks/useChargingSession.ts:59, ensuringnpm run typecheckexits 0 without errors.
10.3 Mobile OS Deployment Baselines & Rollback Protocol
- 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