mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Fix bugs around requesting and entering verification codes.
This commit is contained in:
@@ -74,7 +74,6 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
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 org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.io.IOException
|
||||
@@ -307,7 +306,7 @@ object RegistrationRepository {
|
||||
val result = RegistrationSessionCreationResult.from(registrationSessionResult)
|
||||
if (result is RegistrationSessionCreationResult.Success) {
|
||||
Log.d(TAG, "Updating registration session and E164 in value store.")
|
||||
SignalStore.registration.sessionId = result.getMetadata().metadata.id
|
||||
SignalStore.registration.sessionId = result.sessionId
|
||||
SignalStore.registration.sessionE164 = e164
|
||||
}
|
||||
|
||||
@@ -488,16 +487,6 @@ object RegistrationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deriveTimestamp(headers: RegistrationSessionMetadataHeaders, deltaSeconds: Int?): Long {
|
||||
if (deltaSeconds == null) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
val timestamp: Long = headers.serverDeliveredTimestamp
|
||||
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
||||
}
|
||||
|
||||
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
|
||||
@@ -13,23 +12,37 @@ import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionExcepti
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class RegistrationSessionResult(cause: Throwable?) : RegistrationResult(cause)
|
||||
|
||||
interface SessionMetadataHolder {
|
||||
fun getMetadata(): RegistrationSessionMetadataResponse
|
||||
interface SessionMetadataResult {
|
||||
val sessionId: String
|
||||
val nextSmsTimestamp: Duration
|
||||
val nextCallTimestamp: Duration
|
||||
val nextVerificationAttempt: Duration
|
||||
val allowedToRequestCode: Boolean
|
||||
val challengesRequested: List<Challenge>
|
||||
val verified: Boolean
|
||||
}
|
||||
|
||||
sealed class RegistrationSessionCreationResult(cause: Throwable?) : RegistrationSessionResult(cause) {
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(RegistrationSessionResult::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCreationResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> {
|
||||
Success(networkResult.result)
|
||||
Success(
|
||||
sessionId = networkResult.result.metadata.id,
|
||||
nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
|
||||
nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
|
||||
nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
|
||||
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
|
||||
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
|
||||
verified = networkResult.result.metadata.verified
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
@@ -49,11 +62,15 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration
|
||||
}
|
||||
}
|
||||
|
||||
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCreationResult(null), SessionMetadataHolder {
|
||||
override fun getMetadata(): RegistrationSessionMetadataResponse {
|
||||
return metadata
|
||||
}
|
||||
}
|
||||
class Success(
|
||||
override val sessionId: String,
|
||||
override val nextSmsTimestamp: Duration,
|
||||
override val nextCallTimestamp: Duration,
|
||||
override val nextVerificationAttempt: Duration,
|
||||
override val allowedToRequestCode: Boolean,
|
||||
override val challengesRequested: List<Challenge>,
|
||||
override val verified: Boolean
|
||||
) : RegistrationSessionCreationResult(null), SessionMetadataResult
|
||||
|
||||
class RateLimited(cause: Throwable, val timeRemaining: Long?) : RegistrationSessionCreationResult(cause)
|
||||
class AttemptsExhausted(cause: Throwable) : RegistrationSessionCreationResult(cause)
|
||||
@@ -67,7 +84,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
|
||||
fun from(networkResult: NetworkResult<RegistrationSessionMetadataResponse>): RegistrationSessionCheckResult {
|
||||
return when (networkResult) {
|
||||
is NetworkResult.Success -> {
|
||||
Success(networkResult.result)
|
||||
Success(
|
||||
sessionId = networkResult.result.metadata.id,
|
||||
nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
|
||||
nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
|
||||
nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
|
||||
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
|
||||
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
|
||||
verified = networkResult.result.metadata.verified
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
|
||||
@@ -82,11 +107,15 @@ sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSes
|
||||
}
|
||||
}
|
||||
|
||||
class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCheckResult(null), SessionMetadataHolder {
|
||||
override fun getMetadata(): RegistrationSessionMetadataResponse {
|
||||
return metadata
|
||||
}
|
||||
}
|
||||
class Success(
|
||||
override val sessionId: String,
|
||||
override val nextSmsTimestamp: Duration,
|
||||
override val nextCallTimestamp: Duration,
|
||||
override val nextVerificationAttempt: Duration,
|
||||
override val allowedToRequestCode: Boolean,
|
||||
override val challengesRequested: List<Challenge>,
|
||||
override val verified: Boolean
|
||||
) : RegistrationSessionCheckResult(null), SessionMetadataResult
|
||||
|
||||
class SessionNotFound(cause: Throwable) : RegistrationSessionCheckResult(cause)
|
||||
class UnknownError(cause: Throwable) : RegistrationSessionCheckResult(cause)
|
||||
|
||||
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.registration.data.network
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException
|
||||
@@ -27,6 +26,7 @@ import org.whispersystems.signalservice.internal.push.LockedException
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes.
|
||||
@@ -47,9 +47,9 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
|
||||
} else {
|
||||
Success(
|
||||
sessionId = networkResult.result.metadata.id,
|
||||
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextSms),
|
||||
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextCall),
|
||||
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextVerificationAttempt),
|
||||
nextSmsTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextSms?.seconds),
|
||||
nextCallTimestamp = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextCall?.seconds),
|
||||
nextVerificationAttempt = networkResult.result.deriveTimestamp(delta = networkResult.result.metadata.nextVerificationAttempt?.seconds),
|
||||
allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode,
|
||||
challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation),
|
||||
verified = networkResult.result.metadata.verified
|
||||
@@ -75,8 +75,8 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
|
||||
is RequestVerificationCodeRateLimitException -> {
|
||||
RequestVerificationCodeRateLimited(
|
||||
cause = cause,
|
||||
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextSms),
|
||||
nextCallTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextCall)
|
||||
nextSmsTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextSms?.seconds),
|
||||
nextCallTimestamp = cause.sessionMetadata.deriveTimestamp(delta = cause.sessionMetadata.metadata.nextCall?.seconds)
|
||||
)
|
||||
}
|
||||
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials)
|
||||
@@ -93,7 +93,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
|
||||
}
|
||||
}
|
||||
|
||||
class Success(val sessionId: String, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, nextVerificationAttempt: Long, val allowedToRequestCode: Boolean, challengesRequested: List<Challenge>, val verified: Boolean) : VerificationCodeRequestResult(null)
|
||||
class Success(val sessionId: String, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration, nextVerificationAttempt: Duration, val allowedToRequestCode: Boolean, challengesRequested: List<Challenge>, val verified: Boolean) : VerificationCodeRequestResult(null)
|
||||
|
||||
class ChallengeRequired(val challenges: List<Challenge>) : VerificationCodeRequestResult(null)
|
||||
|
||||
@@ -111,17 +111,17 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
|
||||
|
||||
class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause)
|
||||
|
||||
class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Long, val nextCallTimestamp: Long) : VerificationCodeRequestResult(cause) {
|
||||
val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0 || nextCallTimestamp > 0
|
||||
class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Duration, val nextCallTimestamp: Duration) : VerificationCodeRequestResult(cause) {
|
||||
val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0.seconds || nextCallTimestamp > 0.seconds
|
||||
fun log(now: Duration = System.currentTimeMillis().milliseconds): String {
|
||||
val sms = if (nextSmsTimestamp > 0) {
|
||||
"${(nextSmsTimestamp.milliseconds - now).inWholeSeconds}s"
|
||||
val sms = if (nextSmsTimestamp > 0.seconds) {
|
||||
"${(nextSmsTimestamp - now).inWholeSeconds}s"
|
||||
} else {
|
||||
"Never"
|
||||
}
|
||||
|
||||
val call = if (nextCallTimestamp > 0) {
|
||||
"${(nextCallTimestamp.milliseconds - now).inWholeSeconds}s"
|
||||
val call = if (nextCallTimestamp > 0.seconds) {
|
||||
"${(nextCallTimestamp - now).inWholeSeconds}s"
|
||||
} else {
|
||||
"Never"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
||||
private static final String TAG = Log.tag(SignalStrengthPhoneStateListener.class);
|
||||
|
||||
private final Callback callback;
|
||||
private final Debouncer debouncer = new Debouncer(1000);
|
||||
private final Debouncer debouncer = new Debouncer(1000);
|
||||
private volatile boolean hasLowSignal = true;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) {
|
||||
@@ -40,10 +41,14 @@ public final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
||||
if (signalStrength == null) return;
|
||||
|
||||
if (isLowLevel(signalStrength)) {
|
||||
hasLowSignal = true;
|
||||
Log.w(TAG, "No cell signal detected");
|
||||
debouncer.publish(callback::onNoCellSignalPresent);
|
||||
} else {
|
||||
Log.i(TAG, "Cell signal detected");
|
||||
if (hasLowSignal) {
|
||||
hasLowSignal = false;
|
||||
Log.i(TAG, "Cell signal detected");
|
||||
}
|
||||
debouncer.clear();
|
||||
callback.onCellSignalPresent();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionR
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* State holder shared across all of registration.
|
||||
@@ -41,9 +43,9 @@ data class RegistrationState(
|
||||
val challengesPresented: Set<Challenge> = emptySet(),
|
||||
val captchaToken: String? = null,
|
||||
val allowedToRequestCode: Boolean = false,
|
||||
val nextSmsTimestamp: Long = 0L,
|
||||
val nextCallTimestamp: Long = 0L,
|
||||
val nextVerificationAttempt: Long = 0L,
|
||||
val nextSmsTimestamp: Duration = 0.seconds,
|
||||
val nextCallTimestamp: Duration = 0.seconds,
|
||||
val nextVerificationAttempt: Duration = 0.seconds,
|
||||
val verified: Boolean = false,
|
||||
val smsListenerTimeout: Long = 0L,
|
||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResul
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified
|
||||
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired
|
||||
@@ -69,12 +70,11 @@ import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
@@ -271,26 +271,25 @@ class RegistrationViewModel : ViewModel() {
|
||||
|
||||
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
|
||||
|
||||
if (validSession.metadata.verified) {
|
||||
if (validSession.verified) {
|
||||
Log.i(TAG, "Session is already verified, registering account.")
|
||||
registerVerifiedSession(context, validSession.metadata.id)
|
||||
registerVerifiedSession(context, validSession.sessionId)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!validSession.metadata.allowedToRequestCode) {
|
||||
if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) {
|
||||
if (!validSession.allowedToRequestCode) {
|
||||
if (System.currentTimeMillis().milliseconds > validSession.nextVerificationAttempt) {
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
|
||||
}
|
||||
} else {
|
||||
val challenges = validSession.metadata.requestedInformation
|
||||
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}")
|
||||
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)))
|
||||
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${validSession.challengesRequested.joinToString()}")
|
||||
handleSessionStateResult(context, ChallengeRequired(validSession.challengesRequested))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
requestSmsCodeInternal(context, validSession.metadata.id, e164)
|
||||
requestSmsCodeInternal(context, validSession.sessionId, e164)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +298,7 @@ class RegistrationViewModel : ViewModel() {
|
||||
|
||||
viewModelScope.launch {
|
||||
val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
|
||||
requestSmsCodeInternal(context, validSession.metadata.id, e164)
|
||||
requestSmsCodeInternal(context, validSession.sessionId, e164)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +316,7 @@ class RegistrationViewModel : ViewModel() {
|
||||
Log.d(TAG, "Requesting voice call code…")
|
||||
val codeRequestResponse = RegistrationRepository.requestSmsCode(
|
||||
context = context,
|
||||
sessionId = validSession.metadata.id,
|
||||
sessionId = validSession.sessionId,
|
||||
e164 = e164,
|
||||
password = password,
|
||||
mode = RegistrationRepository.E164VerificationMode.PHONE_CALL
|
||||
@@ -375,7 +374,7 @@ class RegistrationViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? {
|
||||
private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
|
||||
Log.v(TAG, "getOrCreateValidSession()")
|
||||
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
|
||||
val mccMncProducer = MccMncProducer(context)
|
||||
@@ -388,16 +387,16 @@ class RegistrationViewModel : ViewModel() {
|
||||
password = password,
|
||||
mcc = mccMncProducer.mcc,
|
||||
mnc = mccMncProducer.mnc,
|
||||
successListener = { networkResult ->
|
||||
successListener = { sessionData ->
|
||||
store.update {
|
||||
it.copy(
|
||||
sessionId = networkResult.metadata.id,
|
||||
nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms),
|
||||
nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall),
|
||||
nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt),
|
||||
allowedToRequestCode = networkResult.metadata.allowedToRequestCode,
|
||||
challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation),
|
||||
verified = networkResult.metadata.verified
|
||||
sessionId = sessionData.sessionId,
|
||||
nextSmsTimestamp = sessionData.nextSmsTimestamp,
|
||||
nextCallTimestamp = sessionData.nextCallTimestamp,
|
||||
nextVerificationAttempt = sessionData.nextVerificationAttempt,
|
||||
allowedToRequestCode = sessionData.allowedToRequestCode,
|
||||
challengesRequested = sessionData.challengesRequested,
|
||||
verified = sessionData.verified
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -424,7 +423,7 @@ class RegistrationViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") }
|
||||
Log.d(TAG, "Submitting captcha token…")
|
||||
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken)
|
||||
val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.sessionId, captchaToken)
|
||||
Log.d(TAG, "Captcha token submitted.")
|
||||
|
||||
handleSessionStateResult(context, captchaSubmissionResult)
|
||||
@@ -442,12 +441,12 @@ class RegistrationViewModel : ViewModel() {
|
||||
Log.d(TAG, "Getting session in order to perform push token verification…")
|
||||
val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") }
|
||||
|
||||
if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) {
|
||||
if (!session.challengesRequested.contains(Challenge.PUSH)) {
|
||||
return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") }
|
||||
}
|
||||
|
||||
Log.d(TAG, "Requesting push challenge token…")
|
||||
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password)
|
||||
val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.sessionId, e164, password)
|
||||
Log.d(TAG, "Push challenge token submitted.")
|
||||
handleSessionStateResult(context, pushSubmissionResult)
|
||||
}
|
||||
@@ -748,8 +747,8 @@ class RegistrationViewModel : ViewModel() {
|
||||
|
||||
var reglock = registrationLocked
|
||||
|
||||
val session: RegistrationSessionMetadataJson? = getOrCreateValidSession(context)?.metadata
|
||||
val sessionId: String = session?.id ?: return
|
||||
val session: SessionMetadataResult? = getOrCreateValidSession(context)
|
||||
val sessionId: String = session?.sessionId ?: return
|
||||
val registrationData: RegistrationData = getRegistrationData()
|
||||
|
||||
if (session.verified) {
|
||||
@@ -965,24 +964,22 @@ class RegistrationViewModel : ViewModel() {
|
||||
password: String,
|
||||
mcc: String?,
|
||||
mnc: String?,
|
||||
successListener: (RegistrationSessionMetadataResponse) -> Unit,
|
||||
successListener: (SessionMetadataResult) -> Unit,
|
||||
errorHandler: (RegistrationSessionResult) -> Unit
|
||||
): RegistrationSessionMetadataResponse? {
|
||||
): SessionMetadataResult? {
|
||||
Log.d(TAG, "Validating/creating a registration session.")
|
||||
val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mcc, mnc)
|
||||
when (sessionResult) {
|
||||
is RegistrationSessionCheckResult.Success -> {
|
||||
val metadata = sessionResult.getMetadata()
|
||||
successListener(metadata)
|
||||
successListener(sessionResult)
|
||||
Log.d(TAG, "Registration session validated.")
|
||||
return metadata
|
||||
return sessionResult
|
||||
}
|
||||
|
||||
is RegistrationSessionCreationResult.Success -> {
|
||||
val metadata = sessionResult.getMetadata()
|
||||
successListener(metadata)
|
||||
successListener(sessionResult)
|
||||
Log.d(TAG, "Registration session created.")
|
||||
return metadata
|
||||
return sessionResult
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -19,12 +19,15 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding
|
||||
import org.thoughtcrime.securesms.registration.data.network.Challenge
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationResult
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult
|
||||
@@ -119,25 +122,31 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
||||
it.sessionCreationError?.let { error ->
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
sharedState.sessionCreationError?.let { error ->
|
||||
handleSessionCreationError(error)
|
||||
sharedViewModel.sessionCreationErrorShown()
|
||||
}
|
||||
|
||||
it.sessionStateError?.let { error ->
|
||||
sharedState.sessionStateError?.let { error ->
|
||||
handleSessionErrorResponse(error)
|
||||
sharedViewModel.sessionStateErrorShown()
|
||||
}
|
||||
|
||||
it.registerAccountError?.let { error ->
|
||||
sharedState.registerAccountError?.let { error ->
|
||||
handleRegistrationErrorResponse(error)
|
||||
sharedViewModel.registerAccountErrorShown()
|
||||
}
|
||||
|
||||
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp)
|
||||
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp)
|
||||
if (it.inProgress) {
|
||||
if (sharedState.challengesRequested.contains(Challenge.CAPTCHA) && sharedState.captchaToken.isNotNullOrBlank()) {
|
||||
sharedViewModel.submitCaptchaToken(requireContext())
|
||||
} else if (sharedState.challengesRemaining.isNotEmpty()) {
|
||||
handleChallenges(sharedState.challengesRemaining)
|
||||
}
|
||||
|
||||
binding.resendSmsCountDown.startCountDownTo(sharedState.nextSmsTimestamp)
|
||||
binding.callMeCountDown.startCountDownTo(sharedState.nextCallTimestamp)
|
||||
if (sharedState.inProgress) {
|
||||
binding.keyboard.displayProgress()
|
||||
} else {
|
||||
binding.keyboard.displayKeyboard()
|
||||
@@ -173,6 +182,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
|
||||
if (!result.isSuccess()) {
|
||||
Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause())
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is RegistrationSessionCheckResult.Success,
|
||||
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
@@ -219,6 +229,7 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
|
||||
handleRequestVerificationCodeRateLimited(result)
|
||||
}
|
||||
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited()
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
}
|
||||
@@ -239,6 +250,13 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChallenges(remainingChallenges: List<Challenge>) {
|
||||
when (remainingChallenges.first()) {
|
||||
Challenge.CAPTCHA -> moveToCaptcha()
|
||||
Challenge.PUSH -> sharedViewModel.requestAndSubmitPushToken(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentAccountLocked() {
|
||||
binding.keyboard.displayLocked().addListener(
|
||||
object : AssertedSuccessListener<Boolean>() {
|
||||
@@ -363,6 +381,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun moveToCaptcha() {
|
||||
findNavController().safeNavigate(EnterCodeFragmentDirections.actionRequestCaptcha())
|
||||
ThreadUtil.postToMain { sharedViewModel.setInProgress(false) }
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
|
||||
Log.i(TAG, "Received verification code via EventBus.")
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -25,6 +24,8 @@ import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
@@ -32,6 +33,7 @@ import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.i18n.phonenumbers.AsYouTypeFormatter
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
@@ -85,7 +87,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
private lateinit var phoneNumberInputLayout: TextInputEditText
|
||||
private lateinit var spinnerView: MaterialAutoCompleteTextView
|
||||
|
||||
private var currentPhoneNumberFormatter: TextWatcher? = null
|
||||
private var currentPhoneNumberFormatter: AsYouTypeFormatter? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -148,13 +150,17 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
}
|
||||
}
|
||||
|
||||
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
|
||||
fragmentState.phoneNumberFormatter?.let {
|
||||
bindPhoneNumberFormatter(it)
|
||||
fragmentViewModel
|
||||
.uiState
|
||||
.map { it.phoneNumberRegionCode }
|
||||
.distinctUntilChanged()
|
||||
.observe(viewLifecycleOwner) { regionCode ->
|
||||
currentPhoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
|
||||
reformatText(phoneNumberInputLayout.text)
|
||||
phoneNumberInputLayout.requestFocus()
|
||||
}
|
||||
|
||||
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
if (fragmentViewModel.isEnteredNumberPossible(fragmentState)) {
|
||||
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
|
||||
} else {
|
||||
@@ -172,9 +178,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
if (existingPhoneNumber != null) {
|
||||
fragmentViewModel.restoreState(existingPhoneNumber)
|
||||
spinnerView.setText(existingPhoneNumber.countryCode.toString())
|
||||
fragmentViewModel.formatter?.let {
|
||||
bindPhoneNumberFormatter(it)
|
||||
}
|
||||
phoneNumberInputLayout.setText(existingPhoneNumber.nationalNumber.toString())
|
||||
} else {
|
||||
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
|
||||
@@ -183,15 +186,24 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
|
||||
}
|
||||
|
||||
private fun bindPhoneNumberFormatter(formatter: TextWatcher) {
|
||||
if (formatter != currentPhoneNumberFormatter) {
|
||||
currentPhoneNumberFormatter?.let { oldWatcher ->
|
||||
Log.d(TAG, "Removing current phone number formatter in fragment")
|
||||
phoneNumberInputLayout.removeTextChangedListener(oldWatcher)
|
||||
private fun reformatText(text: Editable?) {
|
||||
if (text.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
currentPhoneNumberFormatter?.let { formatter ->
|
||||
formatter.clear()
|
||||
|
||||
var formattedNumber: String? = null
|
||||
text.forEach {
|
||||
if (it.isDigit()) {
|
||||
formattedNumber = formatter.inputDigit(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedNumber != null && text.toString() != formattedNumber) {
|
||||
text.replace(0, text.length, formattedNumber)
|
||||
}
|
||||
phoneNumberInputLayout.addTextChangedListener(formatter)
|
||||
currentPhoneNumberFormatter = formatter
|
||||
Log.d(TAG, "Updated phone number formatter in fragment")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,9 +227,12 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
}
|
||||
}
|
||||
|
||||
phoneNumberInputLayout.addTextChangedListener {
|
||||
fragmentViewModel.setPhoneNumber(it?.toString())
|
||||
}
|
||||
phoneNumberInputLayout.addTextChangedListener(
|
||||
afterTextChanged = {
|
||||
reformatText(it)
|
||||
fragmentViewModel.setPhoneNumber(it?.toString())
|
||||
}
|
||||
)
|
||||
|
||||
val scrollView = binding.scrollView
|
||||
val registerButton = binding.registerButton
|
||||
@@ -325,6 +340,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
when (result) {
|
||||
is RegistrationSessionCheckResult.Success,
|
||||
is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!")
|
||||
|
||||
is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service))
|
||||
is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen)
|
||||
|
||||
@@ -377,6 +393,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
Log.i(TAG, result.log())
|
||||
handleRequestVerificationCodeRateLimited(result)
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result)
|
||||
is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode)
|
||||
is VerificationCodeRequestResult.RateLimited -> {
|
||||
@@ -388,6 +405,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later))
|
||||
}
|
||||
}
|
||||
|
||||
is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() }
|
||||
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is VerificationCodeRequestResult.AlreadyVerified -> presentGenericError(result)
|
||||
@@ -477,6 +495,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
when (mode) {
|
||||
RegistrationRepository.E164VerificationMode.SMS_WITH_LISTENER,
|
||||
RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER -> sharedViewModel.requestSmsCode(requireContext())
|
||||
|
||||
RegistrationRepository.E164VerificationMode.PHONE_CALL -> sharedViewModel.requestVerificationCall(requireContext())
|
||||
}
|
||||
dialogInterface.dismiss()
|
||||
@@ -503,7 +522,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
|
||||
sharedViewModel.fetchFcmToken(requireContext())
|
||||
} else {
|
||||
sharedViewModel.uiState.value?.let { value ->
|
||||
val now = System.currentTimeMillis()
|
||||
val now = System.currentTimeMillis().milliseconds
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberState.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.text.TextWatcher
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
|
||||
/**
|
||||
* State holder for the phone number entry screen, including phone number and Play Services errors.
|
||||
*/
|
||||
data class EnterPhoneNumberState(
|
||||
val countryPrefixIndex: Int = 0,
|
||||
val countryPrefixIndex: Int,
|
||||
val phoneNumber: String = "",
|
||||
val phoneNumberFormatter: TextWatcher? = null,
|
||||
val phoneNumberRegionCode: String,
|
||||
val mode: RegistrationRepository.E164VerificationMode = RegistrationRepository.E164VerificationMode.SMS_WITHOUT_LISTENER,
|
||||
val error: Error = Error.NONE
|
||||
) {
|
||||
|
||||
@@ -5,19 +5,13 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registration.ui.phonenumber
|
||||
|
||||
import android.telephony.PhoneNumberFormattingTextWatcher
|
||||
import android.text.TextWatcher
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
@@ -27,14 +21,22 @@ import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
*/
|
||||
class EnterPhoneNumberViewModel : ViewModel() {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
|
||||
companion object {
|
||||
private val TAG = Log.tag(EnterPhoneNumberViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = MutableStateFlow(EnterPhoneNumberState())
|
||||
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
|
||||
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
|
||||
.sortedBy { it.digits }
|
||||
|
||||
private val store = MutableStateFlow(
|
||||
EnterPhoneNumberState(
|
||||
countryPrefixIndex = 0,
|
||||
phoneNumberRegionCode = supportedCountryPrefixes[0].regionCode
|
||||
)
|
||||
)
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
val formatter: TextWatcher?
|
||||
get() = store.value.phoneNumberFormatter
|
||||
|
||||
val phoneNumber: PhoneNumber?
|
||||
get() = try {
|
||||
parsePhoneNumber(store.value)
|
||||
@@ -43,10 +45,6 @@ class EnterPhoneNumberViewModel : ViewModel() {
|
||||
null
|
||||
}
|
||||
|
||||
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
|
||||
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
|
||||
.sortedBy { it.digits }
|
||||
|
||||
var mode: RegistrationRepository.E164VerificationMode
|
||||
get() = store.value.mode
|
||||
set(value) = store.update {
|
||||
@@ -69,19 +67,10 @@ class EnterPhoneNumberViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(countryPrefixIndex = matchingIndex)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
val regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(digits)
|
||||
val textWatcher = PhoneNumberFormattingTextWatcher(regionCode)
|
||||
|
||||
store.update {
|
||||
Log.d(TAG, "Updating phone number formatter in state")
|
||||
it.copy(phoneNumberFormatter = textWatcher)
|
||||
}
|
||||
}
|
||||
it.copy(
|
||||
countryPrefixIndex = matchingIndex,
|
||||
phoneNumberRegionCode = supportedCountryPrefixes[matchingIndex].regionCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +92,7 @@ class EnterPhoneNumberViewModel : ViewModel() {
|
||||
store.update {
|
||||
it.copy(
|
||||
countryPrefixIndex = prefixIndex,
|
||||
phoneNumberRegionCode = PhoneNumberUtil.getInstance().getRegionCodeForNumber(value) ?: it.phoneNumberRegionCode,
|
||||
phoneNumber = value.nationalNumber.toString()
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user