26 KiB
📊 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
{
"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
{
"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
const api: AxiosInstance = axios.create({
baseURL: "http://192.168.30.102:81",
timeout: 10000,
});
Chức năng:
- Tạo axios instance với base URL và timeout
- Request Interceptor:
- Lấy token từ AsyncStorage
- Thêm token vào header
Authorization: ${token}
- 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
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
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
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
export async function setStorageItem(
key: string,
value: string
): Promise<void> {
await AsyncStorage.setItem(key, value);
}
export async function getStorageItem(key: string): Promise<string | null> {
return await AsyncStorage.getItem(key);
}
export async function removeStorageItem(key: string): Promise<void> {
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
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
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
export async function login(body: Model.LoginRequestBody) {
return api.post<Model.LoginResponse>(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
export async function fetchGpsData() {
return api.get<Model.GPSResponse>(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
export default function RootLayout() {
const router = useRouter();
useEffect(() => {
setRouterInstance(router); // Lưu router instance
}, [router]);
return (
<Stack initialRouteName="auth/login">
<Stack.Screen name="auth/login" />
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" />
</Stack>
);
}
Chức năng:
- Root layout của toàn bộ app
- Set router instance cho config/auth
- 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
- Áp dụng theme (dark/light mode)
- Render Toast component ở root
app/index.tsx - Entry Point
export default function Index() {
return <Redirect href="/auth/login" />;
}
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:
- Check existing login on mount:
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
}
};
- Handle login submit:
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
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="explore" options={{ title: "Explore" }} />
<Tabs.Screen name="setting" options={{ title: "Setting" }} />
</Tabs>
);
}
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:
- State management:
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
- Fetch GPS data:
const getGpsData = async () => {
const response = await fetchGpsData();
setGpsData(response.data);
};
- Render map:
<MapView
initialRegion={{
latitude: 15.70581,
longitude: 116.152685,
latitudeDelta: 2,
longitudeDelta: 2,
}}
>
{gpsData && (
<Marker
coordinate={{ latitude: gpsData.lat, longitude: gpsData.lon }}
title="Device Location"
/>
)}
</MapView>
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:
- Logout:
<ThemedView
onTouchEnd={() => {
removeStorageItem(TOKEN); // Xóa token
router.replace("/auth/login"); // Về login
}}
>
<ThemedText>Đăng xuất</ThemedText>
</ThemedView>
- Demo API call:
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
↓
<Redirect href="/auth/login" />
↓
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
↓
<Marker coordinate={{ lat, lon }} />
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: <token> ← 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: <token> } │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 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:
useStatecho local stateAsyncStoragecho 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ộ appapp/index.tsx- Redirect đầu tiênpackage.json- "main": "expo-router/entry"
2. Authentication Core:
app/auth/login.tsx- Login UI + logicconfig/auth.ts- Router managementconfig/axios.ts- Token injectionutils/token.ts- JWT parsingutils/storage.ts- Token storage
3. API Layer:
config/axios.ts- HTTP clientcontroller/AuthController.ts- Login APIcontroller/DeviceController.ts- GPS APIconstants/index.ts- API endpoints
4. Main Features:
app/(tabs)/index.tsx- GPS Mapapp/(tabs)/setting.tsx- Logoutcomponents/themed-*.tsx- Theme components
🚀 HƯỚNG DẪN CHẠY DỰ ÁN
1. Cài đặt dependencies:
npm install
# hoặc
yarn install
2. Chạy development server:
npm start
# hoặc
npx expo start
3. Chạy trên thiết bị:
- iOS Simulator: Press
itrong terminal - Android Emulator: Press
atrong terminal - Physical Device: Scan QR code với Expo Go app
4. Build production:
# iOS
npx expo run:ios
# Android
npx expo run:android
📝 LƯU Ý KHI PHÁT TRIỂN
1. Thêm API mới:
// 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<Model.NewAPIResponse>(API_NEW_ENDPOINT);
}
2. Thêm screen mới:
// 1. Tạo file trong app/
// app/new-screen.tsx
export default function NewScreen() {
return <ThemedView>...</ThemedView>;
}
// 2. Route tự động: /new-screen
// 3. Navigate: router.push("/new-screen")
3. Environment Variables:
Nên tạo file .env:
API_BASE_URL=http://192.168.30.102:81
API_TIMEOUT=10000
Sử dụng:
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:
<MapView mapType="standard" /> // 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:
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