From 1447e45b011d8dbf3ce6f47e2db4972b2049cb02 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 11:28:50 +0100 Subject: [PATCH 1/8] feat: add ValidateCodeCountdown component for managing code resend countdown --- .../signin/ValidateCodeCountdown/index.tsx | 68 +++++++++++++++++++ .../signin/ValidateCodeCountdown/types.ts | 13 ++++ 2 files changed, 81 insertions(+) create mode 100644 src/pages/signin/ValidateCodeCountdown/index.tsx create mode 100644 src/pages/signin/ValidateCodeCountdown/types.ts diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/pages/signin/ValidateCodeCountdown/index.tsx new file mode 100644 index 000000000000..818b87ee6889 --- /dev/null +++ b/src/pages/signin/ValidateCodeCountdown/index.tsx @@ -0,0 +1,68 @@ + +import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; +import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; +import RenderHTML from '@components/RenderHTML'; +import useLocalize from '@hooks/useLocalize'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps} from './types'; + +const ValidateCodeCountdown = forwardRef( +({shouldDisableResendValidateCode, hasError, isOffline, onResendValidateCode}, ref) => { + const StyleUtils = useStyleUtils(); + const styles = useThemeStyles(); + + const {translate} = useLocalize(); + + const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number); + const timerRef = useRef(undefined); + + useImperativeHandle(ref, () => ({ + resetCountdown: () => setTimeRemaining(CONST.REQUEST_CODE_DELAY as number), + })); + + useEffect(() => { + if (timeRemaining > 0) { + timerRef.current = setTimeout(() => { + setTimeRemaining((prev) => prev - 1); + }, 1000); + } + return () => { + clearTimeout(timerRef.current); + }; + }, [timeRemaining]); + + return ( + + {timeRemaining > 0 && !isOffline ? ( + + + + ) : ( + + + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} + + + )} + + ); +}); + + +export default ValidateCodeCountdown; \ No newline at end of file diff --git a/src/pages/signin/ValidateCodeCountdown/types.ts b/src/pages/signin/ValidateCodeCountdown/types.ts new file mode 100644 index 000000000000..dffc62e0d9d6 --- /dev/null +++ b/src/pages/signin/ValidateCodeCountdown/types.ts @@ -0,0 +1,13 @@ +type ValidateCodeCountdownProps = { + shouldDisableResendValidateCode: boolean; + hasError: boolean; + isOffline: boolean; + onResendValidateCode: () => void; +}; + +type ValidateCodeCountdownHandle = { + resetCountdown: () => void; +}; + +export type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps}; + From 5395920e326cfadafad3e72e2e227ed3537b20c0 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 10:02:59 +0100 Subject: [PATCH 2/8] refactor: use ValidateCodeCountdown component to manage resend countdown and move timer logic --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 56 +++++-------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 006215a1779c..9370fbe1d19f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -7,7 +7,6 @@ import FormHelpMessage from '@components/FormHelpMessage'; import type {MagicCodeInputHandle} from '@components/MagicCodeInput'; import MagicCodeInput from '@components/MagicCodeInput'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import type {WithToggleVisibilityViewProps} from '@components/withToggleVisibilityView'; @@ -16,13 +15,13 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import AccountUtils from '@libs/AccountUtils'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; +import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown'; import Terms from '@pages/signin/Terms'; import {clearAccountMessages, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User'; @@ -30,6 +29,8 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +// import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown'; +import type { ValidateCodeCountdownHandle } from '@pages/signin/ValidateCodeCountdown/types'; import type ValidateCodeFormProps from './types'; type BaseValidateCodeFormProps = WithToggleVisibilityViewProps & @@ -52,14 +53,12 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {canBeMissing: true}); const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const isFocused = useIsFocused(); const {isOffline} = useNetwork(); const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(credentials?.validateCode ?? ''); const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); - const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number); const [recoveryCode, setRecoveryCode] = useState(''); const [needToClearError, setNeedToClearError] = useState(!!account?.errors); @@ -68,7 +67,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const inputValidateCodeRef = useRef(undefined); const input2FARef = useRef(undefined); - const timerRef = useRef(undefined); + const countdownRef = useRef(null); const hasError = !!account && !isEmptyObject(account?.errors) && !needToClearError; const isLoadingResendValidationForm = account?.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; @@ -86,7 +85,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !isVisible || !isFocused) { return; } - setTimeRemaining(CONST.REQUEST_CODE_DELAY); + countdownRef.current?.resetCountdown(); inputValidateCodeRef.current.focus(); }, [isVisible, isFocused]); @@ -118,17 +117,6 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco input2FARef.current.clear(); }, [twoFactorAuthCode]); - useEffect(() => { - if (timeRemaining > 0) { - timerRef.current = setTimeout(() => { - setTimeRemaining(timeRemaining - 1); - }, 1000); - } - return () => { - clearTimeout(timerRef.current); - }; - }, [timeRemaining]); - /** * Handle text input and clear formError upon text change */ @@ -157,7 +145,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco userActionsResendValidateCode(credentials?.login ?? ''); inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. - setTimeRemaining(CONST.REQUEST_CODE_DELAY); + countdownRef.current?.resetCountdown(); }; /** @@ -373,31 +361,13 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco testID="validateCode" /> {hasError && } - - {timeRemaining > 0 && !isOffline ? ( - - - - ) : ( - - - {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} - - - )} - + )} From d6a4df0c74b5a20fd8403b6891e8e6135278c41c Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 10:05:43 +0100 Subject: [PATCH 3/8] commented unused inmport removed --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 9370fbe1d19f..4265043bc07e 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -29,7 +29,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -// import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown'; import type { ValidateCodeCountdownHandle } from '@pages/signin/ValidateCodeCountdown/types'; import type ValidateCodeFormProps from './types'; From d2c5f0dd2140787b9acf38415086b8c6579899d5 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 10:14:05 +0100 Subject: [PATCH 4/8] chore: pretteir run --- .../signin/ValidateCodeCountdown/index.tsx | 63 +++++++++---------- .../signin/ValidateCodeCountdown/types.ts | 1 - .../ValidateCodeForm/BaseValidateCodeForm.tsx | 4 +- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/pages/signin/ValidateCodeCountdown/index.tsx index 818b87ee6889..76b8f14c6e2c 100644 --- a/src/pages/signin/ValidateCodeCountdown/index.tsx +++ b/src/pages/signin/ValidateCodeCountdown/index.tsx @@ -1,17 +1,15 @@ - import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; -import useStyleUtils from '@hooks/useStyleUtils'; -import CONST from '@src/CONST'; -import RenderHTML from '@components/RenderHTML'; -import useLocalize from '@hooks/useLocalize'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; import type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps} from './types'; -const ValidateCodeCountdown = forwardRef( -({shouldDisableResendValidateCode, hasError, isOffline, onResendValidateCode}, ref) => { +const ValidateCodeCountdown = forwardRef(({shouldDisableResendValidateCode, hasError, isOffline, onResendValidateCode}, ref) => { const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); @@ -37,32 +35,31 @@ const ValidateCodeCountdown = forwardRef - {timeRemaining > 0 && !isOffline ? ( - - - - ) : ( - - - {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} - - - )} - + {timeRemaining > 0 && !isOffline ? ( + + + + ) : ( + + + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} + + + )} + ); }); - -export default ValidateCodeCountdown; \ No newline at end of file +export default ValidateCodeCountdown; diff --git a/src/pages/signin/ValidateCodeCountdown/types.ts b/src/pages/signin/ValidateCodeCountdown/types.ts index dffc62e0d9d6..fa78a4967564 100644 --- a/src/pages/signin/ValidateCodeCountdown/types.ts +++ b/src/pages/signin/ValidateCodeCountdown/types.ts @@ -10,4 +10,3 @@ type ValidateCodeCountdownHandle = { }; export type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps}; - diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 4265043bc07e..ba1fc8631a3b 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -21,15 +21,15 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidRecoveryCode, isValidTwoFactorCode, isValidValidateCode} from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; -import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown'; import Terms from '@pages/signin/Terms'; +import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown'; +import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types'; import {clearAccountMessages, clearSignInData as sessionActionsClearSignInData, signIn, signInWithValidateCode} from '@userActions/Session'; import {resendValidateCode as userActionsResendValidateCode} from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type { ValidateCodeCountdownHandle } from '@pages/signin/ValidateCodeCountdown/types'; import type ValidateCodeFormProps from './types'; type BaseValidateCodeFormProps = WithToggleVisibilityViewProps & From 6dd3310509552e58cf9ab91e2231cc393ef200ae Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 11:42:23 +0100 Subject: [PATCH 5/8] refactor: add type for timeRemaining state --- src/pages/signin/ValidateCodeCountdown/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/pages/signin/ValidateCodeCountdown/index.tsx index 76b8f14c6e2c..be4569daf193 100644 --- a/src/pages/signin/ValidateCodeCountdown/index.tsx +++ b/src/pages/signin/ValidateCodeCountdown/index.tsx @@ -15,11 +15,11 @@ const ValidateCodeCountdown = forwardRef(CONST.REQUEST_CODE_DELAY); const timerRef = useRef(undefined); useImperativeHandle(ref, () => ({ - resetCountdown: () => setTimeRemaining(CONST.REQUEST_CODE_DELAY as number), + resetCountdown: () => setTimeRemaining(CONST.REQUEST_CODE_DELAY), })); useEffect(() => { From 07814a2c970ea07aa4a17107969bc76cfd452865 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 7 Nov 2025 14:58:52 +0100 Subject: [PATCH 6/8] refactor: integrate ValidateCodeCountdown for improved countdown management and streamline resend logic --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 39 ++++++++-------- .../signin/ValidateCodeCountdown/index.tsx | 44 ++++--------------- .../signin/ValidateCodeCountdown/types.ts | 5 +-- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 44 ++++++++++++++++--- 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index fff6d4587771..eac8dce8f50a 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -9,7 +9,6 @@ import MagicCodeInput from '@components/MagicCodeInput'; import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import {WideRHPContext} from '@components/WideRHPContextProvider'; import useLocalize from '@hooks/useLocalize'; @@ -21,6 +20,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isMobileSafari} from '@libs/Browser'; import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import {isValidValidateCode} from '@libs/ValidationUtils'; +import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown'; +import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types'; import {clearValidateCodeActionError} from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -115,6 +116,8 @@ function BaseValidateCodeForm({ const StyleUtils = useStyleUtils(); const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(''); + const [isCountdownRunning, setIsCountdownRunning] = useState(true); + const inputValidateCodeRef = useRef(null); const [account = getEmptyObject()] = useOnyx(ONYXKEYS.ACCOUNT, { canBeMissing: true, @@ -123,13 +126,12 @@ function BaseValidateCodeForm({ // 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 [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number); const [canShowError, setCanShowError] = useState(false); 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'); - const timerRef = useRef(undefined); + const countdownRef = useRef(null); const clearDefaultValidationCodeError = useCallback(() => { // Clear "Failed to send magic code" error @@ -184,6 +186,14 @@ function BaseValidateCodeForm({ }, []), ); + useEffect(() => { + if (!isCountdownRunning) { + return; + } + + countdownRef.current?.resetCountdown(); + }, [isCountdownRunning]); + useEffect(() => { if (!validateCodeSent) { return; @@ -199,24 +209,14 @@ function BaseValidateCodeForm({ } }, [validateCodeSent, wideRHPRouteKeys.length]); - useEffect(() => { - if (timeRemaining > 0) { - timerRef.current = setTimeout(() => { - setTimeRemaining(timeRemaining - 1); - }, 1000); - } - return () => { - clearTimeout(timerRef.current); - }; - }, [timeRemaining]); - /** * Request a validate code / magic code be sent to verify this contact method */ const resendValidateCode = () => { sendValidateCode(); inputValidateCodeRef.current?.clear(); - setTimeRemaining(CONST.REQUEST_CODE_DELAY); + countdownRef.current?.resetCountdown(); + setIsCountdownRunning(true); }; /** @@ -274,7 +274,7 @@ function BaseValidateCodeForm({ return getLatestErrorMessage(account ?? {}); }, [canShowError, formError, account, translate]); - const shouldShowTimer = timeRemaining > 0 && !isOffline; + const shouldShowTimer = isCountdownRunning && !isOffline; // latestValidateCodeError only holds an error related to bad magic code // while validateError holds flow-specific errors @@ -294,10 +294,9 @@ function BaseValidateCodeForm({ /> {shouldShowTimer && ( - setIsCountdownRunning(false)} /> )} diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/pages/signin/ValidateCodeCountdown/index.tsx index be4569daf193..a3d43cbc8da1 100644 --- a/src/pages/signin/ValidateCodeCountdown/index.tsx +++ b/src/pages/signin/ValidateCodeCountdown/index.tsx @@ -1,18 +1,10 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import RenderHTML from '@components/RenderHTML'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps} from './types'; -const ValidateCodeCountdown = forwardRef(({shouldDisableResendValidateCode, hasError, isOffline, onResendValidateCode}, ref) => { - const StyleUtils = useStyleUtils(); - const styles = useThemeStyles(); - +const ValidateCodeCountdown = forwardRef(({onCountdownFinish}, ref) => { const {translate} = useLocalize(); const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY); @@ -27,38 +19,20 @@ const ValidateCodeCountdown = forwardRef { setTimeRemaining((prev) => prev - 1); }, 1000); + } else { + onCountdownFinish(); } return () => { clearTimeout(timerRef.current); }; - }, [timeRemaining]); + }, [onCountdownFinish, timeRemaining]); return ( - - {timeRemaining > 0 && !isOffline ? ( - - - - ) : ( - - - {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} - - - )} - + ); }); diff --git a/src/pages/signin/ValidateCodeCountdown/types.ts b/src/pages/signin/ValidateCodeCountdown/types.ts index fa78a4967564..68712b48b0c0 100644 --- a/src/pages/signin/ValidateCodeCountdown/types.ts +++ b/src/pages/signin/ValidateCodeCountdown/types.ts @@ -1,8 +1,5 @@ type ValidateCodeCountdownProps = { - shouldDisableResendValidateCode: boolean; - hasError: boolean; - isOffline: boolean; - onResendValidateCode: () => void; + onCountdownFinish: () => void; }; type ValidateCodeCountdownHandle = { diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index ba1fc8631a3b..78253a40f207 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -15,6 +15,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import AccountUtils from '@libs/AccountUtils'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -60,6 +61,8 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); const [recoveryCode, setRecoveryCode] = useState(''); const [needToClearError, setNeedToClearError] = useState(!!account?.errors); + const [isCountdownRunning, setIsCountdownRunning] = useState(true); + const StyleUtils = useStyleUtils(); const prevRequiresTwoFactorAuth = usePrevious(account?.requiresTwoFactorAuth); const prevValidateCode = usePrevious(credentials?.validateCode); @@ -84,6 +87,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !isVisible || !isFocused) { return; } + setIsCountdownRunning(true); countdownRef.current?.resetCountdown(); inputValidateCodeRef.current.focus(); }, [isVisible, isFocused]); @@ -145,6 +149,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. countdownRef.current?.resetCountdown(); + setIsCountdownRunning(true); }; /** @@ -198,6 +203,14 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco } }; + useEffect(() => { + if (!isCountdownRunning) { + return; + } + + countdownRef.current?.resetCountdown(); + }, [isCountdownRunning]); + useEffect(() => { if (!isLoadingResendValidationForm) { return; @@ -360,13 +373,30 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco testID="validateCode" /> {hasError && } - + + {isCountdownRunning && !isOffline ? ( + + setIsCountdownRunning(false)} + /> + + ) : ( + + + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} + + + )} + )} From baf071f635a7f37d2bf97e066da0ede323327588 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 18 Nov 2025 08:44:32 +0100 Subject: [PATCH 7/8] feature: memoize the handleCountdownFinish method --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 6 +++++- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index eac8dce8f50a..5d610ae2da19 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -276,6 +276,10 @@ function BaseValidateCodeForm({ const shouldShowTimer = isCountdownRunning && !isOffline; + const handleCountdownFinish = useCallback(() => { + setIsCountdownRunning(false); + }, []); + // latestValidateCodeError only holds an error related to bad magic code // while validateError holds flow-specific errors const finalValidateError = !isEmptyObject(latestValidateCodeError) ? latestValidateCodeError : validateError; @@ -296,7 +300,7 @@ function BaseValidateCodeForm({ setIsCountdownRunning(false)} + onCountdownFinish={handleCountdownFinish} /> )} diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 78253a40f207..d2deeed34ea7 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -302,6 +302,10 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco preferredLocale, ]); + const handleCountdownFinish = useCallback(() => { + setIsCountdownRunning(false); + }, []); + return ( {/* At this point, show 2FA only after the user has submitted a magic code and account requires 2FA */} @@ -378,7 +382,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco setIsCountdownRunning(false)} + onCountdownFinish={handleCountdownFinish} /> ) : ( From ab63bdec768a44492cb81f737d47e4cf0dde8abc Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 20 Nov 2025 08:37:56 +0100 Subject: [PATCH 8/8] feat: add displayName to ValidateCodeCountdown --- src/pages/signin/ValidateCodeCountdown/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/ValidateCodeCountdown/index.tsx b/src/pages/signin/ValidateCodeCountdown/index.tsx index a3d43cbc8da1..5581dffd9ffb 100644 --- a/src/pages/signin/ValidateCodeCountdown/index.tsx +++ b/src/pages/signin/ValidateCodeCountdown/index.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import CONST from '@src/CONST'; import type {ValidateCodeCountdownHandle, ValidateCodeCountdownProps} from './types'; -const ValidateCodeCountdown = forwardRef(({onCountdownFinish}, ref) => { +function ValidateCodeCountdown({onCountdownFinish}: ValidateCodeCountdownProps, ref: React.ForwardedRef) { const {translate} = useLocalize(); const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY); @@ -34,6 +34,8 @@ const ValidateCodeCountdown = forwardRef ); -}); +} -export default ValidateCodeCountdown; +ValidateCodeCountdown.displayName = 'ValidateCodeCountdown'; + +export default forwardRef(ValidateCodeCountdown);