import type { CaptchaRes } from '@/components/captcha-box';
import CaptchaBox from '@/components/captcha-box';
import { site } from '@/config.json';
import { useInterval } from '@/hooks/hooks';
import api, { ApiError } from '@/utils/api';
import { signIn, smsSignIn, useAuth } from '@/utils/auth';
import { Button, Toast } from '@ant-design/react-native';
import { Ionicons } from '@expo/vector-icons';
import { Link, router, useLocalSearchParams } from 'expo-router';
import { openBrowserAsync } from 'expo-web-browser';
import React, { useEffect, useRef, useState } from 'react';
import {
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View
} from 'react-native';
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withSequence,
withTiming,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function FieldLabel({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
export default function SignInScreen() {
const [authMode, setAuthMode] = useState<'sms' | 'password'>('sms');
const [agreed, setAgreed] = useState(false);
const [agreementHighlight, setFlushAgree] = useState(false);
const [loading, setLoading] = useState(false);
const [mobile, setMobile] = useState('');
const [code, setCode] = useState('');
const [password, setPassword] = useState('');
const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>();
const { setToken } = useAuth();
const scrollViewRef = useRef(null);
const agreeHighlight = useSharedValue(0);
useEffect(() => {
if (agreementHighlight) {
agreeHighlight.value = withSequence(
withTiming(1, { duration: 140 }),
withTiming(0, { duration: 140 }),
withTiming(1, { duration: 140 }),
withTiming(0, { duration: 140 }),
withTiming(1, { duration: 200 }),
);
} else {
agreeHighlight.value = withTiming(0, { duration: 200 });
}
}, [agreementHighlight, agreeHighlight]);
const agreeBoxStyle = useAnimatedStyle(() => ({
borderColor: interpolateColor(
agreeHighlight.value,
[0, 1],
['rgba(37, 99, 235, 0)', 'rgba(37, 99, 235, 0.5)'],
),
}));
const [captchaVisible, setCaptchaVisible] = useState(false);
const [smsTtl, setSmsTtl] = useState(0);
const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
const needsCaptchaRef = useRef(false);
const handleCaptcha = (res: CaptchaRes) => {
const resolver = waitCaptcha.current;
waitCaptcha.current = null;
setCaptchaVisible(false);
resolver?.(res);
};
const handleSendCode = async () => {
if (mobile.trim().length !== 11) {
Toast.fail('请先输入 11 位手机号');
return;
}
let captcha: CaptchaRes = null!;
while (true) {
if (needsCaptchaRef.current) {
setCaptchaVisible(true);
captcha = await new Promise((resolve) => {
waitCaptcha.current = resolve;
});
if (!captcha || !captcha.ok) {
return;
}
}
try {
const res = await api.post<{
needsCaptcha?: boolean;
timerout: number;
}>(`sms/send?__session_id=${captcha?.sid ?? ''}`, {
mobile: mobile,
event: 'login',
captcha_code: captcha?.code,
});
if (res.needsCaptcha) {
needsCaptchaRef.current = true;
continue;
}
setSmsTtl(res.timerout); // 后端字段拼写为 timerout,保留以兼容接口
break;
} catch (e) {
if (ApiError.isApiError(e)) {
if (e.code === -1) {
Toast.fail("该手机已被注册");
return;
}
if (e.code === -99) {
Toast.fail("图形验证码无效");
return;
}
}
Toast.fail("发送验证码失败");
return;
}
}
};
useInterval(() => {
setSmsTtl((prev) => prev - 1)
}, smsTtl > 0 ? 1000 : null);
const handleLogin = async () => {
setFlushAgree(false);
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
if (mobile.trim().length !== 11) {
Toast.fail('请输入正确的手机号');
return;
}
if (authMode === 'sms' && code.trim().length !== 6) {
Toast.fail('请输入 6 位验证码');
return;
}
if (authMode === 'password' && password.length < 6) {
Toast.fail('请输入不少于 6 位的密码');
return;
}
if (!agreed) {
scrollViewRef.current?.scrollToEnd({ animated: true });
setFlushAgree(true);
Toast.fail('请先阅读并同意协议');
return;
}
setLoading(true);
try {
const token = authMode === 'sms' ? await smsSignIn(mobile, code) : await signIn(mobile, password);
setToken(token);
Toast.success('登录成功');
if (redirectTo) {
router.replace(redirectTo as never);
} else {
router.canGoBack() ? router.back() : router.replace('/');
}
} catch (error) {
console.error('登录失败:', error);
Toast.fail('登录失败,请稍后重试');
} finally {
setLoading(false);
}
};
const insets = useSafeAreaInsets();
return (
欢迎回来
登录贷款助手,开启您的
{[
['sms', '验证码登录'],
['password', '密码登录'],
].map(([mode, label]) => {
const active = authMode === mode;
return (
setAuthMode(mode as 'sms' | 'password')}
className={`flex-1 rounded-xl px-4 py-3 ${active ? 'bg-surface-container-lowest' : ''
}`}
style={({ pressed }) => ({
opacity: loading ? 0.5 : pressed ? 0.86 : 1,
transform: [{ scale: pressed ? 0.99 : 1 }],
})}
>
{label}
);
})}
手机号码
+86
{authMode === 'sms' ? '验证码' : '密码'}
{authMode === 'sms' ? (
0}
hitSlop={8}
onPress={handleSendCode}
>
0 ? 'text-on-secondary-fixed/50' : 'text-primary'}`}>
{smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
) : (
)}
router.push({ pathname: '/sign-up', params: { signIn: 1, redirectTo } })}>
注册账号
遇到问题?
其他方式登录
{[
['chatbubble-ellipses-outline', '#6b7280'],
['logo-apple', '#6b7280'],
].map(([icon, color]) => (
({
opacity: pressed ? 0.8 : 1,
transform: [{ scale: pressed ? 0.94 : 1 }],
})}
>
))}
setAgreed((value) => !value)}
hitSlop={8}
className="pt-1"
>
{agreed ? (
) : null}
登录即代表您已阅读并同意
《用户服务协议》
、
openBrowserAsync(`${site}`)}>《隐私政策》
,以及授权该应用获取您的公开信息。
{loading && }
);
}