Skip to content

Commit d487a31

Browse files
russellwheatleythatfiredev
authored andcommitted
fix(auth): ensure verify phone number waits for timeout to complete (#2303)
1 parent 4db213e commit d487a31

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

auth/src/main/java/com/firebase/ui/auth/AuthException.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ abstract class AuthException(
150150
cause: Throwable? = null
151151
) : AuthException(message, cause)
152152

153+
/**
154+
* Phone verification is in cooldown period for the same phone number.
155+
*
156+
* This exception is thrown when attempting to verify the same phone number
157+
* again before the cooldown period (timeout) has expired.
158+
*
159+
* @property message The detailed error message
160+
* @property cooldownSeconds The number of seconds remaining in the cooldown period
161+
* @property cause The underlying [Throwable] that caused this exception
162+
*/
163+
class PhoneVerificationCooldownException(
164+
message: String,
165+
val cooldownSeconds: Long,
166+
cause: Throwable? = null
167+
) : AuthException(message, cause)
168+
153169
/**
154170
* Multi-Factor Authentication is required to proceed.
155171
*

auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ private fun getRecoveryMessage(
149149
}
150150

151151
is AuthException.TooManyRequestsException -> stringProvider.tooManyRequestsRecoveryMessage
152+
is AuthException.PhoneVerificationCooldownException -> {
153+
// Use the custom message which includes remaining cooldown time
154+
error.message ?: stringProvider.unknownErrorRecoveryMessage
155+
}
152156
is AuthException.MfaRequiredException -> stringProvider.mfaRequiredRecoveryMessage
153157
is AuthException.AccountLinkingRequiredException -> {
154158
// Use the custom message which includes email and provider details
@@ -198,6 +202,7 @@ private fun getRecoveryActionText(
198202
is AuthException.InvalidCredentialsException,
199203
is AuthException.WeakPasswordException,
200204
is AuthException.TooManyRequestsException,
205+
is AuthException.PhoneVerificationCooldownException -> stringProvider.retryAction
201206
is AuthException.UnknownException -> stringProvider.retryAction
202207

203208
else -> stringProvider.retryAction
@@ -218,6 +223,7 @@ private fun isRecoverable(error: AuthException): Boolean {
218223
is AuthException.WeakPasswordException -> true
219224
is AuthException.EmailAlreadyInUseException -> true
220225
is AuthException.TooManyRequestsException -> false // User must wait
226+
is AuthException.PhoneVerificationCooldownException -> false // User must wait for cooldown
221227
is AuthException.MfaRequiredException -> true
222228
is AuthException.AccountLinkingRequiredException -> true
223229
is AuthException.AuthCancelledException -> true

auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ fun PhoneAuthScreen(
158158
val forceResendingToken =
159159
rememberSaveable { mutableStateOf<PhoneAuthProvider.ForceResendingToken?>(null) }
160160
val resendTimerSeconds = rememberSaveable { mutableIntStateOf(0) }
161+
val pendingVerificationPhoneNumber = remember { mutableStateOf<String?>(null) }
162+
val verificationStartTime = remember { mutableStateOf<Long?>(null) }
161163

162164
val authState by authUI.authStateFlow().collectAsState(AuthState.Idle)
163165
val isLoading = authState is AuthState.Loading
@@ -190,6 +192,10 @@ fun PhoneAuthScreen(
190192

191193
is AuthState.SMSAutoVerified -> {
192194
// Auto-verification succeeded, sign in with the credential
195+
// and clear pending verification tracking
196+
pendingVerificationPhoneNumber.value = null
197+
verificationStartTime.value = null
198+
193199
coroutineScope.launch {
194200
try {
195201
authUI.signInWithPhoneAuthCredential(
@@ -247,6 +253,33 @@ fun PhoneAuthScreen(
247253
onSendCodeClick = {
248254
coroutineScope.launch {
249255
try {
256+
val currentTime = System.currentTimeMillis()
257+
val timeoutMs = provider.timeout * 1000
258+
val timeSinceLastVerification = verificationStartTime.value?.let {
259+
currentTime - it
260+
} ?: Long.MAX_VALUE
261+
262+
// Check if the same phone number is being verified again within the cooldown period
263+
val storedNumber = pendingVerificationPhoneNumber.value
264+
val isSameNumber = storedNumber != null && fullPhoneNumber == storedNumber
265+
266+
// Check cooldown: same number and still within timeout period
267+
if (isSameNumber && timeSinceLastVerification < timeoutMs) {
268+
// Calculate remaining cooldown time in seconds
269+
val remainingCooldownSeconds = ((timeoutMs - timeSinceLastVerification) / 1000).coerceAtLeast(1)
270+
val cooldownException = AuthException.PhoneVerificationCooldownException(
271+
message = "Please wait ${remainingCooldownSeconds} second${if (remainingCooldownSeconds != 1L) "s" else ""} before verifying the same phone number again. The cooldown period is ${provider.timeout} seconds.",
272+
cooldownSeconds = remainingCooldownSeconds
273+
)
274+
// Update auth state to show the error
275+
authUI.updateAuthState(AuthState.Error(cooldownException))
276+
throw cooldownException
277+
}
278+
279+
// Track the phone number and start time for cooldown checking
280+
pendingVerificationPhoneNumber.value = fullPhoneNumber
281+
verificationStartTime.value = currentTime
282+
250283
authUI.verifyPhoneNumber(
251284
provider = provider,
252285
activity = activity,

0 commit comments

Comments
 (0)