thêm quét QR đăng nhập, sửa lại logic để gọi api bằng ip thiết bị

This commit is contained in:
Tran Anh Tuan
2025-11-10 16:12:52 +07:00
parent c26de5aefc
commit 1a534eccb0
10 changed files with 224 additions and 218 deletions

View File

@@ -1,14 +1,16 @@
import ScanQRCode from "@/components/ScanQRCode";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { TOKEN } from "@/constants";
import { DOMAIN, TOKEN } from "@/constants";
import { login } from "@/controller/AuthController";
import { showErrorToast } from "@/services/toast_service";
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 {
@@ -29,22 +31,30 @@ export default function LoginScreen() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [isShowingQRScanner, setIsShowingQRScanner] = useState(false);
const checkLogin = useCallback(async () => {
const token = await getStorageItem(TOKEN);
console.log("Token:", 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);
console.log("Parse Token: ", parsed);
// 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);
await removeStorageItem(DOMAIN);
} else {
router.replace("/(tabs)");
}
@@ -54,9 +64,41 @@ export default function LoginScreen() {
checkLogin();
}, [checkLogin]);
const handleLogin = async () => {
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 (!username.trim() || !password.trim()) {
if (!user?.trim() || !pass?.trim()) {
showErrorToast("Vui lòng nhập tài khoản và mật khẩu");
return;
}
@@ -64,8 +106,8 @@ export default function LoginScreen() {
setLoading(true);
try {
const body: Model.LoginRequestBody = {
username: username,
password: password,
username: user,
password: pass,
};
const response = await login(body);
@@ -131,33 +173,87 @@ export default function LoginScreen() {
{/* Password Input */}
<View style={styles.inputGroup}>
<ThemedText style={styles.label}>Mật khẩu</ThemedText>
<TextInput
style={styles.input}
placeholder="Nhập mật khẩu"
placeholderTextColor="#999"
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!loading}
autoCapitalize="none"
/>
<View className="relative">
<TextInput
style={styles.input}
placeholder="Nhập mật khẩu"
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 */}
<TouchableOpacity
style={[
styles.loginButton,
loading && styles.loginButtonDisabled,
]}
onPress={handleLogin}
disabled={loading}
{/* Login Button (3/4) + QR Scan (1/4) */}
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
}}
>
{loading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.loginButtonText}>Đăng nhập</Text>
)}
</TouchableOpacity>
<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}>Đăng nhập</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>
{/* Footer text */}
<View style={styles.footerContainer}>
@@ -176,6 +272,11 @@ export default function LoginScreen() {
</View>
</ThemedView>
</ScrollView>
<ScanQRCode
visible={isShowingQRScanner}
onClose={() => setIsShowingQRScanner(false)}
onScanned={handleQRCodeScanned}
/>
</KeyboardAvoidingView>
);
}