diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab04069e9a..8d5d62d981 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -551,6 +551,7 @@ dependencies { implementation(libs.accompanist.permissions) implementation(libs.kotlin.stdlib.jdk8) implementation(libs.kotlin.reflect) + implementation(libs.kotlinx.coroutines.play.services) implementation(libs.jackson.module.kotlin) implementation(libs.rxjava3.rxandroid) implementation(libs.rxjava3.rxkotlin) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index 80279e706c..c09f2bda4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -8,9 +8,12 @@ package org.thoughtcrime.securesms.registration.v2.data import android.app.backup.BackupManager import android.content.Context import androidx.core.app.NotificationManagerCompat +import com.google.android.gms.auth.api.phone.SmsRetriever import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.signal.core.util.Base64 @@ -62,6 +65,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import java.io.IOException @@ -262,6 +266,7 @@ object RegistrationRepository { suspend fun validateSession(context: Context, sessionId: String, e164: String, password: String): RegistrationSessionCheckResult = withContext(Dispatchers.IO) { val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + Log.d(TAG, "Validating registration session with service.") val registrationSessionResult = api.getRegistrationSessionStatus(sessionId) return@withContext RegistrationSessionCheckResult.from(registrationSessionResult) } @@ -275,12 +280,15 @@ object RegistrationRepository { val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi val registrationSessionResult = if (fcmToken == null) { + Log.d(TAG, "Creating registration session without FCM token.") api.createRegistrationSession(null, mcc, mnc) } else { + Log.d(TAG, "Creating registration session with FCM token.") createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) } val result = RegistrationSessionCreationResult.from(registrationSessionResult) if (result is RegistrationSessionCreationResult.Success) { + Log.d(TAG, "Updating registration session and E164 in value store.") SignalStore.registrationValues().sessionId = result.getMetadata().body.id SignalStore.registrationValues().sessionE164 = e164 } @@ -293,9 +301,13 @@ object RegistrationRepository { */ suspend fun createOrValidateSession(context: Context, sessionId: String?, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionResult { if (sessionId != null) { + Log.d(TAG, "Validating existing registration session.") val sessionValidationResult = validateSession(context, sessionId, e164, password) when (sessionValidationResult) { - is RegistrationSessionCheckResult.Success -> return sessionValidationResult + is RegistrationSessionCheckResult.Success -> { + Log.d(TAG, "Existing registration session is valid.") + return sessionValidationResult + } is RegistrationSessionCheckResult.UnknownError -> { Log.w(TAG, "Encountered error when validating existing session.", sessionValidationResult.getCause()) return sessionValidationResult @@ -313,19 +325,11 @@ object RegistrationRepository { /** * Asks the service to send a verification code through one of our supported channels (SMS, phone call). */ - suspend fun requestSmsCode(context: Context, sessionId: String, e164: String, password: String, mode: Mode = Mode.SMS_WITHOUT_LISTENER): VerificationCodeRequestResult = + suspend fun requestSmsCode(context: Context, sessionId: String, e164: String, password: String, mode: Mode): VerificationCodeRequestResult = withContext(Dispatchers.IO) { val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - // TODO [regv2]: support other verification code [Mode] options - val codeRequestResult = if (mode == Mode.PHONE_CALL) { - // TODO [regv2] - val notImplementedError = NotImplementedError() - Log.w(TAG, "Not yet implemented!", notImplementedError) - NetworkResult.ApplicationError(notImplementedError) - } else { - api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported) - } + val codeRequestResult = api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported, mode.transport) return@withContext VerificationCodeRequestResult.from(codeRequestResult) } @@ -342,7 +346,7 @@ object RegistrationRepository { /** * Submits the solved captcha token to the service. */ - suspend fun submitCaptchaToken(context: Context, e164: String, password: String, sessionId: String, captchaToken: String) = + suspend fun submitCaptchaToken(context: Context, e164: String, password: String, sessionId: String, captchaToken: String): VerificationCodeRequestResult = withContext(Dispatchers.IO) { val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi val captchaSubmissionResult = api.submitCaptchaToken(sessionId = sessionId, captchaToken = captchaToken) @@ -408,6 +412,7 @@ object RegistrationRepository { eventBus.register(subscriber) try { + Log.d(TAG, "Requesting a registration session with FCM token…") val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc) if (sessionCreationResponse !is NetworkResult.Success) { return@withContext sessionCreationResponse @@ -450,7 +455,13 @@ object RegistrationRepository { val usernamePasswords = async { retrieveLocalSvrCredentials() } val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi - val result = api.getSvrAuthCredential(e164, usernamePasswords.await()) + val authTokens = usernamePasswords.await() + + if (authTokens.isEmpty()) { + return@withContext BackupAuthCheckResult.SuccessWithoutCredentials() + } + + val result = api.getSvrAuthCredential(e164, authTokens) .runIfSuccessful { val removedInvalidTokens = SignalStore.svr().removeAuthTokens(it.invalid) if (removedInvalidTokens) { @@ -484,8 +495,37 @@ object RegistrationRepository { .toList() } - enum class Mode(val isSmsRetrieverSupported: Boolean) { - SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false) + /** + * Starts an SMS listener to auto-enter a verification code. + * + * The listener [lives for 5 minutes](https://developers.google.com/android/reference/com/google/android/gms/auth/api/phone/SmsRetrieverApi). + * + * @return whether or not the Play Services SMS Listener was successfully registered. + */ + suspend fun registerSmsListener(context: Context): Boolean { + Log.d(TAG, "Attempting to start verification code SMS retriever.") + val started = withTimeoutOrNull(5.seconds.inWholeMilliseconds) { + try { + SmsRetriever.getClient(context).startSmsRetriever().await() + Log.d(TAG, "Successfully started verification code SMS retriever.") + return@withTimeoutOrNull true + } catch (ex: Exception) { + Log.w(TAG, "Could not start verification code SMS retriever due to exception.", ex) + return@withTimeoutOrNull false + } + } + + if (started == null) { + Log.w(TAG, "Could not start verification code SMS retriever due to timeout.") + } + + return started == true + } + + enum class Mode(val isSmsRetrieverSupported: Boolean, val transport: PushServiceSocket.VerificationCodeTransport) { + SMS_WITH_LISTENER(true, PushServiceSocket.VerificationCodeTransport.SMS), + SMS_WITHOUT_LISTENER(false, PushServiceSocket.VerificationCodeTransport.SMS), + PHONE_CALL(false, PushServiceSocket.VerificationCodeTransport.VOICE) } private class PushTokenChallengeSubscriber { @@ -494,6 +534,7 @@ object RegistrationRepository { @Subscribe fun onChallengeEvent(pushChallengeEvent: PushChallengeRequest.PushChallengeEvent) { + Log.d(TAG, "Push challenge received!") challenge = pushChallengeEvent.challenge latch.countDown() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt index f44aff463b..1c4cfa8312 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt @@ -28,6 +28,7 @@ data class RegistrationV2State( val captchaToken: String? = null, val nextSms: Long = 0L, val nextCall: Long = 0L, + val smsListenerTimeout: Long = 0L, val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, val networkError: Throwable? = null ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index c7d4efc566..0e2eaeaf42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -55,6 +55,7 @@ import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.internal.push.LockedException import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import java.io.IOException +import kotlin.time.Duration.Companion.minutes /** * ViewModel shared across all of registration. @@ -124,10 +125,12 @@ class RegistrationV2ViewModel : ViewModel() { } private suspend fun updateFcmToken(context: Context): String? { + Log.d(TAG, "Fetching FCM token…") val fcmToken = RegistrationRepository.getFcmToken(context) store.update { it.copy(fcmToken = fcmToken) } + Log.d(TAG, "FCM token fetched.") return fcmToken } @@ -147,8 +150,6 @@ class RegistrationV2ViewModel : ViewModel() { return } - // TODO [regv2]: initialize Play Services sms retriever - val mccMncProducer = MccMncProducer(context) val e164 = state.phoneNumber.toE164() if (hasRecoveryPassword() && matchesSavedE164(e164)) { // Re-registration when the local database is intact. @@ -187,14 +188,81 @@ class RegistrationV2ViewModel : ViewModel() { return@launch } - val codeRequestResponse = RegistrationRepository.requestSmsCode(context, validSession.body.id, e164, password) + requestSmsCodeInternal(context, validSession.body.id, e164) + } + } + + fun requestSmsCode(context: Context) { + val e164 = getCurrentE164() + + if (e164 == null) { + Log.w(TAG, "Phone number was null after confirmation.") + onErrorOccurred() + return + } + + viewModelScope.launch { + val validSession = getOrCreateValidSession(context) ?: return@launch + requestSmsCodeInternal(context, validSession.body.id, e164) + } + } + + fun requestVerificationCall(context: Context) { + val e164 = getCurrentE164() + + if (e164 == null) { + Log.w(TAG, "Phone number was null after confirmation.") + onErrorOccurred() + return + } + + viewModelScope.launch { + val validSession = getOrCreateValidSession(context) ?: return@launch + Log.d(TAG, "Requesting voice call code…") + val codeRequestResponse = RegistrationRepository.requestSmsCode( + context = context, + sessionId = validSession.body.id, + e164 = e164, + password = password, + mode = RegistrationRepository.Mode.PHONE_CALL + ) + Log.d(TAG, "Voice call code request submitted.") handleSessionStateResult(context, codeRequestResponse) } } + private suspend fun requestSmsCodeInternal(context: Context, sessionId: String, e164: String) { + var smsListenerReady = false + Log.d(TAG, "Captcha token submitted.") + if (store.value.smsListenerTimeout < System.currentTimeMillis()) { + smsListenerReady = store.value.isFcmSupported && RegistrationRepository.registerSmsListener(context) + + if (smsListenerReady) { + val smsRetrieverTimeout = System.currentTimeMillis() + 5.minutes.inWholeMilliseconds + Log.d(TAG, "Successfully started verification code SMS retriever, which will last until $smsRetrieverTimeout.") + store.update { it.copy(smsListenerTimeout = smsRetrieverTimeout) } + } else { + Log.d(TAG, "Could not start verification code SMS retriever.") + } + } + + Log.d(TAG, "Requesting SMS code…") + val codeRequestResponse = RegistrationRepository.requestSmsCode( + context = context, + sessionId = sessionId, + e164 = e164, + password = password, + mode = if (smsListenerReady) RegistrationRepository.Mode.SMS_WITH_LISTENER else RegistrationRepository.Mode.SMS_WITHOUT_LISTENER + ) + Log.d(TAG, "SMS code request submitted.") + + handleSessionStateResult(context, codeRequestResponse) + } + private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!") + Log.d(TAG, "Validating/creating a registration session.") val mccMncProducer = MccMncProducer(context) val existingSessionId = store.value.sessionId @@ -210,6 +278,7 @@ class RegistrationV2ViewModel : ViewModel() { ) } } + Log.d(TAG, "Registration session validated.") return metadata } is RegistrationSessionCreationResult.Success -> { @@ -222,27 +291,46 @@ class RegistrationV2ViewModel : ViewModel() { ) } } + Log.d(TAG, "Registration session created.") return metadata } - is RegistrationSessionCheckResult.SessionNotFound -> Log.w(TAG, "This should be impossible to reach at this stage; it should have been handled in RegistrationRepository.", sessionResult.getCause()) - is RegistrationSessionCheckResult.UnknownError -> Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) - is RegistrationSessionCreationResult.MalformedRequest -> Log.i(TAG, "Malformed request error occurred while creating registration session.", sessionResult.getCause()) - is RegistrationSessionCreationResult.RateLimited -> Log.i(TAG, "Rate limit occurred while creating registration session.", sessionResult.getCause()) - is RegistrationSessionCreationResult.ServerUnableToParse -> Log.i(TAG, "Server unable to parse request for creating registration session.", sessionResult.getCause()) - is RegistrationSessionCreationResult.UnknownError -> Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) + is RegistrationSessionCheckResult.SessionNotFound -> { + Log.w(TAG, "This should be impossible to reach at this stage; it should have been handled in RegistrationRepository.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } + is RegistrationSessionCheckResult.UnknownError -> { + Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } + is RegistrationSessionCreationResult.MalformedRequest -> { + Log.i(TAG, "Malformed request error occurred while creating registration session.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } + is RegistrationSessionCreationResult.RateLimited -> { + Log.i(TAG, "Rate limit occurred while creating registration session.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } + is RegistrationSessionCreationResult.ServerUnableToParse -> { + Log.i(TAG, "Server unable to parse request for creating registration session.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } + is RegistrationSessionCreationResult.UnknownError -> { + Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) + handleGenericError(sessionResult.getCause()) + } } - setInProgress(false) return null } fun submitCaptchaToken(context: Context) { val e164 = getCurrentE164() ?: throw IllegalStateException("TODO") - val sessionId = store.value.sessionId ?: throw IllegalStateException("TODO") val captchaToken = store.value.captchaToken ?: throw IllegalStateException("TODO") viewModelScope.launch { - val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, sessionId, captchaToken) - + val session = getOrCreateValidSession(context) ?: return@launch + Log.d(TAG, "Submitting captcha token…") + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) + Log.d(TAG, "Captcha token submitted.") handleSessionStateResult(context, captchaSubmissionResult) } } @@ -258,6 +346,7 @@ class RegistrationV2ViewModel : ViewModel() { } is Success -> { + Log.d(TAG, "New registration session status received.") updateFcmToken(context) store.update { it.copy( @@ -273,10 +362,12 @@ class RegistrationV2ViewModel : ViewModel() { is AttemptsExhausted -> Log.w(TAG, "TODO") is ChallengeRequired -> store.update { // TODO [regv2] handle push challenge required + Log.d(TAG, "[${sessionResult.challenges.joinToString()}] registration challenges received.") it.copy( registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED ) } + is ImpossibleNumber -> Log.w(TAG, "TODO") is NonNormalizedNumber -> Log.w(TAG, "TODO") is RateLimited -> Log.w(TAG, "TODO") @@ -447,6 +538,12 @@ class RegistrationV2ViewModel : ViewModel() { RegistrationUtil.maybeMarkRegistrationComplete() } + fun clearNetworkError() { + store.update { + it.copy(networkError = null) + } + } + private fun matchesSavedE164(e164: String?): Boolean { return if (e164 == null) { false diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt index ac31b27215..5dd792cedc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt @@ -58,6 +58,20 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it) } + binding.callMeCountDown.apply { + setTextResources(R.string.RegistrationActivity_call, R.string.RegistrationActivity_call_me_instead_available_in) + setOnClickListener { + sharedViewModel.requestVerificationCall(requireContext()) + } + } + + binding.resendSmsCountDown.apply { + setTextResources(R.string.RegistrationActivity_resend_code, R.string.RegistrationActivity_resend_sms_available_in) + setOnClickListener { + sharedViewModel.requestSmsCode(requireContext()) + } + } + binding.keyboard.setOnKeyPressListener { key -> if (!autopilotCodeEntryActive) { if (key >= 0) { @@ -67,6 +81,11 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter } } } + + sharedViewModel.uiState.observe(viewLifecycleOwner) { + binding.resendSmsCountDown.startCountDownTo(it.nextSms) + binding.callMeCountDown.startCountDownTo(it.nextCall) + } } private fun popBackStack() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt index ac62477c1e..a52168657b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt @@ -243,12 +243,14 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio } private fun presentNetworkError(networkError: Throwable) { - // TODO [regv2]: check specific errors with a when clause Log.i(TAG, "Unknown error during verification code request", networkError) - MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.RegistrationActivity_unable_to_connect_to_service) - .setPositiveButton(android.R.string.ok, null) - .show() + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_unable_to_connect_to_service) + setPositiveButton(android.R.string.ok) { _, _ -> sharedViewModel.clearNetworkError() } + setOnCancelListener { sharedViewModel.clearNetworkError() } + setOnDismissListener { sharedViewModel.clearNetworkError() } + show() + } } private fun onRegistrationButtonClicked() { diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 5aaef221c7..10dcad6d08 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -46,6 +46,7 @@ dependencyResolutionManagement { library("kotlin-reflect", "org.jetbrains.kotlin", "kotlin-reflect").versionRef("kotlin") library("kotlin-gradle-plugin", "org.jetbrains.kotlin", "kotlin-gradle-plugin").versionRef("kotlin") library("ktlint", "org.jlleitschuh.gradle:ktlint-gradle:11.4.2") + library("kotlinx-coroutines-play-services", "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1") // Android X library("androidx-activity-ktx", "androidx.activity", "activity-ktx").versionRef("androidx-activity") diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8904302400..057f3623d2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5050,6 +5050,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + @@ -5100,6 +5111,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -5201,6 +5217,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -5238,6 +5262,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -5264,6 +5296,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -5296,6 +5336,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index a3fa4a317a..0ecff8ae87 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -54,9 +54,9 @@ class RegistrationApi( * * @param androidSmsRetrieverSupported whether the system framework will automatically parse the incoming verification message. */ - fun requestSmsVerificationCode(sessionId: String?, locale: Locale?, androidSmsRetrieverSupported: Boolean): NetworkResult { + fun requestSmsVerificationCode(sessionId: String?, locale: Locale?, androidSmsRetrieverSupported: Boolean, transport: PushServiceSocket.VerificationCodeTransport): NetworkResult { return NetworkResult.fromFetch { - pushServiceSocket.requestVerificationCode(sessionId, locale, androidSmsRetrieverSupported, PushServiceSocket.VerificationCodeTransport.SMS) + pushServiceSocket.requestVerificationCode(sessionId, locale, androidSmsRetrieverSupported, transport) } }