diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index e69de29..93f0b5b 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -0,0 +1,53 @@
+import { Tabs } from "expo-router";
+import { Ionicons } from "@expo/vector-icons";
+import { Button, Alert } from "react-native";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { useRouter } from "expo-router";
+
+export default function TabsLayout() {
+ const router = useRouter();
+
+ const handleLogout = async () => {
+ Alert.alert("Đăng xuất", "Bạn có chắc muốn đăng xuất?", [
+ {
+ text: "Hủy",
+ style: "cancel",
+ },
+ {
+ text: "Đăng xuất",
+ style: "destructive",
+ onPress: async () => {
+ await AsyncStorage.removeItem("authToken");
+ router.replace("/"); // quay về màn hình đăng nhập
+ },
+ },
+ ]);
+ };
+
+ return (
+
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ headerRight: () => ,
+ }}
+ />
+
+ );
+}
diff --git a/app/(tabs)/account.tsx b/app/(tabs)/account.tsx
index e69de29..5d1ad1e 100644
--- a/app/(tabs)/account.tsx
+++ b/app/(tabs)/account.tsx
@@ -0,0 +1,9 @@
+import { View, Text } from "react-native";
+
+export default function HomeScreen() {
+ return (
+
+ Chào mừng đến Tài khoản!
+
+ );
+}
diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx
new file mode 100644
index 0000000..8d14b13
--- /dev/null
+++ b/app/(tabs)/home.tsx
@@ -0,0 +1,9 @@
+import { View, Text } from "react-native";
+
+export default function HomeScreen() {
+ return (
+
+ Chào mừng đến Trang chủ!
+
+ );
+}
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/app/(tabs)/products.tsx b/app/(tabs)/products.tsx
index e69de29..55fd12a 100644
--- a/app/(tabs)/products.tsx
+++ b/app/(tabs)/products.tsx
@@ -0,0 +1,9 @@
+import { View, Text } from "react-native";
+
+export default function HomeScreen() {
+ return (
+
+ Chào mừng đến Sản phẩm!
+
+ );
+}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index e69de29..006f56b 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -0,0 +1,27 @@
+import { Stack, router } from "expo-router";
+import { useEffect, useState } from "react";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
+export default function RootLayout() {
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ const token = await AsyncStorage.getItem("authToken");
+ setIsLoggedIn(!!token);
+ setIsLoading(false);
+ };
+ checkAuth();
+ }, []);
+
+ if (isLoading) return null;
+
+ return (
+
+ {!isLoggedIn && }
+ {isLoggedIn && }
+
+ );
+
+}
diff --git a/app/index.tsx b/app/index.tsx
new file mode 100644
index 0000000..1b90385
--- /dev/null
+++ b/app/index.tsx
@@ -0,0 +1,72 @@
+import { useState } from "react";
+import { View, TextInput, Button, Text, Alert, ScrollView } from "react-native";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { useRouter } from "expo-router";
+import { login, register } from "@/services/auth";
+
+export default function AuthScreen() {
+ const router = useRouter();
+ const [mode, setMode] = useState<"login" | "register">("register");
+
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const handleSubmit = async () => {
+ if (!email.trim() || !password.trim()) {
+ Alert.alert("Lỗi", "Vui lòng nhập email và mật khẩu");
+ return;
+ }
+
+ try {
+ let res;
+ if (mode === "register") {
+ res = await register(email, password);
+ } else {
+ res = await login(email, password);
+ }
+
+ await AsyncStorage.setItem("authToken", res.token);
+ router.replace("/(tabs)/home");
+ } catch (error) {
+ console.log(error);
+ Alert.alert("Lỗi", "Sai thông tin đăng nhập hoặc server lỗi");
+ }
+ };
+
+ return (
+
+
+ {mode === "register" ? "Đăng ký tài khoản" : "Đăng nhập"}
+
+
+
+
+
+
+
+ setMode(mode === "register" ? "login" : "register")}
+ >
+ {mode === "register" ? "Đã có tài khoản? Đăng nhập" : "Chưa có tài khoản? Đăng ký"}
+
+
+ );
+}
+
+const styles = {
+ input: { borderWidth: 1, marginBottom: 10, padding: 10, borderRadius: 5 },
+};
diff --git a/package-lock.json b/package-lock.json
index 732bd3c..b321a5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^15.0.3",
+ "@react-native-async-storage/async-storage": "^1.22.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -2670,6 +2671,17 @@
}
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "1.22.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz",
+ "integrity": "sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA==",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.60 <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",
@@ -7504,6 +7516,14 @@
"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==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -8420,6 +8440,17 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
+ "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==",
+ "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",
diff --git a/package.json b/package.json
index 240f96e..2f79edb 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
+ "@react-native-async-storage/async-storage": "^1.22.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -31,17 +32,17 @@
"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-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/services/auth.ts b/services/auth.ts
index e69de29..75a1477 100644
--- a/services/auth.ts
+++ b/services/auth.ts
@@ -0,0 +1,34 @@
+const API_URL = "http://192.168.2.141:8080/api/auth";
+
+export async function register(email: string, password: string) {
+ const response = await fetch(`${API_URL}/register`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+
+ if (!response.ok) throw new Error("Đăng ký thất bại");
+ return await response.json();
+}
+
+export async function login(email: string, password: string) {
+ const response = await fetch(`${API_URL}/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+
+ if (!response.ok) throw new Error("Đăng nhập thất bại");
+ return await response.json();
+}
+
+export async function logout(token: string) {
+ const response = await fetch(`${API_URL}/logout`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return response.ok;
+}