You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 12:15:17