Improve UX for missing SVR data in regV5.

This commit is contained in:
Greyson Parrelli
2026-06-29 12:24:29 -04:00
committed by Michelle Tang
parent bdb6b7d5f1
commit 55b5997a4e
9 changed files with 65 additions and 4 deletions
@@ -289,6 +289,8 @@ class RegistrationRepository(val context: Context, val networkController: Networ
backupSizeBytes = provisioningMessage.backupSizeBytes ?: 0,
backupVersion = provisioningMessage.backupVersion
)
pin = provisioningMessage.pin ?: ""
pinIsAlphanumeric = provisioningMessage.pin?.any { !it.isDigit() } == true
}
val aep = AccountEntropyPool(provisioningMessage.accountEntropyPool)
@@ -69,6 +69,8 @@ class PinEntryForRegistrationLockViewModel(
is PinEntryScreenEvents.Skip -> {
handleSkip()
}
is PinEntryScreenEvents.CreateNewPin,
is PinEntryScreenEvents.ContactSupport -> Unit
is PinEntryScreenEvents.ToggleKeyboard,
is PinEntryScreenEvents.NeedHelp -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
@@ -76,6 +76,8 @@ class PinEntryForSmsBypassViewModel(
is PinEntryScreenEvents.Skip -> {
handleSkip()
}
is PinEntryScreenEvents.CreateNewPin,
is PinEntryScreenEvents.ContactSupport -> Unit
is PinEntryScreenEvents.ToggleKeyboard,
is PinEntryScreenEvents.NeedHelp -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
@@ -72,6 +72,15 @@ class PinEntryForSvrRestoreViewModel(
is PinEntryScreenEvents.Skip -> {
handleSkip()
}
is PinEntryScreenEvents.CreateNewPin -> {
Log.i(TAG, "[CreateNewPin] User opted to create a new PIN after no data was found. Navigating to PIN creation.")
stateEmitter(state.copy(showNoDataToRestoreDialog = false))
parentEventEmitter.navigateTo(RegistrationRoute.PinCreate)
}
is PinEntryScreenEvents.ContactSupport -> {
Log.i(TAG, "[ContactSupport] User opted to contact support after no data was found.")
stateEmitter(state.copy(showNoDataToRestoreDialog = false))
}
is PinEntryScreenEvents.ToggleKeyboard,
is PinEntryScreenEvents.NeedHelp -> {
stateEmitter(PinEntryScreenEventHandler.applyEvent(state, event))
@@ -128,9 +137,8 @@ class PinEntryForSvrRestoreViewModel(
state.copy(triesRemaining = error.triesRemaining)
}
is NetworkController.RestoreMasterKeyError.NoDataFound -> {
Log.w(TAG, "[PinEntered] No SVR data found. Need to create a PIN instead.")
parentEventEmitter.navigateTo(RegistrationRoute.PinCreate)
state
Log.w(TAG, "[PinEntered] No SVR data found. Prompting user to create a new PIN.")
state.copy(showNoDataToRestoreDialog = true)
}
}
}
@@ -45,6 +45,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
@@ -111,6 +112,22 @@ fun PinEntryScreen(
)
}
if (state.showNoDataToRestoreDialog) {
Dialogs.SimpleAlertDialog(
title = "",
body = stringResource(R.string.PinEntryScreen__no_data_could_be_found),
confirm = stringResource(R.string.PinEntryScreen__create_new_pin),
dismiss = stringResource(R.string.PinEntryScreen__contact_support),
onConfirm = { onEvent(PinEntryScreenEvents.CreateNewPin) },
onDeny = { onEvent(PinEntryScreenEvents.ContactSupport) },
onDismissRequest = { onEvent(PinEntryScreenEvents.ContactSupport) },
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
)
}
// autofocus PIN field on initial composition
LaunchedEffect(Unit) {
focusRequester.requestFocus()
@@ -12,4 +12,6 @@ sealed class PinEntryScreenEvents {
data object ToggleKeyboard : PinEntryScreenEvents()
data object NeedHelp : PinEntryScreenEvents()
data object Skip : PinEntryScreenEvents()
data object CreateNewPin : PinEntryScreenEvents()
data object ContactSupport : PinEntryScreenEvents()
}
@@ -11,6 +11,7 @@ data class PinEntryState(
val showNeedHelp: Boolean = false,
val isAlphanumericKeyboard: Boolean = false,
val loading: Boolean = false,
val showNoDataToRestoreDialog: Boolean = false,
val triesRemaining: Int? = null,
val mode: Mode = Mode.SvrRestore,
val oneTimeEvent: OneTimeEvent? = null,
@@ -373,6 +373,10 @@
<string name="PinEntryScreen__create_new_pin">Create New PIN</string>
<!-- Labels the button in the skip dialog that dismisses it. -->
<string name="PinEntryScreen__cancel">Cancel</string>
<!-- Body of the dialog shown when no data could be found to restore from the user's PIN, prompting them to create a new PIN. -->
<string name="PinEntryScreen__no_data_could_be_found">No data could be found to restore your account. Please create a new PIN.</string>
<!-- Labels the button in the no-data-to-restore dialog that lets the user contact support. -->
<string name="PinEntryScreen__contact_support">Contact support</string>
<!-- Title for the screen asking the user to grant the notification permission -->
<string name="AllowNotificationsScreen__allow_notifications">Allow Notifications</string>
@@ -163,7 +163,7 @@ class PinEntryForSvrRestoreViewModelTest {
}
@Test
fun `PinEntered with no SVR data navigates to PinCreate`() = runTest {
fun `PinEntered with no SVR data shows the no-data-to-restore dialog without navigating`() = runTest {
val svrCredentials = NetworkController.SvrCredentials(
username = "test-username",
password = "test-password"
@@ -179,6 +179,19 @@ class PinEntryForSvrRestoreViewModelTest {
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().showNoDataToRestoreDialog).isEqualTo(true)
}
// ==================== No Data To Restore Dialog Tests ====================
@Test
fun `CreateNewPin dismisses the dialog and navigates to PinCreate`() = runTest {
val initialState = PinEntryState(mode = PinEntryState.Mode.SvrRestore, showNoDataToRestoreDialog = true)
viewModel.applyEvent(initialState, PinEntryScreenEvents.CreateNewPin, parentEventEmitter, stateEmitter)
assertThat(emittedStates.last().showNoDataToRestoreDialog).isEqualTo(false)
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents.first())
.isInstanceOf<RegistrationFlowEvent.NavigateToScreen>()
@@ -186,6 +199,16 @@ class PinEntryForSvrRestoreViewModelTest {
.isInstanceOf<RegistrationRoute.PinCreate>()
}
@Test
fun `ContactSupport dismisses the dialog without navigating`() = runTest {
val initialState = PinEntryState(mode = PinEntryState.Mode.SvrRestore, showNoDataToRestoreDialog = true)
viewModel.applyEvent(initialState, PinEntryScreenEvents.ContactSupport, parentEventEmitter, stateEmitter)
assertThat(emittedStates.last().showNoDataToRestoreDialog).isEqualTo(false)
assertThat(emittedParentEvents).hasSize(0)
}
@Test
fun `PinEntered with network error restoring master key returns NetworkError event`() = runTest {
val svrCredentials = NetworkController.SvrCredentials(