254 lines
7.9 KiB
TypeScript
254 lines
7.9 KiB
TypeScript
import Error from "@/assets/icons/svg/error.svg";
|
|
import { fetchApi } from "@/lib/server-api-util";
|
|
import { User } from "@/types/user";
|
|
import OTPInputView from '@twotalltotems/react-native-otp-input';
|
|
import { router } from "expo-router";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Animated, TextInput as RNTextInput, StyleSheet, TouchableOpacity, View } from "react-native";
|
|
import { useAuth } from "../../contexts/auth-context";
|
|
import { ThemedText } from "../ThemedText";
|
|
|
|
interface CodeProps {
|
|
phone: string;
|
|
}
|
|
|
|
const Code = ({ phone }: CodeProps) => {
|
|
const { t } = useTranslation();
|
|
const { login } = useAuth();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const refs = useRef<Array<RNTextInput | null>>(Array(6).fill(null));
|
|
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
const [code, setCode] = useState<string[]>([]);
|
|
const [error, setError] = useState<string>('');
|
|
|
|
const focusNext = (index: number, value: string) => {
|
|
if (value && index < 5) {
|
|
refs?.current?.[index + 1]?.focus();
|
|
}
|
|
};
|
|
const focusPrevious = (index: number, key: string) => {
|
|
if (key === 'Backspace' && index > 0 && !code[index]) {
|
|
refs?.current?.[index - 1]?.focus();
|
|
}
|
|
};
|
|
const handleCodeChange = (text: string, index: number) => {
|
|
setError('');
|
|
const newCode = [...code];
|
|
|
|
// Handle pasted code from SMS or autofill
|
|
if ((text.length === 6 || text.length > 1) && /^\d+$/.test(text)) {
|
|
const digits = text.split('').slice(0, 6); // Ensure we only take first 6 digits
|
|
setCode(digits);
|
|
refs.current[5]?.focus(); // Focus on the last input
|
|
// Auto-submit if we have exactly 6 digits
|
|
if (digits.length === 6) {
|
|
handleTelLogin();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle single digit input
|
|
if (text.length <= 1 && /^\d?$/.test(text)) {
|
|
newCode[index] = text;
|
|
setCode(newCode);
|
|
|
|
// Auto-submit if this is the last digit
|
|
if (text && index === 5) {
|
|
handleTelLogin();
|
|
} else if (text) {
|
|
focusNext(index, text);
|
|
}
|
|
}
|
|
};
|
|
const sendVerificationCode = async () => {
|
|
try {
|
|
// 发送验证码
|
|
await fetchApi(`/iam/veritification-code`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ phone: phone }),
|
|
})
|
|
|
|
} catch (error) {
|
|
}
|
|
}
|
|
|
|
const handleTelLogin = async () => {
|
|
setError('');
|
|
if (!code.join('')) {
|
|
setError(t("auth.telLogin.codeRequired", { ns: 'login' }));
|
|
return;
|
|
}
|
|
// 如果验证码不是六位,提示错误
|
|
if (code.join('').length !== 6) {
|
|
setError(t("auth.telLogin.codeInvalid", { ns: 'login' }));
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setCountdown(60);
|
|
try {
|
|
await fetchApi<User>(`/iam/login/phone-login`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ phone: phone, code: code.join('') }),
|
|
}).then((res) => {
|
|
login(res, res.access_token || '')
|
|
router.replace('/user-message')
|
|
}).catch((error) => {
|
|
setError(t("auth.telLogin.codeVaild", { ns: 'login' }));
|
|
})
|
|
setIsLoading(false);
|
|
} catch (error) {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
// 60s倒计时
|
|
const [countdown, setCountdown] = useState(0);
|
|
useEffect(() => {
|
|
if (countdown > 0) {
|
|
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [countdown]);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.contentContainer}>
|
|
<View style={styles.headerContainer}>
|
|
<ThemedText style={styles.title}>
|
|
{t("auth.telLogin.codeTitle", { ns: 'login' })}
|
|
</ThemedText>
|
|
<ThemedText style={styles.subtitle}>
|
|
{t("auth.telLogin.secondTitle", { ns: 'login' })}
|
|
</ThemedText>
|
|
<ThemedText style={styles.phoneNumber}>
|
|
{phone}
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<OTPInputView
|
|
pinCount={6}
|
|
onCodeChanged={(code) => {
|
|
setCode([code]);
|
|
}}
|
|
onCodeFilled={() => {
|
|
handleTelLogin()
|
|
}}
|
|
code={code.join('')}
|
|
autoFocusOnLoad={false}
|
|
codeInputFieldStyle={styles.underlineStyleBase}
|
|
codeInputHighlightStyle={styles.underlineStyleHighLighted}
|
|
style={styles.otpContainer}
|
|
placeholderCharacter="-"
|
|
placeholderTextColor="#AC7E35"
|
|
/>
|
|
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
|
|
<Error />
|
|
<ThemedText style={styles.errorText}>
|
|
{error}
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.footerContainer}>
|
|
<ThemedText style={styles.footerText}>
|
|
{t("auth.telLogin.sendAgain", { ns: 'login' })}
|
|
</ThemedText>
|
|
<TouchableOpacity onPress={() => {
|
|
if (countdown <= 0) {
|
|
sendVerificationCode()
|
|
}
|
|
}}>
|
|
<ThemedText style={[
|
|
styles.resendText,
|
|
countdown > 0 && styles.disabledResendText
|
|
]}>
|
|
{countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#FFFFFF',
|
|
},
|
|
contentContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
headerContainer: {
|
|
alignItems: 'center',
|
|
marginBottom: 16,
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
fontWeight: '600',
|
|
marginBottom: 8,
|
|
paddingTop: 4,
|
|
color: '#111827',
|
|
},
|
|
subtitle: {
|
|
fontSize: 16,
|
|
color: '#4B5563',
|
|
textAlign: 'center',
|
|
marginBottom: 4,
|
|
},
|
|
phoneNumber: {
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: '#E2793F',
|
|
},
|
|
otpContainer: {
|
|
width: '100%',
|
|
height: 80,
|
|
},
|
|
underlineStyleBase: {
|
|
width: 50,
|
|
height: 50,
|
|
borderWidth: 0,
|
|
borderRadius: 16,
|
|
fontSize: 18,
|
|
color: '#000000',
|
|
textAlign: 'center',
|
|
backgroundColor: '#FFF8DE',
|
|
},
|
|
underlineStyleHighLighted: {
|
|
borderColor: '#E2793F',
|
|
backgroundColor: '#FFF8DE',
|
|
borderWidth: 2,
|
|
},
|
|
errorContainer: {
|
|
width: '100%',
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
alignItems: 'center',
|
|
},
|
|
errorText: {
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: '#E2793F',
|
|
marginLeft: 8,
|
|
},
|
|
footerContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
marginTop: 8,
|
|
},
|
|
footerText: {
|
|
color: '#6B7280',
|
|
},
|
|
resendText: {
|
|
color: '#E2793F',
|
|
fontWeight: '500',
|
|
marginLeft: 4,
|
|
},
|
|
disabledResendText: {
|
|
color: '#9CA3AF',
|
|
},
|
|
});
|
|
|
|
export default Code; |