mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-06-29 10:35:52 +01:00
Enforce change number post registration delay.
This commit is contained in:
committed by
jeffrey-signal
parent
4decae274b
commit
a9649fd017
+16
-3
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.registration.sms.ReceivedSmsEvent
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.math.ceil
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
@@ -168,7 +169,7 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n
|
||||
is ChangeNumberResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
|
||||
is ChangeNumberResult.AuthorizationFailed -> presentIncorrectCodeDialog()
|
||||
is ChangeNumberResult.AttemptsExhausted -> presentAccountLocked()
|
||||
is ChangeNumberResult.RateLimited -> presentRateLimitedDialog()
|
||||
is ChangeNumberResult.RateLimited -> presentRateLimitedDialog(result.timeRemaining)
|
||||
|
||||
else -> presentGenericError(result)
|
||||
}
|
||||
@@ -195,13 +196,25 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n
|
||||
)
|
||||
}
|
||||
|
||||
private fun presentRateLimitedDialog() {
|
||||
private fun presentRateLimitedDialog(retryAfterSeconds: Long = 0) {
|
||||
binding.codeEntryLayout.keyboard.displayFailure().addListener(
|
||||
object : AssertedSuccessListener<Boolean?>() {
|
||||
override fun onSuccess(result: Boolean?) {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
if (retryAfterSeconds > 0) {
|
||||
val minutes = ceil(retryAfterSeconds / 60.0).toInt().coerceAtLeast(1)
|
||||
setMessage(
|
||||
if (minutes >= 60) {
|
||||
val hours = ceil(minutes / 60.0).toInt()
|
||||
resources.getQuantityString(R.plurals.ChangeNumberEnterCodeFragment__too_many_attempts_try_again_in_hours, hours, hours)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.ChangeNumberEnterCodeFragment__too_many_attempts_try_again_in_minutes, minutes, minutes)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
|
||||
}
|
||||
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
binding.codeEntryLayout.callMeCountDown.visibility = View.VISIBLE
|
||||
binding.codeEntryLayout.resendSmsCountDown.visibility = View.VISIBLE
|
||||
|
||||
+20
-4
@@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
@@ -34,7 +33,10 @@ import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class ChangeNumberFragment : ComposeFragment() {
|
||||
|
||||
@@ -46,10 +48,25 @@ class ChangeNumberFragment : ComposeFragment() {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onContinueClick = {
|
||||
navController.safeNavigate(ChangeNumberFragmentDirections.actionChangePhoneNumberFragmentToEnterPhoneNumberChangeFragment())
|
||||
val remainingWaitSeconds = remainingPostRegistrationWaitSeconds()
|
||||
if (remainingWaitSeconds > 0) {
|
||||
ChangeNumberPostRegistrationWaitSheet.show(parentFragmentManager, remainingWaitSeconds)
|
||||
} else {
|
||||
navController.safeNavigate(ChangeNumberFragmentDirections.actionChangePhoneNumberFragmentToEnterPhoneNumberChangeFragment())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun remainingPostRegistrationWaitSeconds(): Long {
|
||||
val registeredAt = SignalStore.account.registeredAtTimestamp
|
||||
if (registeredAt <= 0) {
|
||||
return 0
|
||||
}
|
||||
val waitingPeriodSeconds = RemoteConfig.changeNumberPostRegistrationWaitingPeriodSeconds
|
||||
val elapsedSeconds = (System.currentTimeMillis() - registeredAt).milliseconds.inWholeSeconds
|
||||
return (waitingPeriodSeconds - elapsedSeconds).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -73,7 +90,7 @@ fun ChangeNumberScreen(
|
||||
.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.change_number_hero_image),
|
||||
painter = painterResource(id = R.drawable.change_number),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 20.dp)
|
||||
@@ -83,7 +100,6 @@ fun ChangeNumberScreen(
|
||||
text = stringResource(id = R.string.AccountSettingsFragment__change_phone_number),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
)
|
||||
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* Sheet shown when the user attempts to change their phone number before the
|
||||
* post-registration waiting period has elapsed.
|
||||
*/
|
||||
class ChangeNumberPostRegistrationWaitSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_REMAINING_SECONDS = "arg.remaining_seconds"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, remainingSeconds: Long) {
|
||||
ChangeNumberPostRegistrationWaitSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putLong(ARG_REMAINING_SECONDS, remainingSeconds)
|
||||
}
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private val remainingSeconds: Long
|
||||
get() = requireArguments().getLong(ARG_REMAINING_SECONDS)
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
SheetContent(
|
||||
remainingSeconds = remainingSeconds,
|
||||
onDismiss = { dismissAllowingStateLoss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
remainingSeconds: Long,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters(gutterSize = 36.dp)
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.change_number_error),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 26.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ChangeNumberPostRegistrationWaitSheet__title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ChangeNumberPostRegistrationWaitSheet__body),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = formatTryAgainIn(remainingSeconds),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 32.dp, bottom = 24.dp, start = 12.dp, end = 12.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.ChangeNumberPostRegistrationWaitSheet__ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SheetContentMinutesPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
SheetContent(
|
||||
remainingSeconds = 25 * 60,
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SheetContentHoursPreview() {
|
||||
Previews.BottomSheetContentPreview {
|
||||
SheetContent(
|
||||
remainingSeconds = 2 * 60 * 60,
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatTryAgainIn(remainingSeconds: Long): String {
|
||||
val minutes = ceil(remainingSeconds / 60.0).toInt().coerceAtLeast(1)
|
||||
return if (minutes >= 60) {
|
||||
val hours = ceil(minutes / 60.0).toInt()
|
||||
pluralStringResource(R.plurals.ChangeNumberPostRegistrationWaitSheet__try_again_in_hours, hours, hours)
|
||||
} else {
|
||||
pluralStringResource(R.plurals.ChangeNumberPostRegistrationWaitSheet__try_again_in_minutes, minutes, minutes)
|
||||
}
|
||||
}
|
||||
@@ -434,7 +434,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
val isRegistered: Boolean
|
||||
get() = getBoolean(KEY_IS_REGISTERED, false)
|
||||
|
||||
fun setRegistered(registered: Boolean) {
|
||||
fun setRegistered(registered: Boolean, isAciChanged: Boolean = false) {
|
||||
Log.i(TAG, "Setting push registered: $registered", Throwable())
|
||||
|
||||
val previous = isRegistered
|
||||
@@ -451,7 +451,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
clearLocalCredentials()
|
||||
}
|
||||
|
||||
if (!previous && registered) {
|
||||
if (registered && (!previous || isAciChanged)) {
|
||||
registeredAtTimestamp = System.currentTimeMillis()
|
||||
} else if (!registered) {
|
||||
registeredAtTimestamp = -1
|
||||
|
||||
+2
-1
@@ -184,6 +184,7 @@ object RegistrationRepository {
|
||||
val aci: ACI = ACI.parseOrThrow(data.aci)
|
||||
val pni: PNI = PNI.parseOrThrow(data.pni)
|
||||
val hasPin: Boolean = data.hasPin
|
||||
val isAciChanged: Boolean = SignalStore.account.aci != aci
|
||||
|
||||
SignalStore.account.setAci(aci)
|
||||
SignalStore.account.setPni(pni)
|
||||
@@ -232,7 +233,7 @@ object RegistrationRepository {
|
||||
}
|
||||
|
||||
SignalStore.account.setServicePassword(data.servicePassword)
|
||||
SignalStore.account.setRegistered(true)
|
||||
SignalStore.account.setRegistered(registered = true, isAciChanged = isAciChanged)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false)
|
||||
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
|
||||
|
||||
@@ -1411,6 +1411,15 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/** Seconds after registration during which change-number is blocked. */
|
||||
@JvmStatic
|
||||
@get:JvmName("changeNumberPostRegistrationWaitingPeriodSeconds")
|
||||
val changeNumberPostRegistrationWaitingPeriodSeconds: Long by remoteLong(
|
||||
key = "global.changeNumber.postRegistrationWaitingPeriodSeconds",
|
||||
defaultValue = 3600,
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/**
|
||||
* A ratio between 0 and 1, where 0 means that a session is never archived due
|
||||
* to a lack of PQ, and 1 means that a session is always archived due to a
|
||||
|
||||
Reference in New Issue
Block a user