diff --git a/CauTrucDuAn.md b/CauTrucDuAn.md new file mode 100644 index 0000000..f0ae7b0 --- /dev/null +++ b/CauTrucDuAn.md @@ -0,0 +1,1046 @@ +# 📊 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_ diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 12493c0..23f86f2 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -29,7 +29,7 @@ export default function TabLayout() { ( ),