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

Kotlin Clean架构下Firebase电话认证Recaptcha及Error Code39问题

在Clean架构下实现Firebase手机号认证的问题解决

问题描述

我在Kotlin中采用Clean架构实现Firebase Phone Authentication时,遇到两个核心问题:

  • 未在PhoneAuthOptions构建器中添加Activity,触发错误:'App verification failed - a valid Activity is required to complete the Recaptcha flow'
  • 尝试规避传递Activity到ViewModel后,出现新错误:
[SmsRetrieverHelper] SMS validation code request failed: unknown status code: 17499 Error Code:39
2024-08-22 16:40:42.831 7414-7414 ContentValues com.example.poputka W onVerificationFailed
com.google.firebase.FirebaseException: An internal error occurred. [ Error Code:39 ]

已在Firebase项目中添加SHA-256和SHA-1指纹,本以为无需Recaptcha,但实际仍触发相关校验。同时不想向ViewModel传递Activity引用(违反Clean架构规范),求可行解决方案。

现有实现代码

class AuthenticationDataSourceImpl @Inject constructor(private val auth: FirebaseAuth) :
    AuthenticationDataSource {

    override suspend fun sendVerificationCode(phoneNumber: String): AuthFirebaseResult =
        suspendCancellableCoroutine { continuation ->
            val options = PhoneAuthOptions.newBuilder(auth)
                .setPhoneNumber(phoneNumber)
                .setTimeout(60L, java.util.concurrent.TimeUnit.SECONDS)
                .setCallbacks(object : OnVerificationStateChangedCallbacks() {
                    override fun onVerificationCompleted(credential: PhoneAuthCredential) {
                        Log.d(TAG, "onVerificationCompleted:$credential")

                        if (continuation.isActive) {
                            continuation.resume(AuthFirebaseResult.PhoneNumberVerified(credential) )
                        }
                    }

                    override fun onVerificationFailed(e: FirebaseException) {
                        Log.w(TAG, "onVerificationFailed", e)
                        if (continuation.isActive) {
                            continuation.resume(AuthFirebaseResult.Exception(e))
                        }
                    }

                     override fun onCodeSent(
                         verificationId: String,
                         token: PhoneAuthProvider.ForceResendingToken,
                     ) {
                         Log.d(TAG, "onCodeSent:$verificationId")
                         if (continuation.isActive) {
                             continuation.resume(AuthFirebaseResult.VerificationCodeSent(verificationId))
                         }
                     }
                })
                .build()

            verifyPhoneNumber(options)

            continuation.invokeOnCancellation {
                // Handle the cancellation if necessary
            }
        }


    override suspend fun signInWithPhoneAuthCredential(credential: PhoneAuthCredential): NetworkResult<AuthResult> =
        suspendCancellableCoroutine { continuation ->
            auth.signInWithCredential(credential)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        // Sign in success, update UI with the signed-in user's information
                        Log.d("TAG", "signInWithCredential:success")

                        val user = task.result?.user

                        Log.d("user", user.toString())
                        continuation.resume(NetworkResult.Success(task.result!!))
                    } else {
                        // Sign in failed, display a message and update the UI
                        Log.w("TAG", "signInWithCredential:failure", task.exception)
                        if (task.exception is FirebaseAuthInvalidCredentialsException) {
                            // The verification code entered was invalid
                            continuation.resume(
                                NetworkResult.Exception(
                                    (task.exception as FirebaseAuthInvalidCredentialsException).cause
                                        ?: Exception()
                                )
                            )
                        }
                    }
                }
        }

    override suspend fun getCredential(verificationId: String, code: String): NetworkResult<PhoneAuthCredential> {
        return try {
            val credential = PhoneAuthProvider.getCredential(verificationId, code)
            NetworkResult.Success(credential)
        } catch (e: Exception) {
            NetworkResult.Exception(e)
        }
    }
}

解决方案

1. 明确Firebase认证的验证逻辑

添加SHA指纹只是启用自动短信检索的前提,但Firebase会根据设备环境(如模拟器、陌生设备)或风控策略触发Recaptcha校验,必须提供Activity上下文来加载Recaptcha的WebView弹窗,无法完全绕过。

2. 不违反Clean架构的Activity传递方案

不要让ViewModel或DataSource持有Activity引用,而是在**UI层(Fragment/Activity)**发起请求时传入,通过UseCase和ViewModel中转:

调整实现步骤:

  • 修改AuthenticationDataSource的sendVerificationCode方法签名,新增Activity参数:
    override suspend fun sendVerificationCode(phoneNumber: String, activity: Activity): AuthFirebaseResult =
        suspendCancellableCoroutine { continuation ->
            val options = PhoneAuthOptions.newBuilder(auth)
                .setPhoneNumber(phoneNumber)
                .setTimeout(60L, java.util.concurrent.TimeUnit.SECONDS)
                .setActivity(activity) // 添加上Activity引用
                .setCallbacks(...) // 原有回调逻辑不变
                .build()
            verifyPhoneNumber(options)
            // 其余取消逻辑不变
        }
    
  • 在UseCase和ViewModel中同步修改方法签名,仅作为参数中转,不持有Activity引用
  • 在Fragment/Activity中调用时传入this(Activity)或requireActivity()(Fragment),并确保在生命周期内完成认证流程

3. 修复Error Code:39错误

该错误通常由以下原因导致,逐一排查:

  • SHA指纹配置错误:检查Firebase控制台项目设置,确认添加的是当前构建变体(debug/release)的SHA-1和SHA-256指纹
  • 包名不匹配:核对Firebase控制台注册的应用包名与项目build.gradle中的包名完全一致
  • 依赖缺失:确保添加了短信检索服务依赖到app/build.gradle:
    implementation "com.google.android.gms:play-services-auth-api-phone:18.0.1"
    
  • 网络问题:确认设备能正常访问Firebase服务,可切换网络测试

4. 额外优化:避免内存泄漏

  • 在UI层使用viewLifecycleOwner.lifecycleScope启动协程调用认证方法,确保协程随UI生命周期销毁
  • ViewModel中不存储Activity引用,仅在调用UseCase时一次性传递

内容的提问来源于stack exchange,提问作者tereshkevich

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 07:24:53