# 📊 PHÂN TÍCH CHI TIẾT DỰ ÁN SGW-APP (SeaGateway) ## 🎯 TỔNG QUAN DỰ ÁN Đây là một ứng dụng **React Native** sử dụng **Expo Router** để quản lý thiết bị GPS trên biển (Marine/Ship tracking). Ứng dụng có các tính năng: - Đăng nhập với JWT token - Hiển thị GPS trên bản đồ (react-native-maps) - Quản lý session/authentication - Dark/Light mode --- ## 📁 PHÂN TÍCH TỪNG FILE ### **1. Configuration Files (Cấu hình)** #### `app.json` - Expo Configuration ```json { "expo": { "name": "sgw-app", "slug": "sgw-app", "scheme": "sgwapp", "newArchEnabled": true } } ``` **Chức năng:** - Cấu hình tên app, icon, splash screen - Enable tính năng mới của React Native (`newArchEnabled: true`) - Thiết lập routing scheme: `sgwapp://` - Cấu hình cho iOS, Android và Web #### `tsconfig.json` - TypeScript Config ```json { "compilerOptions": { "paths": { "@/*": ["./*"] } } } ``` **Chức năng:** - Cho phép import với alias `@/` thay vì `../../` - VD: `import { api } from "@/config"` thay vì `import { api } from "../../config"` #### `package.json` - Dependencies **Các thư viện chính:** - **expo-router**: File-based routing - **axios**: HTTP client để call API - **@react-native-async-storage/async-storage**: Lưu token local - **react-native-maps**: Hiển thị bản đồ - **react-native-toast-message**: Thông báo lỗi/thành công --- ### **2. Config Folder (Cấu hình ứng dụng)** #### `config/axios.ts` - HTTP Client Setup ```typescript const api: AxiosInstance = axios.create({ baseURL: "http://192.168.30.102:81", timeout: 10000, }); ``` **Chức năng:** 1. **Tạo axios instance** với base URL và timeout 2. **Request Interceptor**: - Lấy token từ AsyncStorage - Thêm token vào header `Authorization: ${token}` 3. **Response Interceptor**: - Xử lý lỗi HTTP (401, 404, 500...) - Hiển thị toast message lỗi - Map status code sang message Tiếng Anh **Lưu ý:** - Base URL đang hard-coded là `192.168.30.102:81` (IP local) - Token format: `Authorization: ${token}` (không có prefix "Bearer") #### `config/auth.ts` - Router Management ```typescript let routerInstance: Router | null = null; export const setRouterInstance = (router: Router) => { routerInstance = router; }; export const handle401 = () => { routerInstance?.replace("/login"); }; ``` **Chức năng:** - Lưu router instance để redirect từ non-component (axios interceptor) - `handle401()`: Redirect về login khi token hết hạn - **Hiện tại chưa được dùng** (comment trong axios.ts) #### `config/toast.ts` - Toast Notifications ```typescript export const showToastError = (message: string, title?: string): void => { Toast.show({ type: "error", text1: title || "error", text2: message, }); }; ``` **Chức năng:** - Wrapper cho `react-native-toast-message` - 4 loại toast: Success, Error, Info, Warning - Export: `showToastSuccess`, `showToastError`, `showToastInfo`, `showToastWarning` --- ### **3. Constants Folder (Hằng số)** #### `constants/index.ts` ```typescript export const TOKEN = "token"; export const BASE_URL = "https://sgw-device.gms.vn"; // API Endpoints export const API_PATH_LOGIN = "/api/agent/login"; export const API_GET_GPS = "/api/sgw/gps"; export const API_PATH_SHIP_INFO = "/api/sgw/shipinfo"; // ... nhiều API endpoints khác ``` **Chức năng:** - Lưu tất cả constants: API paths, storage keys - **Lưu ý:** `BASE_URL` ở đây khác với base URL trong axios.ts --- ### **4. Utils Folder (Tiện ích)** #### `utils/storage.ts` - AsyncStorage Wrapper ```typescript export async function setStorageItem( key: string, value: string ): Promise { await AsyncStorage.setItem(key, value); } export async function getStorageItem(key: string): Promise { return await AsyncStorage.getItem(key); } export async function removeStorageItem(key: string): Promise { await AsyncStorage.removeItem(key); } ``` **Chức năng:** - Wrapper cho AsyncStorage với error handling - Lưu/đọc/xóa dữ liệu local (chủ yếu là token) #### `utils/token.ts` - JWT Parser ```typescript export function parseJwtToken(token: string) { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64).split('').map(...) ); return JSON.parse(jsonPayload); } ``` **Chức năng:** - Parse JWT token để lấy payload (exp, userId...) - Decode base64url thành JSON object - Dùng để check token expiration --- ### **5. Controller Folder (API Calls)** #### `controller/typings.d.ts` - Type Definitions ```typescript declare namespace Model { interface LoginRequestBody { username: string; password: string; } interface LoginResponse { token?: string; } interface GPSResonse { lat: number; lon: number; s: number; // speed h: number; // heading fishing: boolean; } } ``` **Chức năng:** - Define TypeScript interfaces cho API request/response - Namespace `Model` để tránh conflict #### `controller/AuthController.ts` ```typescript export async function login(body: Model.LoginRequestBody) { return api.post(API_PATH_LOGIN, body); } ``` **Chức năng:** - Gọi API login với username/password - Return response chứa JWT token #### `controller/DeviceController.ts` ```typescript export async function fetchGpsData() { return api.get(API_GET_GPS); } ``` **Chức năng:** - Gọi API lấy GPS data của thiết bị - Return lat, lon, speed, heading, fishing status --- ### **6. App Folder (Screens & Routes)** #### `app/_layout.tsx` - Root Layout ```typescript export default function RootLayout() { const router = useRouter(); useEffect(() => { setRouterInstance(router); // Lưu router instance }, [router]); return ( ); } ``` **Chức năng:** 1. **Root layout** của toàn bộ app 2. Set router instance cho config/auth 3. Cấu hình Stack Navigator với 3 screens: - `auth/login`: Màn hình đăng nhập - `(tabs)`: Tabs navigator (Home, Explore, Settings) - `modal`: Modal demo 4. Áp dụng theme (dark/light mode) 5. Render Toast component ở root #### `app/index.tsx` - Entry Point ```typescript export default function Index() { return ; } ``` **Chức năng:** - Redirect root path `/` về `/auth/login` - Entry point của app #### `app/auth/login.tsx` - Login Screen **Luồng hoạt động:** 1. **Check existing login on mount:** ```typescript useEffect(() => { checkLogin(); // Kiểm tra token đã lưu }, []); const checkLogin = async () => { const token = await getStorageItem(TOKEN); if (!token) return; const parsed = parseJwtToken(token); const { exp } = parsed; const now = Math.floor(Date.now() / 1000); // Nếu token còn > 1 giờ → auto login if (exp - now > 3600) { router.replace("/(tabs)"); } else { await removeStorageItem(TOKEN); // Xóa token hết hạn } }; ``` 2. **Handle login submit:** ```typescript const handleLogin = async () => { // Validate input if (!username.trim() || !password.trim()) { showToastError("Lỗi", "Vui lòng nhập tài khoản và mật khẩu"); return; } setLoading(true); const body = { username, password }; const response = await login(body); if (response?.data.token) { await setStorageItem(TOKEN, response.data.token); router.replace("/(tabs)"); // Chuyển sang tabs } }; ``` **UI:** - Form đăng nhập với username/password - Loading indicator khi đang submit - KeyboardAvoidingView cho iOS - Support dark/light mode #### `app/(tabs)/_layout.tsx` - Tabs Layout ```typescript export default function TabLayout() { return ( ); } ``` **Chức năng:** - Cấu hình Bottom Tabs với 3 tabs - Mỗi tab có icon riêng - Haptic feedback khi tap tab #### `app/(tabs)/index.tsx` - Home Screen (GPS Map) **Luồng hoạt động:** 1. **State management:** ```typescript const [gpsData, setGpsData] = useState(null); ``` 2. **Fetch GPS data:** ```typescript const getGpsData = async () => { const response = await fetchGpsData(); setGpsData(response.data); }; ``` 3. **Render map:** ```typescript {gpsData && ( )} ``` **Chức năng:** - Hiển thị bản đồ với react-native-maps - Button "Get GPS Data" để fetch vị trí thiết bị - Hiển thị marker trên bản đồ khi có data #### `app/(tabs)/explore.tsx` - Explore Screen **Chức năng:** - Demo screen với Collapsible components - Giới thiệu về file-based routing, dark mode, animations - Không có logic nghiệp vụ #### `app/(tabs)/setting.tsx` - Settings Screen **Luồng hoạt động:** 1. **Logout:** ```typescript { removeStorageItem(TOKEN); // Xóa token router.replace("/auth/login"); // Về login }} > Đăng xuất ``` 2. **Demo API call:** ```typescript const getData = async () => { const response = await api.get("/todos/1"); setData(response.data); }; ``` **Chức năng:** - Nút đăng xuất (xóa token + redirect login) - Demo fetch data từ API --- ## 🔄 LUỒNG CHẠY CHI TIẾT ### **1. App Startup Flow** ``` 📱 App Launch ↓ expo-router/entry ↓ app/_layout.tsx (RootLayout) ↓ setRouterInstance(router) ← Lưu router instance ↓ Stack Navigator initialized ↓ app/index.tsx ↓ ↓ app/auth/login.tsx ``` ### **2. Login Flow (Chi tiết)** ``` 🔐 Login Screen Mount ↓ useEffect(() => checkLogin()) ↓ getStorageItem(TOKEN) ↓ ┌─────────────────┐ │ Token exists? │ └─────────────────┘ ├─ NO → Hiển thị form login │ └─ YES → parseJwtToken(token) ↓ Check exp time ↓ ┌───────────────────┐ │ Token còn > 1h? │ └───────────────────┘ ├─ YES → router.replace("/(tabs)") ← Auto login │ └─ NO → removeStorageItem(TOKEN) ← Xóa token hết hạn Hiển thị form login ``` ### **3. Submit Login Flow** ``` 👤 User nhập username/password ↓ Press "Đăng nhập" ↓ handleLogin() ↓ Validate input (username && password) ├─ Invalid → showToastError("Vui lòng nhập...") │ └─ Valid ↓ setLoading(true) ↓ login({ username, password }) ← AuthController ↓ api.post("/api/agent/login", body) ↓ ┌─ Request Interceptor ─┐ │ Thêm Authorization │ │ header (nếu có token) │ └───────────────────────┘ ↓ Call API backend ↓ ┌─ Response Interceptor ─┐ │ Check status code │ │ 401 → handle401() │ │ 500 → showToastError() │ └────────────────────────┘ ↓ ┌──────────────────┐ │ Response success?│ └──────────────────┘ ├─ NO → showToastError(error.message) │ setLoading(false) │ └─ YES → Kiểm tra response.data.token ├─ Token exists? │ ├─ YES │ │ ↓ │ │ setStorageItem(TOKEN, token) │ │ ↓ │ │ router.replace("/(tabs)") │ │ ↓ │ │ app/(tabs)/_layout.tsx │ │ │ └─ NO → Không làm gì │ └─ setLoading(false) ``` ### **4. Tabs Navigation Flow** ``` 📱 app/(tabs)/_layout.tsx ↓ Bottom Tabs Navigator ├─ Tab 1: index.tsx (Home/GPS Map) ├─ Tab 2: explore.tsx (Explore) └─ Tab 3: setting.tsx (Settings) ``` ### **5. GPS Data Fetch Flow** ``` 🗺️ Home Screen (index.tsx) ↓ User tap "Get GPS Data" button ↓ getGpsData() ↓ fetchGpsData() ← DeviceController ↓ api.get("/api/sgw/gps") ↓ ┌─ Request Interceptor ─┐ │ getStorageItem(TOKEN) │ │ Add Authorization │ └───────────────────────┘ ↓ Call API backend ↓ ┌─ Response ─┐ │ { │ │ lat, │ │ lon, │ │ s, │ │ h, │ │ fishing │ │ } │ └────────────┘ ↓ setGpsData(response.data) ↓ Re-render MapView ↓ ``` ### **6. Logout Flow** ``` ⚙️ Settings Screen ↓ User tap "Đăng xuất" ↓ removeStorageItem(TOKEN) ← Xóa token khỏi AsyncStorage ↓ router.replace("/auth/login") ↓ Back to Login Screen ``` --- ## 🔧 CƠ CHẾ HOẠT ĐỘNG CHI TIẾT ### **1. Authentication Mechanism** ``` Token Storage: AsyncStorage (local device storage) Key: "token" Value: JWT string Token Format: Authorization: ← Không có "Bearer" prefix Token Validation: 1. Parse JWT → lấy exp (expiration time) 2. So sánh với thời gian hiện tại 3. Nếu còn > 1 giờ → valid 4. Nếu < 1 giờ → expired → xóa token ``` ### **2. API Request Flow với Interceptors** ``` ┌───────────────────────────────────────┐ │ APPLICATION CODE │ │ api.post("/api/login", body) │ └───────────────┬───────────────────────┘ ↓ ┌───────────────────────────────────────┐ │ REQUEST INTERCEPTOR │ │ 1. getStorageItem("token") │ │ 2. config.headers.Authorization │ │ = token │ └───────────────┬───────────────────────┘ ↓ ┌───────────────────────────────────────┐ │ NETWORK REQUEST │ │ POST http://192.168.30.102:81/... │ │ Headers: { Authorization: } │ └───────────────┬───────────────────────┘ ↓ ┌───────────────────────────────────────┐ │ BACKEND SERVER │ │ Validate token → Process request │ └───────────────┬───────────────────────┘ ↓ ┌───────────────────────────────────────┐ │ RESPONSE INTERCEPTOR │ │ 1. Check status code │ │ 2. If error: │ │ - Map to message │ │ - showToastError() │ │ - If 401: handle401() │ └───────────────┬───────────────────────┘ ↓ ┌───────────────────────────────────────┐ │ APPLICATION CODE │ │ Handle response.data │ └───────────────────────────────────────┘ ``` ### **3. Routing Mechanism (Expo Router)** ``` File System Routes ─────────────────────────────────────── app/ ├─ index.tsx → / ├─ _layout.tsx → (Root layout) ├─ auth/ │ └─ login.tsx → /auth/login ├─ (tabs)/ │ ├─ _layout.tsx → (Tabs layout) │ ├─ index.tsx → /(tabs) │ ├─ explore.tsx → /(tabs)/explore │ └─ setting.tsx → /(tabs)/setting └─ modal.tsx → /modal ``` ### **4. State Management** Dự án **KHÔNG dùng Redux/MobX**, chỉ dùng: - `useState` cho local state - `AsyncStorage` cho persistent storage (token) - Props drilling (không nhiều level) ### **5. Error Handling Strategy** ``` Levels of Error Handling: ───────────────────────────────────── 1. Component Level (try-catch): try { await login(body); } catch (error) { showToastError(error.message); } 2. Axios Response Interceptor: - Catch ALL HTTP errors - Map status code → message - Show toast automatically 3. Utils Error Handling: - Storage errors → console.error - Token parse errors → return null ``` --- ## 🎯 KẾT LUẬN ### **Điểm Mạnh:** ✅ Cấu trúc rõ ràng (config, controller, utils, app) ✅ TypeScript với type definitions đầy đủ ✅ Axios interceptors xử lý auth tự động ✅ JWT token validation với expiration check ✅ Toast notifications thống nhất ✅ File-based routing với Expo Router ✅ Support dark/light mode ### **Điểm Cần Cải Thiện:** ⚠️ Base URL hard-coded (nên dùng env variables) ⚠️ `handle401()` chưa được enable trong axios ⚠️ Thiếu refresh token mechanism ⚠️ Không có loading state global ⚠️ Constants có 2 BASE_URL khác nhau (confusing) ⚠️ GPS data fetch chưa tự động (phải tap button) ### **Tech Stack Summary:** - **Framework:** React Native + Expo - **Navigation:** Expo Router (file-based) - **HTTP Client:** Axios với interceptors - **Storage:** AsyncStorage - **Maps:** react-native-maps - **UI:** Custom themed components - **Notifications:** react-native-toast-message --- ## 📋 CẤU TRÚC THƯ MỤC ``` SeaGateway-App/ ├── app/ # Screens & Routes (Expo Router) │ ├── _layout.tsx # Root layout + Stack Navigator │ ├── index.tsx # Entry point (redirect to login) │ ├── modal.tsx # Modal demo screen │ ├── (tabs)/ # Tab Navigator Group │ │ ├── _layout.tsx # Tabs layout config │ │ ├── index.tsx # Home screen (GPS Map) │ │ ├── explore.tsx # Explore screen (demo) │ │ └── setting.tsx # Settings screen (logout) │ └── auth/ │ └── login.tsx # Login screen │ ├── assets/ # Static resources │ └── images/ # Images, icons, splash │ ├── components/ # Reusable UI components │ ├── themed-text.tsx # Text với theme support │ ├── themed-view.tsx # View với theme support │ ├── haptic-tab.tsx # Tab với haptic feedback │ ├── parallax-scroll-view.tsx # Parallax scroll │ └── ui/ # UI components │ ├── collapsible.tsx │ └── icon-symbol.tsx │ ├── config/ # App configuration │ ├── index.ts # Export all configs │ ├── axios.ts # Axios instance + interceptors │ ├── auth.ts # Router management cho auth │ └── toast.ts # Toast notification helpers │ ├── constants/ # Constants & API paths │ ├── index.ts # API endpoints, storage keys │ └── theme.ts # Theme colors │ ├── controller/ # API Controllers (Business Logic) │ ├── index.ts # Export all controllers │ ├── typings.d.ts # TypeScript type definitions │ ├── AuthController.ts # Auth APIs (login) │ └── DeviceController.ts # Device APIs (GPS data) │ ├── hooks/ # Custom React hooks │ ├── use-color-scheme.ts # Detect dark/light mode │ └── use-theme-color.ts # Get theme colors │ ├── utils/ # Utility functions │ ├── storage.ts # AsyncStorage wrapper │ └── token.ts # JWT token parser │ ├── scripts/ # Build/dev scripts │ └── reset-project.js # Reset project script │ ├── app.json # Expo configuration ├── package.json # Dependencies ├── tsconfig.json # TypeScript config └── .gitignore # Git ignore rules ``` --- ## 🔑 CÁC FILE QUAN TRỌNG ### **1. Entry Points:** - `app/_layout.tsx` - Root của toàn bộ app - `app/index.tsx` - Redirect đầu tiên - `package.json` - "main": "expo-router/entry" ### **2. Authentication Core:** - `app/auth/login.tsx` - Login UI + logic - `config/auth.ts` - Router management - `config/axios.ts` - Token injection - `utils/token.ts` - JWT parsing - `utils/storage.ts` - Token storage ### **3. API Layer:** - `config/axios.ts` - HTTP client - `controller/AuthController.ts` - Login API - `controller/DeviceController.ts` - GPS API - `constants/index.ts` - API endpoints ### **4. Main Features:** - `app/(tabs)/index.tsx` - GPS Map - `app/(tabs)/setting.tsx` - Logout - `components/themed-*.tsx` - Theme components --- ## 🚀 HƯỚNG DẪN CHẠY DỰ ÁN ### **1. Cài đặt dependencies:** ```bash npm install # hoặc yarn install ``` ### **2. Chạy development server:** ```bash npm start # hoặc npx expo start ``` ### **3. Chạy trên thiết bị:** - **iOS Simulator:** Press `i` trong terminal - **Android Emulator:** Press `a` trong terminal - **Physical Device:** Scan QR code với Expo Go app ### **4. Build production:** ```bash # iOS npx expo run:ios # Android npx expo run:android ``` --- ## 📝 LƯU Ý KHI PHÁT TRIỂN ### **1. Thêm API mới:** ```typescript // 1. Define type trong controller/typings.d.ts declare namespace Model { interface NewAPIResponse { data: string; } } // 2. Thêm endpoint trong constants/index.ts export const API_NEW_ENDPOINT = "/api/new/endpoint"; // 3. Tạo controller function // controller/NewController.ts export async function fetchNewData() { return api.get(API_NEW_ENDPOINT); } ``` ### **2. Thêm screen mới:** ```typescript // 1. Tạo file trong app/ // app/new-screen.tsx export default function NewScreen() { return ...; } // 2. Route tự động: /new-screen // 3. Navigate: router.push("/new-screen") ``` ### **3. Environment Variables:** **Nên tạo file `.env`:** ```bash API_BASE_URL=http://192.168.30.102:81 API_TIMEOUT=10000 ``` **Sử dụng:** ```typescript import Constants from "expo-constants"; const api = axios.create({ baseURL: Constants.expoConfig?.extra?.apiUrl, }); ``` ### **4. Debugging Tips:** - Check console logs trong terminal (metro bundler) - Network requests: React Native Debugger - Token issues: Log token trong axios interceptor - Routing issues: Log router navigation events --- ## 🐛 CÁC LỖI THƯỜNG GẶP & CÁCH XỬ LÝ ### **1. Token expired không redirect:** **Nguyên nhân:** `handle401()` bị comment **Giải pháp:** Uncomment dòng `handle401()` trong `config/axios.ts` ### **2. Map không hiển thị:** **Nguyên nhân:** - Chưa config Google Maps API key - MapView `mapType="none"` (không có tile) **Giải pháp:** ```typescript // Thay vì "none" ``` ### **3. AsyncStorage warning:** **Nguyên nhân:** AsyncStorage deprecated trong React Native core **Giải pháp:** Đã dùng `@react-native-async-storage/async-storage` ✅ ### **4. Build failed trên iOS:** **Nguyên nhân:** Chưa install CocoaPods **Giải pháp:** ```bash cd ios && pod install ``` --- ## 🔐 BẢO MẬT & BEST PRACTICES ### **1. Token Security:** ✅ Lưu trong AsyncStorage (secure trên device) ✅ Check expiration trước khi dùng ❌ Không có refresh token (nên thêm) ❌ Không encrypt token (AsyncStorage không encrypted mặc định) ### **2. API Security:** ✅ HTTPS cho production (BASE_URL: https://...) ❌ Hard-coded credentials (không có trong code ✅) ⚠️ IP local trong axios base URL (đổi thành domain cho prod) ### **3. Code Security:** ✅ TypeScript strict mode ✅ Try-catch cho async operations ✅ Validate input trước khi gửi API ❌ Không có rate limiting (phụ thuộc backend) --- ## 📚 TÀI LIỆU THAM KHẢO - **Expo Router:** https://docs.expo.dev/router/introduction/ - **React Native:** https://reactnative.dev/ - **Axios:** https://axios-http.com/ - **React Native Maps:** https://github.com/react-native-maps/react-native-maps - **AsyncStorage:** https://react-native-async-storage.github.io/async-storage/ --- _Document được tạo ngày: October 30, 2025_ _Version: 1.0.0_ _Author: GitHub Copilot Analysis_