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,