439 lines
12 KiB
TypeScript
439 lines
12 KiB
TypeScript
import EnIcon from "@/assets/icons/en_icon.png";
|
|
import VnIcon from "@/assets/icons/vi_icon.png";
|
|
import RotateSwitch from "@/components/rotate-switch";
|
|
import ScanQRCode from "@/components/ScanQRCode";
|
|
import { ThemedText } from "@/components/themed-text";
|
|
import { ThemedView } from "@/components/themed-view";
|
|
import SliceSwitch from "@/components/ui/slice-switch";
|
|
import { DOMAIN, TOKEN } from "@/constants";
|
|
import { login } from "@/controller/AuthController";
|
|
import { useI18n } from "@/hooks/use-i18n";
|
|
import { showErrorToast, showWarningToast } from "@/services/toast_service";
|
|
import {
|
|
getStorageItem,
|
|
removeStorageItem,
|
|
setStorageItem,
|
|
} from "@/utils/storage";
|
|
import { parseJwtToken } from "@/utils/token";
|
|
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
|
|
import { useRouter } from "expo-router";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Image,
|
|
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 [showPassword, setShowPassword] = useState(false);
|
|
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
|
|
const { t, setLocale, locale } = useI18n();
|
|
const [isVNLang, setIsVNLang] = useState(false);
|
|
|
|
const checkLogin = useCallback(async () => {
|
|
const token = await getStorageItem(TOKEN);
|
|
const domain = await getStorageItem(DOMAIN);
|
|
// console.log("Token:", token);
|
|
// removeStorageItem(DOMAIN);
|
|
console.log("Domain:", domain);
|
|
if (!token) {
|
|
return;
|
|
}
|
|
if (!domain) {
|
|
return;
|
|
}
|
|
const parsed = parseJwtToken(token);
|
|
|
|
const { exp } = parsed;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const oneHour = 60 * 60;
|
|
if (exp - now < oneHour) {
|
|
await removeStorageItem(TOKEN);
|
|
await removeStorageItem(DOMAIN);
|
|
} else {
|
|
router.replace("/(tabs)");
|
|
}
|
|
}, [router]);
|
|
|
|
useEffect(() => {
|
|
setIsVNLang(locale === "vi");
|
|
}, [locale]);
|
|
|
|
useEffect(() => {
|
|
checkLogin();
|
|
}, [checkLogin]);
|
|
|
|
const handleQRCodeScanned = async (data: string) => {
|
|
console.log("QR Code Scanned Data:", data);
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
if (parsed.username && parsed.password) {
|
|
// update UI fields
|
|
setUsername(parsed.username);
|
|
setPassword(parsed.password);
|
|
console.log("Domain: ", parsed.device_ip);
|
|
|
|
// close scanner so user sees the filled form
|
|
await setStorageItem(DOMAIN, parsed.device_ip);
|
|
|
|
// // call login directly with scanned credentials to avoid waiting for state to update
|
|
await handleLogin({
|
|
username: parsed.username,
|
|
password: parsed.password,
|
|
});
|
|
} else {
|
|
showWarningToast("Mã QR không hợp lệ");
|
|
}
|
|
} catch (error) {
|
|
showWarningToast("Mã QR không hợp lệ");
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (creds?: {
|
|
username: string;
|
|
password: string;
|
|
}) => {
|
|
const user = creds?.username ?? username;
|
|
const pass = creds?.password ?? password;
|
|
|
|
// Validate input
|
|
if (!user?.trim() || !pass?.trim()) {
|
|
showErrorToast("Vui lòng nhập tài khoản và mật khẩu");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const body: Model.LoginRequestBody = {
|
|
username: user,
|
|
password: pass,
|
|
};
|
|
|
|
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)
|
|
console.log("Login Token ");
|
|
|
|
await setStorageItem(TOKEN, response.data.token);
|
|
console.log("Token:", response.data.token);
|
|
router.replace("/(tabs)");
|
|
}
|
|
} catch (error) {
|
|
showErrorToast(
|
|
error instanceof Error ? error.message : "Đăng nhập thất bại"
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSwitchLanguage = (isVN: boolean) => {
|
|
if (isVN) {
|
|
setLocale("vi");
|
|
} else {
|
|
setLocale("en");
|
|
}
|
|
setIsVNLang(isVN);
|
|
};
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
style={{ flex: 1 }}
|
|
>
|
|
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
|
<ThemedView style={styles.container}>
|
|
{/* Header */}
|
|
<View style={styles.headerContainer}>
|
|
{/* Logo */}
|
|
<Image
|
|
source={require("@/assets/images/logo.png")}
|
|
style={styles.logo}
|
|
resizeMode="contain"
|
|
/>
|
|
<ThemedText type="title" style={styles.title}>
|
|
{t("common.app_name")}
|
|
</ThemedText>
|
|
</View>
|
|
|
|
{/* Form */}
|
|
<View style={styles.formContainer}>
|
|
{/* Username Input */}
|
|
<View style={styles.inputGroup}>
|
|
<ThemedText style={styles.label}>{t("auth.username")}</ThemedText>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder={t("auth.username_placeholder")}
|
|
placeholderTextColor="#999"
|
|
value={username}
|
|
onChangeText={setUsername}
|
|
editable={!loading}
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
|
|
{/* Password Input */}
|
|
<View style={styles.inputGroup}>
|
|
<ThemedText style={styles.label}>{t("auth.password")}</ThemedText>
|
|
<View className="relative">
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder={t("auth.password_placeholder")}
|
|
placeholderTextColor="#999"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secureTextEntry={!showPassword}
|
|
editable={!loading}
|
|
autoCapitalize="none"
|
|
/>
|
|
{/* Position absolute with top:0 and bottom:0 and justifyContent:center
|
|
ensures the icon remains vertically centered inside the input */}
|
|
<TouchableOpacity
|
|
onPress={() => setShowPassword(!showPassword)}
|
|
style={{
|
|
position: "absolute",
|
|
right: 12,
|
|
top: 0,
|
|
bottom: 0,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
padding: 4,
|
|
}}
|
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
>
|
|
<Ionicons
|
|
name={showPassword ? "eye-off" : "eye"}
|
|
size={22}
|
|
color="#666"
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Login Button (3/4) + QR Scan (1/4) */}
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.loginButton,
|
|
loading && styles.loginButtonDisabled,
|
|
{ flex: 5, marginRight: 12, marginTop: 0 },
|
|
]}
|
|
onPress={() => handleLogin()}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator color="#fff" size="small" />
|
|
) : (
|
|
<Text style={styles.loginButtonText}>{t("auth.login")}</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={{
|
|
flex: 1,
|
|
paddingVertical: 10,
|
|
marginTop: 0,
|
|
borderColor: "#ddd",
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
backgroundColor: "transparent",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
onPress={() => setIsShowingQRScanner(true)}
|
|
disabled={loading}
|
|
>
|
|
<MaterialIcons
|
|
name="qr-code-scanner"
|
|
size={28}
|
|
color="#007AFF"
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Language Switcher */}
|
|
<View style={styles.languageSwitcherContainer}>
|
|
<RotateSwitch
|
|
initialValue={isVNLang}
|
|
onChange={handleSwitchLanguage}
|
|
size="sm"
|
|
offImage={EnIcon}
|
|
onImage={VnIcon}
|
|
/>
|
|
<SliceSwitch
|
|
size="sm"
|
|
leftIcon="moon"
|
|
leftIconColor="white"
|
|
rightIcon="sunny"
|
|
rightIconColor="orange"
|
|
activeBackgroundColor="black"
|
|
inactiveBackgroundColor="white"
|
|
inactiveOverlayColor="black"
|
|
activeOverlayColor="white"
|
|
/>
|
|
</View>
|
|
|
|
{/* Copyright */}
|
|
<View style={styles.copyrightContainer}>
|
|
<ThemedText style={styles.copyrightText}>
|
|
© {new Date().getFullYear()} - {t("common.footer_text")}
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
</ThemedView>
|
|
</ScrollView>
|
|
<ScanQRCode
|
|
visible={isShowingQRScanner}
|
|
onClose={() => setIsShowingQRScanner(false)}
|
|
onScanned={handleQRCodeScanned}
|
|
/>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollContainer: {
|
|
flexGrow: 1,
|
|
justifyContent: "center",
|
|
},
|
|
container: {
|
|
flex: 1,
|
|
paddingHorizontal: 20,
|
|
justifyContent: "center",
|
|
},
|
|
headerContainer: {
|
|
marginBottom: 40,
|
|
alignItems: "center",
|
|
},
|
|
logo: {
|
|
width: 120,
|
|
height: 120,
|
|
marginBottom: 20,
|
|
},
|
|
title: {
|
|
fontSize: 28,
|
|
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",
|
|
},
|
|
copyrightContainer: {
|
|
marginTop: 20,
|
|
alignItems: "center",
|
|
},
|
|
copyrightText: {
|
|
fontSize: 12,
|
|
opacity: 0.6,
|
|
textAlign: "center",
|
|
},
|
|
languageSwitcherContainer: {
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
justifyContent: "center",
|
|
|
|
alignItems: "center",
|
|
marginTop: 24,
|
|
gap: 20,
|
|
},
|
|
languageSwitcherLabel: {
|
|
fontSize: 12,
|
|
fontWeight: "600",
|
|
textAlign: "center",
|
|
opacity: 0.8,
|
|
},
|
|
languageButtonsContainer: {
|
|
flexDirection: "row",
|
|
gap: 10,
|
|
},
|
|
languageButton: {
|
|
flex: 1,
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 12,
|
|
borderWidth: 1,
|
|
borderColor: "#ddd",
|
|
borderRadius: 8,
|
|
alignItems: "center",
|
|
backgroundColor: "transparent",
|
|
},
|
|
languageButtonActive: {
|
|
backgroundColor: "#007AFF",
|
|
borderColor: "#007AFF",
|
|
},
|
|
languageButtonText: {
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
color: "#666",
|
|
},
|
|
languageButtonTextActive: {
|
|
color: "#fff",
|
|
fontWeight: "600",
|
|
},
|
|
});
|