diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
index be904d25e9..5d388d6c54 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
@@ -82,6 +82,7 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.Util
import org.signal.core.util.logging.Log
+import org.signal.registration.PendingRestoreOption
import org.signal.registration.R
import org.signal.registration.RegistrationDependencies
import org.signal.registration.screens.OnePaneRegistrationScaffold
@@ -413,7 +414,15 @@ private fun NextButton(
enabled = !state.showSpinner,
modifier = Modifier.testTag(TestTags.PHONE_NUMBER_REGISTER_WITHOUT_NUMBER_BUTTON)
) {
- Text(stringResource(R.string.RegistrationActivity_register_without_number))
+ Text(
+ stringResource(
+ if (state.hasExistingAccount) {
+ R.string.RegistrationActivity_use_account_id
+ } else {
+ R.string.RegistrationActivity_register_without_number
+ }
+ )
+ )
}
Spacer(modifier = Modifier.weight(1f))
@@ -646,6 +655,20 @@ private fun PhoneNumberScreenRegisterWithoutNumberPreview() {
}
}
+@AllDevicePreviews
+@Composable
+private fun PhoneNumberScreenUseAccountIdPreview() {
+ Previews.Preview {
+ PhoneNumberScreen(
+ state = PhoneNumberEntryState(
+ isPhoneNumberlessRegistrationAvailable = true,
+ pendingRestoreOption = PendingRestoreOption.RemoteBackup
+ ),
+ onEvent = {}
+ )
+ }
+}
+
@AllDevicePreviews
@Composable
private fun PhoneNumberScreenAccountIdPreview() {
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt
index d1b831525e..7d99495c09 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt
@@ -52,6 +52,10 @@ data class PhoneNumberEntryState(
null
}
+ /** Whether the user already told us they have an account to restore, which lets us skip past the Signal Login purchase screen. */
+ val hasExistingAccount: Boolean
+ get() = pendingRestoreOption != null
+
/** Whether what has been entered is complete enough to submit, be it a phone number or an account ID. */
val isNextEnabled: Boolean
get() {
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt
index 6ddd2ef621..9cd2637fc1 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt
@@ -151,7 +151,11 @@ class PhoneNumberEntryViewModel(
parentEventEmitter.navigateTo(RegistrationRoute.LinkAccount())
}
is PhoneNumberEntryScreenEvents.RegisterWithoutNumber -> {
- parentEventEmitter.navigateTo(RegistrationRoute.SignalLoginPayment)
+ if (state.hasExistingAccount) {
+ parentEventEmitter.navigateTo(RegistrationRoute.SignalLoginCredentialEntry())
+ } else {
+ parentEventEmitter.navigateTo(RegistrationRoute.SignalLoginPayment)
+ }
}
is PhoneNumberEntryScreenEvents.CaptchaCompleted -> {
stateEmitter(applyCaptchaCompleted(state, event.token, parentEventEmitter))
diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml
index e5ab935c4e..eb8002824d 100644
--- a/feature/registration/src/main/res/values/strings.xml
+++ b/feature/registration/src/main/res/values/strings.xml
@@ -55,6 +55,8 @@
Next
Register without number
+
+ Use account ID
Is the phone number below correct?
diff --git a/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt b/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt
index 8a67620d2f..c9c9fcb982 100644
--- a/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/RegistrationEndToEndTest.kt
@@ -1840,6 +1840,38 @@ class RegistrationEndToEndTest {
assert(storageController.restoreDecision == RestoreDecision.SKIPPED) { "Expected SKIPPED restore decision but was ${storageController.restoreDecision}" }
}
+ @Test
+ fun `a pending restore turns the numberless button into a direct jump to signal login entry`() {
+ enableSignalLoginRegistration()
+ val login = signalLoginFor(reregistration = true)
+
+ var registrationComplete = false
+ launchRegistrationFlow(onRegistrationComplete = { registrationComplete = true })
+
+ startManualRestore()
+ chooseRestoreOption(TestTags.ARCHIVE_RESTORE_SELECTION_FROM_SIGNAL_BACKUPS)
+
+ // The user already said they have an account to restore, so the purchase screen is skipped entirely
+ waitForTag(TestTags.PHONE_NUMBER_SCREEN)
+ composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_REGISTER_WITHOUT_NUMBER_BUTTON).performClick()
+ waitForTag(TestTags.SIGNAL_LOGIN_CREDENTIAL_ENTRY_SCREEN)
+ assert(composeTestRule.onAllNodesWithTag(TestTags.SIGNAL_LOGIN_PAYMENT_SCREEN).fetchSemanticsNodes().isEmpty()) {
+ "Expected the Signal Login payment screen to be skipped for a user with a pending restore"
+ }
+
+ enterSignalLogin(login)
+
+ chooseRestoreOption(TestTags.ARCHIVE_RESTORE_SELECTION_NONE)
+ waitForTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON)
+ composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).performClick()
+
+ waitFor("registration to complete") { registrationComplete }
+
+ assert(purchaseApi.launchCount == 0) { "A user with an existing login should never hit Google Play" }
+ assert(networkController.lastCreateSessionE164 == null) { "Expected no verification session for a numberless login" }
+ assert(networkController.lastRegisterAccountRequest?.aci == login.aci) { "Expected the entered login to be reclaimed but was ${networkController.lastRegisterAccountRequest}" }
+ }
+
@Test
fun `typing an account id into the phone number field goes straight to signal login entry with it filled in`() {
enableSignalLoginRegistration()
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt
index a663341b9d..d24301088d 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt
@@ -46,6 +46,7 @@ import org.signal.network.api.RegistrationApiV2.SvrCredentials
import org.signal.network.api.RegistrationApiV2.ThirdPartyServiceErrorResponse
import org.signal.network.api.RegistrationApiV2.UpdateSessionError
import org.signal.registration.KeyMaterial
+import org.signal.registration.PendingRestoreOption
import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.RegisteredAccountData
import org.signal.registration.RegistrationFlowEvent
@@ -449,6 +450,38 @@ class PhoneNumberEntryViewModelTest {
.isInstanceOf()
}
+ @Test
+ fun `RegisterWithoutNumber navigates to the Signal Login payment screen`() = runTest {
+ viewModel.applyEvent(
+ PhoneNumberEntryState(),
+ PhoneNumberEntryScreenEvents.RegisterWithoutNumber,
+ parentEventEmitter,
+ stateEmitter
+ )
+
+ assertThat(emittedEvents).hasSize(1)
+ assertThat(emittedEvents.first())
+ .isInstanceOf()
+ .prop(RegistrationFlowEvent.NavigateToScreen::route)
+ .isInstanceOf()
+ }
+
+ @Test
+ fun `RegisterWithoutNumber skips payment and goes straight to credential entry when a restore is pending`() = runTest {
+ viewModel.applyEvent(
+ PhoneNumberEntryState(pendingRestoreOption = PendingRestoreOption.RemoteBackup),
+ PhoneNumberEntryScreenEvents.RegisterWithoutNumber,
+ parentEventEmitter,
+ stateEmitter
+ )
+
+ assertThat(emittedEvents).hasSize(1)
+ assertThat(emittedEvents.first())
+ .isInstanceOf()
+ .prop(RegistrationFlowEvent.NavigateToScreen::route)
+ .isEqualTo(RegistrationRoute.SignalLoginCredentialEntry())
+ }
+
@Test
fun `initial state reflects repository link and sync availability`() = runTest {
every { mockRepository.isLinkAndSyncAvailable } returns true
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt
index b508b97d48..6f27691d65 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt
@@ -25,6 +25,7 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.ui.CoreUiDependenciesRule
import org.signal.core.ui.compose.theme.SignalTheme
+import org.signal.registration.PendingRestoreOption
import org.signal.registration.R
import org.signal.registration.screens.shared.AccountIdError
import org.signal.registration.test.TestTags
@@ -326,6 +327,42 @@ class PhoneNumberScreenTest {
}
}
+ @Test
+ fun `the numberless button offers to register without a number when there is no pending restore`() {
+ // Given
+ composeTestRule.setContent {
+ SignalTheme {
+ PhoneNumberScreen(
+ state = PhoneNumberEntryState(isPhoneNumberlessRegistrationAvailable = true),
+ onEvent = {}
+ )
+ }
+ }
+
+ // Then
+ composeTestRule.onNodeWithText(context.getString(R.string.RegistrationActivity_register_without_number)).assertExists()
+ }
+
+ @Test
+ fun `the numberless button offers to use an account ID when a restore is pending`() {
+ // Given
+ composeTestRule.setContent {
+ SignalTheme {
+ PhoneNumberScreen(
+ state = PhoneNumberEntryState(
+ isPhoneNumberlessRegistrationAvailable = true,
+ pendingRestoreOption = PendingRestoreOption.RemoteBackup
+ ),
+ onEvent = {}
+ )
+ }
+ }
+
+ // Then
+ composeTestRule.onNodeWithText(context.getString(R.string.RegistrationActivity_use_account_id)).assertExists()
+ composeTestRule.onNodeWithText(context.getString(R.string.RegistrationActivity_register_without_number)).assertDoesNotExist()
+ }
+
private fun accountIdState(accountId: String = "a6b284822e3283d07f2391360a4c2b91") = PhoneNumberEntryState(
accountId = accountId,
formattedNumber = accountId,