React Native Expo应用完成2FA后iCloud Keychain保存弹窗不显示
解决Expo React Native登录2FA后iCloud Keychain凭据保存弹窗不出现的问题
问题分析
核心原因是:当焦点停留在textContentType="oneTimeCode"的OTP输入框时,系统会判定当前处于一次性验证码场景,不会触发普通账号密码的保存提示;加上首次登录失败后焦点状态的变化,导致后续成功登录时无法触发弹窗。
解决步骤
1. 登录成功前切换焦点到密码输入框
主动让密码输入框获取焦点,让系统识别到当前是凭据相关的输入场景,触发保存提示:
- 给密码输入框添加ref:
const passwordInputRef = useRef<TextInput>(null);
- 绑定到密码TextInput组件:
<TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry textContentType="password" style={styles.input} ref={passwordInputRef} />
- 登录成功后先切换焦点再跳转:
try { await login({ email, password, otpAttempt: otpCode, }); passwordInputRef.current?.focus(); // 延迟跳转确保焦点切换生效 setTimeout(() => { router.navigate('/home'); }, 100); } catch (error) { // 原有错误处理逻辑 }
2. 登录前主动失焦OTP输入框
点击登录时,若OTP框显示则先失焦,避免焦点停留在OTP框干扰凭据保存逻辑:
- 给OTP输入框添加ref:
const otpInputRef = useRef<TextInput>(null);
- 绑定到OTP的TextInput组件:
<TextInput placeholder="OTP CODE" value={otpCode} onChangeText={setOtpCode} textContentType="oneTimeCode" style={styles.input} ref={otpInputRef} />
- 在登录方法开头添加失焦逻辑:
const handleOnSignIn = async ({ email, password, otpCode }: SignInForm) => { if (isOtpCodeRequired) { otpInputRef.current?.blur(); } try { // 原有登录逻辑 } catch (error) { // 原有错误处理逻辑 } };
修改后的完整代码
import theme from '@config/theme'; import { router } from 'expo-router'; import React, { useState, useRef } from 'react'; import { Button, StyleSheet, Text, TextInput, View } from 'react-native'; import Toast from 'react-native-toast-message'; import { useSignIn } from '../hooks'; import { ErrorApiResponse, SignInForm } from '../types'; const SignInScreen = () => { const { login } = useSignIn(); const [isOtpCodeRequired, setIsOtpCodeRequired] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otpCode, setOtpCode] = useState(''); const passwordInputRef = useRef<TextInput>(null); const otpInputRef = useRef<TextInput>(null); const handleOnSignIn = async ({ email, password, otpCode }: SignInForm) => { if (isOtpCodeRequired) { otpInputRef.current?.blur(); } try { await login({ email, password, otpAttempt: otpCode, }); passwordInputRef.current?.focus(); setTimeout(() => { router.navigate('/home'); }, 100); } catch (error) { const typedError = error as ErrorApiResponse; if ( typedError?.response?.status === 401 && typedError?.response?.data?.errorCode === 12345 ) { setIsOtpCodeRequired(true); } else { Toast.show({ type: 'error', text1: 'Login Error', text2: typedError?.response?.data?.error, }); } } }; return ( <View style={styles.mainContainer}> <View style={styles.formContainer}> <View style={{}}> <Text>Email</Text> <TextInput value={email} onChangeText={setEmail} placeholder="Email" keyboardType="email-address" textContentType="emailAddress" style={styles.input} /> </View> <View style={{}}> <Text>Password</Text> <TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry textContentType="password" style={styles.input} ref={passwordInputRef} /> </View> {isOtpCodeRequired && ( <View style={{}}> <Text>Otp Code</Text> <TextInput placeholder="OTP CODE" value={otpCode} onChangeText={setOtpCode} textContentType="oneTimeCode" style={styles.input} ref={otpInputRef} /> </View> )} </View> <View style={styles.buttonContainer}> <Button title="Log in" onPress={() => handleOnSignIn({ email, password, otpCode })} /> </View> </View> ); }; const styles = StyleSheet.create({ mainContainer: { flex: 1, justifyContent: 'space-between', paddingHorizontal: theme.sizes.dimensions.padding * 2, }, formContainer: { marginTop: 46 }, input: { borderWidth: 1, borderColor: theme.colors.gray500, padding: 8, borderRadius: 8, }, buttonContainer: { marginBottom: theme.sizes.dimensions.padding * 2, }, }); export default SignInScreen;
内容的提问来源于stack exchange,提问作者Ale
相关产品推荐
相关产品推荐

