1047 lines
26 KiB
Markdown
1047 lines
26 KiB
Markdown
# 📊 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<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
|
|
|
|
```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<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`
|
|
|
|
```typescript
|
|
export async function fetchGpsData() {
|
|
return api.get<Model.GPSResonse>(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 (
|
|
<Stack initialRouteName="auth/login">
|
|
<Stack.Screen name="auth/login" />
|
|
<Stack.Screen name="(tabs)" />
|
|
<Stack.Screen name="modal" />
|
|
</Stack>
|
|
);
|
|
}
|
|
```
|
|
|
|
**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 <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:**
|
|
|
|
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 (
|
|
<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:**
|
|
|
|
1. **State management:**
|
|
|
|
```typescript
|
|
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(null);
|
|
```
|
|
|
|
2. **Fetch GPS data:**
|
|
|
|
```typescript
|
|
const getGpsData = async () => {
|
|
const response = await fetchGpsData();
|
|
setGpsData(response.data);
|
|
};
|
|
```
|
|
|
|
3. **Render map:**
|
|
|
|
```typescript
|
|
<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:**
|
|
|
|
1. **Logout:**
|
|
|
|
```typescript
|
|
<ThemedView
|
|
onTouchEnd={() => {
|
|
removeStorageItem(TOKEN); // Xóa token
|
|
router.replace("/auth/login"); // Về login
|
|
}}
|
|
>
|
|
<ThemedText>Đăng xuất</ThemedText>
|
|
</ThemedView>
|
|
```
|
|
|
|
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
|
|
↓
|
|
<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:
|
|
|
|
- `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<Model.NewAPIResponse>(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 <ThemedView>...</ThemedView>;
|
|
}
|
|
|
|
// 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
|
|
<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:**
|
|
|
|
```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_
|