From 8ed789baf96cc94c30a1736514acc612719803db Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 14 Jan 2026 14:29:25 +0000 Subject: [PATCH] fix(auth): ensure verify phone number waits for timeout to complete --- .../com/firebase/ui/auth/AuthException.kt | 16 ++++++++++ .../auth/ui/components/ErrorRecoveryDialog.kt | 6 ++++ .../auth/ui/screens/phone/PhoneAuthScreen.kt | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt index 63af11c73..46d22f068 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthException.kt +++ b/auth/src/main/java/com/firebase/ui/auth/AuthException.kt @@ -150,6 +150,22 @@ abstract class AuthException( cause: Throwable? = null ) : AuthException(message, cause) + /** + * Phone verification is in cooldown period for the same phone number. + * + * This exception is thrown when attempting to verify the same phone number + * again before the cooldown period (timeout) has expired. + * + * @property message The detailed error message + * @property cooldownSeconds The number of seconds remaining in the cooldown period + * @property cause The underlying [Throwable] that caused this exception + */ + class PhoneVerificationCooldownException( + message: String, + val cooldownSeconds: Long, + cause: Throwable? = null + ) : AuthException(message, cause) + /** * Multi-Factor Authentication is required to proceed. * diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt index 96c2e7978..eb1463a01 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt @@ -145,6 +145,10 @@ private fun getRecoveryMessage( } is AuthException.TooManyRequestsException -> stringProvider.tooManyRequestsRecoveryMessage + is AuthException.PhoneVerificationCooldownException -> { + // Use the custom message which includes remaining cooldown time + error.message ?: stringProvider.unknownErrorRecoveryMessage + } is AuthException.MfaRequiredException -> stringProvider.mfaRequiredRecoveryMessage is AuthException.AccountLinkingRequiredException -> { // Use the custom message which includes email and provider details @@ -194,6 +198,7 @@ private fun getRecoveryActionText( is AuthException.InvalidCredentialsException, is AuthException.WeakPasswordException, is AuthException.TooManyRequestsException, + is AuthException.PhoneVerificationCooldownException -> stringProvider.retryAction is AuthException.UnknownException -> stringProvider.retryAction else -> stringProvider.retryAction @@ -214,6 +219,7 @@ private fun isRecoverable(error: AuthException): Boolean { is AuthException.WeakPasswordException -> true is AuthException.EmailAlreadyInUseException -> true is AuthException.TooManyRequestsException -> false // User must wait + is AuthException.PhoneVerificationCooldownException -> false // User must wait for cooldown is AuthException.MfaRequiredException -> true is AuthException.AccountLinkingRequiredException -> true is AuthException.AuthCancelledException -> true diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt index 300b2112e..2a1690bdf 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt @@ -158,6 +158,8 @@ fun PhoneAuthScreen( val forceResendingToken = rememberSaveable { mutableStateOf(null) } val resendTimerSeconds = rememberSaveable { mutableIntStateOf(0) } + val pendingVerificationPhoneNumber = remember { mutableStateOf(null) } + val verificationStartTime = remember { mutableStateOf(null) } val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) val isLoading = authState is AuthState.Loading @@ -189,6 +191,9 @@ fun PhoneAuthScreen( } is AuthState.SMSAutoVerified -> { + // Auto-verification succeeded, clear pending verification tracking + pendingVerificationPhoneNumber.value = null + verificationStartTime.value = null // Auto-verification succeeded, sign in with the credential coroutineScope.launch { try { @@ -246,6 +251,33 @@ fun PhoneAuthScreen( onSendCodeClick = { coroutineScope.launch { try { + val currentTime = System.currentTimeMillis() + val timeoutMs = provider.timeout * 1000 + val timeSinceLastVerification = verificationStartTime.value?.let { + currentTime - it + } ?: Long.MAX_VALUE + + // Check if the same phone number is being verified again within the cooldown period + val storedNumber = pendingVerificationPhoneNumber.value + val isSameNumber = storedNumber != null && fullPhoneNumber == storedNumber + + // Check cooldown: same number and still within timeout period + if (isSameNumber && timeSinceLastVerification < timeoutMs) { + // Calculate remaining cooldown time in seconds + val remainingCooldownSeconds = ((timeoutMs - timeSinceLastVerification) / 1000).coerceAtLeast(1) + val cooldownException = AuthException.PhoneVerificationCooldownException( + message = "Please wait ${remainingCooldownSeconds} second${if (remainingCooldownSeconds != 1L) "s" else ""} before verifying the same phone number again. The cooldown period is ${provider.timeout} seconds.", + cooldownSeconds = remainingCooldownSeconds + ) + // Update auth state to show the error + authUI.updateAuthState(AuthState.Error(cooldownException)) + throw cooldownException + } + + // Track the phone number and start time for cooldown checking + pendingVerificationPhoneNumber.value = fullPhoneNumber + verificationStartTime.value = currentTime + authUI.verifyPhoneNumber( provider = provider, activity = activity,