Files
SeaGateway-App/CauTrucDuAn.md
2025-11-06 17:45:03 +07:00

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:

  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

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:

  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

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:
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
  }
};
  1. 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:

  1. State management:
const [gpsData, setGpsData] = useState<Model.GPSResponse | null>(null);
  1. Fetch GPS data:
const getGpsData = async () => {
  const response = await fetchGpsData();
  setGpsData(response.data);
};
  1. 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:

  1. Logout:
<ThemedView
  onTouchEnd={() => {
    removeStorageItem(TOKEN); // Xóa token
    router.replace("/auth/login"); // Về login
  }}
>
  <ThemedText>Đăng xuất</ThemedText>
</ThemedView>
  1. 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:

  • 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:

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 i trong terminal
  • Android Emulator: Press a trong 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


Document được tạo ngày: October 30, 2025
Version: 1.0.0
Author: GitHub Copilot Analysis