From 38c466548b955facaf81b0fc4cafde0eaa7ad25a Mon Sep 17 00:00:00 2001 From: twinkle-react Date: Sun, 5 Apr 2026 10:08:27 +0530 Subject: [PATCH] fix/80025-android-keyboard-magic-code --- src/components/MagicCodeInput.tsx | 6 ++ .../ValidateCodeForm/BaseValidateCodeForm.tsx | 63 +++++++++++++------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index cf569fa23bee9..2f9810a3d143f 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -105,6 +105,9 @@ type MagicCodeInputProps = { /** Accessibility label for the input */ accessibilityLabel?: string; + /** Optional: receives the native text input ref so callers can run shared autofocus logic */ + inputCallbackRef?: (ref: BaseTextInputRef | null) => void; + /** Reference to the outer element */ ref?: ForwardedRef; }; @@ -156,6 +159,7 @@ function MagicCodeInput({ hasError = false, testID = '', accessibilityLabel, + inputCallbackRef, ref, }: MagicCodeInputProps) { const styles = useThemeStyles(); @@ -473,6 +477,8 @@ function MagicCodeInput({ }} ref={(newRef) => { inputRef.current = newRef; + // Notify parent/shared hook about the native TextInput ref (optional). + inputCallbackRef?.(newRef); }} autoFocus={autoFocus} inputMode="numeric" diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index fc2fd16b609a5..f230418b761b9 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -1,7 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; @@ -13,6 +13,7 @@ import Text from '@components/Text'; import ValidateCodeCountdown from '@components/ValidateCodeCountdown'; import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types'; import {useWideRHPState} from '@components/WideRHPContextProvider'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -20,6 +21,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isMobileSafari} from '@libs/Browser'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidValidateCode} from '@libs/ValidationUtils'; import {clearValidateCodeActionError} from '@userActions/User'; @@ -121,15 +123,19 @@ function BaseValidateCodeForm({ const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(''); const [isCountdownRunning, setIsCountdownRunning] = useState(true); + const [focusCycle, setFocusCycle] = useState(0); const inputValidateCodeRef = useRef(null); - const [account = getEmptyObject()] = useOnyx(ONYXKEYS.ACCOUNT); + const {inputCallbackRef, inputRef} = useAutoFocusInput(false); + const [account = getEmptyObject()] = useOnyx(ONYXKEYS.ACCOUNT, { + canBeMissing: true, + }); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case const shouldDisableResendValidateCode = !!isOffline || account?.isLoading; const focusTimeoutRef = useRef(null); const [canShowError, setCanShowError] = useState(false); - const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE, {canBeMissing: true}); const validateCodeSent = useMemo(() => hasMagicCodeBeenSent ?? validateCodeAction?.validateCodeSent, [hasMagicCodeBeenSent, validateCodeAction?.validateCodeSent]); const latestValidateCodeError = getLatestErrorField(validateCodeAction, validateCodeActionErrorField); const defaultValidateCodeError = getLatestErrorField(validateCodeAction, 'actionVerified'); @@ -163,29 +169,48 @@ function BaseValidateCodeForm({ useFocusEffect( useCallback(() => { - if (!inputValidateCodeRef.current) { - return; - } + const focusInput = () => { + inputValidateCodeRef.current?.focusLastSelected(); + }; + + // Force a fresh input instance on each screen focus to avoid stale native focus state on reopen. + setFocusCycle((prev) => prev + 1); + + const runFocusWithRetry = () => { + focusInput(); + focusTimeoutRef.current = setTimeout(() => { + if (inputRef.current?.isFocused?.()) { + return; + } + focusInput(); + }, CONST.ANIMATED_TRANSITION); + }; + if (focusTimeoutRef.current) { clearTimeout(focusTimeoutRef.current); } - // Keyboard won't show if we focus the input with a delay, so we need to focus immediately. - if (!isMobileSafari()) { - focusTimeoutRef.current = setTimeout(() => { - inputValidateCodeRef.current?.focusLastSelected(); - }, CONST.ANIMATED_TRANSITION); - } else { - inputValidateCodeRef.current?.focusLastSelected(); + if (DeviceCapabilities.canUseTouchScreen()) { + focusTimeoutRef.current = setTimeout(runFocusWithRetry, 0); + + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + }; } + const interactionTask = InteractionManager.runAfterInteractions(runFocusWithRetry); + return () => { + interactionTask.cancel(); if (!focusTimeoutRef.current) { return; } clearTimeout(focusTimeoutRef.current); }; - }, []), + }, [inputRef]), ); useEffect(() => { @@ -200,9 +225,9 @@ function BaseValidateCodeForm({ if (!validateCodeSent) { return; } - // Delay prevents the input from gaining focus before the RHP slide-out animation finishes, - // which would cause issues with the RHP sliding out smoothly and flickering of the wide RHP in the background. - if ((wideRHPRouteKeys.length > 0 && !isMobileSafari()) || isInPageModal) { + const shouldDelayClear = !DeviceCapabilities.canUseTouchScreen() && ((wideRHPRouteKeys.length > 0 && !isMobileSafari()) || isInPageModal); + + if (shouldDelayClear) { focusTimeoutRef.current = setTimeout(() => { inputValidateCodeRef.current?.clear(); }, CONST.ANIMATED_TRANSITION); @@ -288,6 +313,7 @@ function BaseValidateCodeForm({ return ( <> {shouldShowTimer && ( @@ -359,7 +386,6 @@ function BaseValidateCodeForm({ style={[styles.mt4]} success={false} large - sentryLabel={CONST.SENTRY_LABEL.VALIDATE_CODE.SKIP} /> )} {!hideSubmitButton && ( @@ -372,7 +398,6 @@ function BaseValidateCodeForm({ large // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isLoading={account?.isLoading || isLoading} - sentryLabel={CONST.SENTRY_LABEL.VALIDATE_CODE.VERIFY} /> )}