From 79959a3050c97539460643dedeb6722f6e50ab42 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Wed, 29 Oct 2025 17:47:45 +0700 Subject: [PATCH] initial: add login --- .gitignore | 36 ++++++ app/(tabs)/_layout.tsx | 38 ++++-- app/(tabs)/index.tsx | 69 ++++++---- app/(tabs)/setting.tsx | 71 +++++++++++ app/_layout.tsx | 56 +++++++-- app/auth/login.tsx | 235 +++++++++++++++++++++++++++++++++++ app/index.tsx | 5 + config/auth.ts | 28 +++++ config/axios.ts | 70 +++++++++++ config/index.ts | 3 + config/toast.ts | 79 ++++++++++++ constants/index.ts | 30 +++++ controller/AuthController.ts | 7 ++ controller/index.ts | 3 + controller/typings.d.ts | 9 ++ package-lock.json | 138 ++++++++++++++++++-- package.json | 11 +- utils/storage.ts | 30 +++++ utils/token.ts | 13 ++ 19 files changed, 864 insertions(+), 67 deletions(-) create mode 100644 app/(tabs)/setting.tsx create mode 100644 app/auth/login.tsx create mode 100644 app/index.tsx create mode 100644 config/auth.ts create mode 100644 config/axios.ts create mode 100644 config/index.ts create mode 100644 config/toast.ts create mode 100644 constants/index.ts create mode 100644 controller/AuthController.ts create mode 100644 controller/index.ts create mode 100644 controller/typings.d.ts create mode 100644 utils/storage.ts create mode 100644 utils/token.ts diff --git a/.gitignore b/.gitignore index f8c6c2e..dbe0d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,39 @@ app-example # generated native folders /ios /android + +# IDE & Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +*.sublime-project +*.sublime-workspace + +# Testing & Coverage +coverage/ +.nyc_output/ +test-results/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.development +.env.production +.env.test + +# OS generated files +Thumbs.db +ehthumbs.db + +# Temporary files +tmp/ +temp/ +*.tmp + diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 54e11d0..12493c0 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,10 +1,10 @@ -import { Tabs } from 'expo-router'; -import React from 'react'; +import { Tabs } from "expo-router"; +import React from "react"; -import { HapticTab } from '@/components/haptic-tab'; -import { IconSymbol } from '@/components/ui/icon-symbol'; -import { Colors } from '@/constants/theme'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { HapticTab } from "@/components/haptic-tab"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; +import { useColorScheme } from "@/hooks/use-color-scheme"; export default function TabLayout() { const colorScheme = useColorScheme(); @@ -12,22 +12,36 @@ export default function TabLayout() { return ( + }} + > , + title: "Home", + tabBarIcon: ({ color }) => ( + + ), }} /> , + title: "Explore", + tabBarIcon: ({ color }) => ( + + ), + }} + /> + ( + + ), }} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 786b736..17675f1 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,38 +1,42 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; +import { Image } from "expo-image"; +import { Platform, StyleSheet, TouchableOpacity } from "react-native"; -import { HelloWave } from '@/components/hello-wave'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { Link } from 'expo-router'; +import { HelloWave } from "@/components/hello-wave"; +import ParallaxScrollView from "@/components/parallax-scroll-view"; +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { Text } from "@react-navigation/elements"; +import { Link, useRouter } from "expo-router"; export default function HomeScreen() { + const router = useRouter(); return ( - }> + } + > - Welcome! + Nicce! Step 1: Try it - Edit app/(tabs)/index.tsx to see changes. - Press{' '} + Edit{" "} + app/(tabs)/index.tsx{" "} + to see changes. Press{" "} {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', + ios: "cmd + d", + android: "cmd + m", + web: "F12", })} - {' '} + {" "} to open developer tools. @@ -43,18 +47,22 @@ export default function HomeScreen() { - alert('Action pressed')} /> + alert("Action pressed")} + /> alert('Share pressed')} + onPress={() => alert("Share pressed")} /> alert('Delete pressed')} + onPress={() => alert("Delete pressed")} /> @@ -68,20 +76,29 @@ export default function HomeScreen() { Step 3: Get a fresh start {`When you're ready, run `} - npm run reset-project to get a fresh{' '} - app directory. This will move the current{' '} - app to{' '} + + npm run reset-project + {" "} + to get a fresh app{" "} + directory. This will move the current{" "} + app to{" "} app-example. + router.replace("/auth/login")} + // disabled={loading} + > + Đăng nhập + ); } const styles = StyleSheet.create({ titleContainer: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", gap: 8, }, stepContainer: { @@ -93,6 +110,6 @@ const styles = StyleSheet.create({ width: 290, bottom: 0, left: 0, - position: 'absolute', + position: "absolute", }, }); diff --git a/app/(tabs)/setting.tsx b/app/(tabs)/setting.tsx new file mode 100644 index 0000000..0efce87 --- /dev/null +++ b/app/(tabs)/setting.tsx @@ -0,0 +1,71 @@ +import { useRouter } from "expo-router"; +import { StyleSheet } from "react-native"; + +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { api } from "@/config"; +import { TOKEN } from "@/constants"; +import { removeStorageItem } from "@/utils/storage"; +import { useState } from "react"; + +type Todo = { + userId: number; + id: number; + title: string; + completed: boolean; +}; + +export default function SettingScreen() { + const router = useRouter(); + const [data, setData] = useState(null); + + // useEffect(() => { + // getData(); + // }, []); + + const getData = async () => { + try { + const response = await api.get("/todos/1"); + setData(response.data); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + + return ( + + Settings + { + removeStorageItem(TOKEN); + router.replace("/auth/login"); + }} + > + Đăng xuất + + {data && ( + + {data.title} + {data.completed} + {data.id} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "center", + padding: 20, + }, + button: { + marginTop: 20, + padding: 10, + backgroundColor: "#007AFF", + borderRadius: 5, + }, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx index f518c9b..16e6986 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,24 +1,54 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; +import { + DarkTheme, + DefaultTheme, + ThemeProvider, +} from "@react-navigation/native"; +import { Stack, useRouter } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import { useEffect } from "react"; +import "react-native-reanimated"; +import Toast from "react-native-toast-message"; -import { useColorScheme } from '@/hooks/use-color-scheme'; - -export const unstable_settings = { - anchor: '(tabs)', -}; +import { setRouterInstance } from "@/config/auth"; +import { useColorScheme } from "@/hooks/use-color-scheme"; export default function RootLayout() { const colorScheme = useColorScheme(); + const router = useRouter(); + + useEffect(() => { + setRouterInstance(router); + }, [router]); return ( - - - - + + + + + + + + ); } diff --git a/app/auth/login.tsx b/app/auth/login.tsx new file mode 100644 index 0000000..da6ae04 --- /dev/null +++ b/app/auth/login.tsx @@ -0,0 +1,235 @@ +import { ThemedText } from "@/components/themed-text"; +import { ThemedView } from "@/components/themed-view"; +import { showToastError } from "@/config"; +import { TOKEN } from "@/constants"; +import { login } from "@/controller/AuthController"; +import { + getStorageItem, + removeStorageItem, + setStorageItem, +} from "@/utils/storage"; +import { parseJwtToken } from "@/utils/token"; +import { useRouter } from "expo-router"; +import React, { useCallback, useEffect, useState } from "react"; +import { + ActivityIndicator, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; + +export default function LoginScreen() { + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + const checkLogin = useCallback(async () => { + const token = await getStorageItem(TOKEN); + if (!token) { + return; + } + const parsed = parseJwtToken(token); + console.log("Parse Token: ", parsed); + + const { exp } = parsed; + const now = Math.floor(Date.now() / 1000); + const oneHour = 60 * 60; + if (exp - now < oneHour) { + await removeStorageItem(TOKEN); + } else { + router.replace("/(tabs)"); + } + }, [router]); + + useEffect(() => { + checkLogin(); + }, [checkLogin]); + + 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); + try { + const body: Model.LoginRequestBody = { + username: username, + password: password, + }; + + const response = await login(body); + + // Nếu thành công, lưu token và chuyển sang (tabs) + console.log("Login thành công với data:", response.data); + if (response?.data.token) { + // Lưu token vào storage nếu cần (thêm logic này sau) + await setStorageItem(TOKEN, response.data.token); + console.log("Token:", response.data.token); + router.replace("/(tabs)"); + } + } catch (error) { + showToastError( + "Lỗi", + error instanceof Error ? error.message : "Đăng nhập thất bại" + ); + } finally { + setLoading(false); + } + }; + + return ( + + + + {/* Header */} + + + SGW App + + + Đăng nhập để tiếp tục + + + + {/* Form */} + + {/* Username Input */} + + Tài khoản + + + + {/* Password Input */} + + Mật khẩu + + + + {/* Login Button */} + + {loading ? ( + + ) : ( + Đăng nhập + )} + + + {/* Footer text */} + + + Chưa có tài khoản?{" "} + Đăng ký ngay + + + + + + + ); +} + +const styles = StyleSheet.create({ + scrollContainer: { + flexGrow: 1, + justifyContent: "center", + }, + container: { + flex: 1, + paddingHorizontal: 20, + justifyContent: "center", + }, + headerContainer: { + marginBottom: 40, + alignItems: "center", + }, + title: { + fontSize: 32, + fontWeight: "bold", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + opacity: 0.7, + }, + formContainer: { + gap: 16, + }, + inputGroup: { + gap: 8, + }, + label: { + fontSize: 14, + fontWeight: "600", + }, + input: { + borderWidth: 1, + borderColor: "#ddd", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 12, + fontSize: 16, + backgroundColor: "#f5f5f5", + color: "#000", + }, + loginButton: { + backgroundColor: "#007AFF", + paddingVertical: 14, + borderRadius: 8, + alignItems: "center", + marginTop: 16, + }, + loginButtonDisabled: { + opacity: 0.6, + }, + loginButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + footerContainer: { + marginTop: 16, + alignItems: "center", + }, + footerText: { + fontSize: 14, + }, + linkText: { + color: "#007AFF", + fontWeight: "600", + }, +}); diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..12fdf93 --- /dev/null +++ b/app/index.tsx @@ -0,0 +1,5 @@ +import { Redirect } from "expo-router"; + +export default function Index() { + return ; +} diff --git a/config/auth.ts b/config/auth.ts new file mode 100644 index 0000000..8794caa --- /dev/null +++ b/config/auth.ts @@ -0,0 +1,28 @@ +import { Router } from "expo-router"; + +let routerInstance: Router | null = null; + +/** + * Set router instance để dùng trong non-component context + */ +export const setRouterInstance = (router: Router) => { + routerInstance = router; +}; + +/** + * Handle 401 error - redirect to login + */ +export const handle401 = () => { + if (routerInstance) { + (routerInstance as any).replace("/login"); + } else { + console.warn("Router instance not set, cannot redirect to login"); + } +}; + +/** + * Clear router instance (optional, for cleanup) + */ +export const clearRouterInstance = () => { + routerInstance = null; +}; diff --git a/config/axios.ts b/config/axios.ts new file mode 100644 index 0000000..3e177ca --- /dev/null +++ b/config/axios.ts @@ -0,0 +1,70 @@ +import axios, { AxiosInstance } from "axios"; +import { handle401 } from "./auth"; +import { showToastError } from "./toast"; + +const codeMessage = { + 200: "The server successfully returned the requested data。", + 201: "New or modified data succeeded。", + 202: "A request has been queued in the background (asynchronous task)。", + 204: "Data deleted successfully。", + 400: "There is an error in the request sent, the server did not perform the operation of creating or modifying data。", + 401: "The user does not have permission (token, username, password is wrong) 。", + 403: "User is authorized, but access is prohibited。", + 404: "The request issued was for a non-existent record, the server did not operate。", + 406: "The requested format is not available。", + 410: "The requested resource is permanently deleted and will no longer be available。", + 422: "When creating an object, a validation error occurred。", + 500: "Server error, please check the server。", + 502: "Gateway error。", + 503: "Service unavailable, server temporarily overloaded or maintained。", + 504: "Gateway timeout。", +}; + +// Tạo instance axios với cấu hình cơ bản +const api: AxiosInstance = axios.create({ + baseURL: "http://192.168.30.102:81", // Thay bằng API thật của bạn + timeout: 10000, // Timeout 10 giây + headers: { + "Content-Type": "application/json", + }, +}); + +// Interceptor cho request (thêm token nếu cần) +api.interceptors.request.use( + (config) => { + // Thêm auth token nếu có + // const token = getTokenFromStorage(); + // if (token) { + // config.headers.Authorization = `Bearer ${token}`; + // } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Interceptor cho response (xử lý lỗi chung) +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // Xử lý lỗi chung, ví dụ: redirect login nếu 401 + const { status, statusText, data } = error.response; + + // Ưu tiên: codeMessage → backend message → statusText + const errMsg = + codeMessage[status as keyof typeof codeMessage] || + data?.message || + statusText || + "Unknown error"; + showToastError(errMsg); + if (error.response?.status === 401) { + handle401(); + } + return Promise.reject(error); + } +); + +export default api; diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 0000000..d88607d --- /dev/null +++ b/config/index.ts @@ -0,0 +1,3 @@ +export * from "./auth"; +export { default as api } from "./axios"; +export * from "./toast"; diff --git a/config/toast.ts b/config/toast.ts new file mode 100644 index 0000000..c884608 --- /dev/null +++ b/config/toast.ts @@ -0,0 +1,79 @@ +import Toast from "react-native-toast-message"; + +export enum ToastType { + SUCCESS = "success", + ERROR = "error", + WARNING = "error", // react-native-toast-message không có 'warning', dùng 'error' + INFO = "info", +} + +/** + * Success toast + */ +export const showToastSuccess = (message: string, title?: string): void => { + Toast.show({ + type: "success", + text1: title || "Success", + text2: message, + }); +}; + +/** + * Error toast + */ +export const showToastError = (message: string, title?: string): void => { + Toast.show({ + type: ToastType.ERROR, + text1: title || ToastType.ERROR, + text2: message, + }); +}; + +/** + * Info toast + */ +export const showToastInfo = (message: string, title?: string): void => { + Toast.show({ + type: ToastType.INFO, + text1: title || ToastType.INFO, + text2: message, + }); +}; + +/** + * Warning toast + */ +export const showToastWarning = (message: string, title?: string): void => { + Toast.show({ + type: ToastType.WARNING, + text1: title || ToastType.WARNING, + text2: message, + }); +}; + +/** + * Default toast + */ +export const showToastDefault = (message: string, title?: string): void => { + Toast.show({ + type: ToastType.INFO, + text1: title || ToastType.INFO, + text2: message, + }); +}; + +/** + * Custom toast với type tùy chọn + */ +export const show = ( + message: string, + type: ToastType, + title?: string +): void => { + const titleText = title || type.charAt(0).toUpperCase() + type.slice(1); + Toast.show({ + type, + text1: titleText, + text2: message, + }); +}; diff --git a/constants/index.ts b/constants/index.ts new file mode 100644 index 0000000..77464a5 --- /dev/null +++ b/constants/index.ts @@ -0,0 +1,30 @@ + +export const TOKEN = "token"; +export const BASE_URL = "https://sgw-device.gms.vn"; +export const MAP_TRACKPOINTS_ID = "ship-trackpoints"; +export const MAP_POLYLINE_BAN = "ban-polyline"; +export const MAP_POLYGON_BAN = "ban-polygon"; + +// Global Constants + +// Route Constants +export const ROUTE_LOGIN = "/login"; +export const ROUTE_HOME = "/map"; +export const ROUTE_TRIP = "/trip"; + +// API Path Constants +export const API_PATH_LOGIN = "/api/agent/login"; +export const API_PATH_ENTITIES = "/api/io/entities"; +export const API_PATH_SHIP_INFO = "/api/sgw/shipinfo"; +export const API_GET_ALL_LAYER = "/api/sgw/geojsonlist"; +export const API_GET_LAYER_INFO = "/api/sgw/geojson"; +export const API_GET_TRIP = "/api/sgw/trip"; +export const API_GET_ALARMS = "/api/io/alarms"; +export const API_UPDATE_TRIP_STATUS = "/api/sgw/tripState"; +export const API_HAUL_HANDLE = "/api/sgw/fishingLog"; +export const API_GET_GPS = "/api/sgw/gps"; +export const API_GET_FISH = "/api/sgw/fishspecies"; +export const API_UPDATE_FISHING_LOGS = "/api/sgw/fishingLog"; +export const API_SOS = "/api/sgw/sos"; +export const API_PATH_SHIP_TRACK_POINTS = "/api/sgw/trackpoints"; +export const API_GET_ALL_BANZONES = "/api/sgw/banzones"; diff --git a/controller/AuthController.ts b/controller/AuthController.ts new file mode 100644 index 0000000..700e823 --- /dev/null +++ b/controller/AuthController.ts @@ -0,0 +1,7 @@ +import { api } from "@/config"; +import { API_PATH_LOGIN } from "@/constants"; + +export async function login(body: Model.LoginRequestBody) { + console.log("Login request body:", body); + return api.post(API_PATH_LOGIN, body); +} diff --git a/controller/index.ts b/controller/index.ts new file mode 100644 index 0000000..1b281ff --- /dev/null +++ b/controller/index.ts @@ -0,0 +1,3 @@ +import * as AuthController from "./AuthController"; + +export { AuthController }; diff --git a/controller/typings.d.ts b/controller/typings.d.ts new file mode 100644 index 0000000..27e0970 --- /dev/null +++ b/controller/typings.d.ts @@ -0,0 +1,9 @@ +declare namespace Model { + interface LoginRequestBody { + username: string; + password: string; + } + interface LoginResponse { + token?: string; + } +} diff --git a/package-lock.json b/package-lock.json index d3a4eae..2ad0170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "axios": "^1.13.1", "expo": "~54.0.20", "expo-constants": "~18.0.10", "expo-font": "~14.0.9", @@ -31,6 +33,7 @@ "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-toast-message": "^2.3.3", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1" }, @@ -2871,6 +2874,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -4321,6 +4336,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4337,6 +4358,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4766,7 +4798,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5041,6 +5072,18 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -5369,6 +5412,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5447,7 +5499,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5582,7 +5633,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5592,7 +5642,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5630,7 +5679,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5643,7 +5691,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7098,6 +7145,26 @@ "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fontfaceobserver": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", @@ -7136,6 +7203,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -7246,7 +7329,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7289,7 +7371,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7442,7 +7523,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7519,7 +7599,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7532,7 +7611,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8082,6 +8160,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9062,7 +9149,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9074,6 +9160,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -10332,6 +10430,12 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10620,6 +10724,16 @@ "react-native": "*" } }, + "node_modules/react-native-toast-message": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.3.3.tgz", + "integrity": "sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", diff --git a/package.json b/package.json index ae4f9eb..73c25be 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", + "axios": "^1.13.1", "expo": "~54.0.20", "expo-constants": "~18.0.10", "expo-font": "~14.0.9", @@ -31,17 +33,18 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-worklets": "0.5.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0" + "react-native-toast-message": "^2.3.3", + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@types/react": "~19.1.0", - "typescript": "~5.9.2", "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0" + "eslint-config-expo": "~10.0.0", + "typescript": "~5.9.2" }, "private": true } diff --git a/utils/storage.ts b/utils/storage.ts new file mode 100644 index 0000000..c08a937 --- /dev/null +++ b/utils/storage.ts @@ -0,0 +1,30 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export async function setStorageItem( + key: string, + value: string +): Promise { + try { + await AsyncStorage.setItem(key, value); + } catch (error) { + console.error("Error setting storage item:", error); + } +} + +export async function getStorageItem(key: string): Promise { + try { + const value = await AsyncStorage.getItem(key); + return value; + } catch (error) { + console.error("Error getting storage item:", error); + return null; + } +} + +export async function removeStorageItem(key: string): Promise { + try { + await AsyncStorage.removeItem(key); + } catch (error) { + console.error("Error removing storage item:", error); + } +} diff --git a/utils/token.ts b/utils/token.ts new file mode 100644 index 0000000..e02b1ce --- /dev/null +++ b/utils/token.ts @@ -0,0 +1,13 @@ +export function parseJwtToken(token: string) { + if (!token) return null; + + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join(''), + ); + return JSON.parse(jsonPayload); +} \ No newline at end of file