mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Fix skip button during PIN entry in regV5.
This commit is contained in:
committed by
Michelle Tang
parent
80aa521e42
commit
bfdc628dd2
+26
-2
@@ -22,12 +22,14 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -167,6 +169,7 @@ private fun OnePaneLayout(
|
||||
params = params,
|
||||
canSubmitPin = canSubmitPin,
|
||||
isElevated = scrollState.canScrollForward,
|
||||
loading = state.loading,
|
||||
onNext = { onEvent(PinCreationScreenEvents.PinSubmitted(activePin.value)) }
|
||||
)
|
||||
}
|
||||
@@ -237,6 +240,7 @@ private fun TwoPaneLayout(
|
||||
params = params,
|
||||
canSubmitPin = canSubmitPin,
|
||||
isElevated = firstPaneScrollState.canScrollForward || secondPaneScrollState.canScrollForward,
|
||||
loading = state.loading,
|
||||
onNext = { onEvent(PinCreationScreenEvents.PinSubmitted(activePin.value)) }
|
||||
)
|
||||
}
|
||||
@@ -515,6 +519,7 @@ private fun NextButton(
|
||||
params: RegistrationScaffold.Params,
|
||||
canSubmitPin: Boolean,
|
||||
isElevated: Boolean,
|
||||
loading: Boolean,
|
||||
onNext: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -528,12 +533,20 @@ private fun NextButton(
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNext,
|
||||
enabled = canSubmitPin,
|
||||
enabled = canSubmitPin && !loading,
|
||||
modifier = Modifier
|
||||
.widthIn(max = params.maxButtonWidth)
|
||||
.padding(params.footerPadding)
|
||||
) {
|
||||
Text(stringResource(R.string.PinCreationScreen__next))
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 3.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(R.string.PinCreationScreen__next))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,3 +599,14 @@ private fun PinCreationScreenMismatchPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun PinCreationScreenLoadingPreview() {
|
||||
Previews.Preview {
|
||||
PinCreationScreen(
|
||||
state = PinCreationState(isConfirmEnabled = true, loading = true),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,10 +12,11 @@ data class PinCreationState(
|
||||
val isAlphanumericKeyboard: Boolean = false,
|
||||
val isConfirmEnabled: Boolean = false,
|
||||
val pinMismatch: Boolean = false,
|
||||
val loading: Boolean = false,
|
||||
val firstPin: String? = null,
|
||||
val accountEntropyPool: AccountEntropyPool? = null
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()})"
|
||||
return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()})"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -67,9 +67,9 @@ class PinCreationViewModel(
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "[PinSubmitted] Confirmation PIN matched.")
|
||||
_state.value = state.copy(pinMismatch = false)
|
||||
_state.value = state.copy(pinMismatch = false, loading = true)
|
||||
val result = applyPinSubmitted(state, event.pin)
|
||||
_state.value = result
|
||||
_state.value = result.copy(loading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -145,11 +145,9 @@ class PinEntryForSvrRestoreViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSkip() {
|
||||
Log.i(TAG, "[Skip] User opted out of restoring data and creating a PIN. Recording choice and completing registration.")
|
||||
repository.setPinOptedOut()
|
||||
repository.setRestoreDecision(RestoreDecision.SKIPPED)
|
||||
parentEventEmitter(RegistrationFlowEvent.RegistrationComplete)
|
||||
private fun handleSkip() {
|
||||
Log.i(TAG, "[Skip] User opted to skip restoring their PIN. Navigating to PIN creation.")
|
||||
parentEventEmitter.navigateTo(RegistrationRoute.PinCreate)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
|
||||
+37
-4
@@ -15,11 +15,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
@@ -46,6 +48,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.registration.R
|
||||
@@ -64,6 +67,7 @@ fun PinEntryScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var pin by rememberSaveable { mutableStateOf("") }
|
||||
var showSkipDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val canSubmitPin = pin.isNotEmpty()
|
||||
|
||||
@@ -75,6 +79,7 @@ fun PinEntryScreen(
|
||||
canSubmitPin = canSubmitPin,
|
||||
focusRequester = focusRequester,
|
||||
onPinChanged = { pin = it },
|
||||
onSkip = { showSkipDialog = true },
|
||||
onEvent = onEvent,
|
||||
modifier = modifier
|
||||
)
|
||||
@@ -86,11 +91,26 @@ fun PinEntryScreen(
|
||||
canSubmitPin = canSubmitPin,
|
||||
focusRequester = focusRequester,
|
||||
onPinChanged = { pin = it },
|
||||
onSkip = { showSkipDialog = true },
|
||||
onEvent = onEvent,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
if (showSkipDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.PinEntryScreen__skip_pin_entry),
|
||||
body = stringResource(R.string.PinEntryScreen__skip_pin_entry_message),
|
||||
confirm = stringResource(R.string.PinEntryScreen__create_new_pin),
|
||||
dismiss = stringResource(R.string.PinEntryScreen__cancel),
|
||||
onConfirm = {
|
||||
showSkipDialog = false
|
||||
onEvent(PinEntryScreenEvents.Skip)
|
||||
},
|
||||
onDismiss = { showSkipDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
// autofocus PIN field on initial composition
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
@@ -105,6 +125,7 @@ private fun OnePaneLayout(
|
||||
canSubmitPin: Boolean,
|
||||
focusRequester: FocusRequester,
|
||||
onPinChanged: (String) -> Unit,
|
||||
onSkip: () -> Unit,
|
||||
onEvent: (PinEntryScreenEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -147,7 +168,7 @@ private fun OnePaneLayout(
|
||||
|
||||
if (state.mode != PinEntryState.Mode.RegistrationLock) {
|
||||
SkipButton(
|
||||
onSkip = { onEvent(PinEntryScreenEvents.Skip) },
|
||||
onSkip = onSkip,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(params.edgeInset)
|
||||
@@ -160,6 +181,7 @@ private fun OnePaneLayout(
|
||||
params = params,
|
||||
canSubmitPin = canSubmitPin,
|
||||
isElevated = scrollState.canScrollForward,
|
||||
loading = state.loading,
|
||||
onContinue = { onEvent(PinEntryScreenEvents.PinEntered(pin)) }
|
||||
)
|
||||
}
|
||||
@@ -174,6 +196,7 @@ private fun TwoPaneLayout(
|
||||
canSubmitPin: Boolean,
|
||||
focusRequester: FocusRequester,
|
||||
onPinChanged: (String) -> Unit,
|
||||
onSkip: () -> Unit,
|
||||
onEvent: (PinEntryScreenEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -226,7 +249,7 @@ private fun TwoPaneLayout(
|
||||
|
||||
if (state.mode != PinEntryState.Mode.RegistrationLock) {
|
||||
SkipButton(
|
||||
onSkip = { onEvent(PinEntryScreenEvents.Skip) },
|
||||
onSkip = onSkip,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(params.edgeInset)
|
||||
@@ -239,6 +262,7 @@ private fun TwoPaneLayout(
|
||||
params = params,
|
||||
canSubmitPin = canSubmitPin,
|
||||
isElevated = firstPaneScrollState.canScrollForward || secondPaneScrollState.canScrollForward,
|
||||
loading = state.loading,
|
||||
onContinue = { onEvent(PinEntryScreenEvents.PinEntered(pin)) }
|
||||
)
|
||||
}
|
||||
@@ -366,6 +390,7 @@ private fun ContinueButton(
|
||||
params: RegistrationScaffold.Params,
|
||||
canSubmitPin: Boolean,
|
||||
isElevated: Boolean,
|
||||
loading: Boolean,
|
||||
onContinue: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -379,12 +404,20 @@ private fun ContinueButton(
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onContinue,
|
||||
enabled = canSubmitPin,
|
||||
enabled = canSubmitPin && !loading,
|
||||
modifier = Modifier
|
||||
.widthIn(max = params.maxButtonWidth)
|
||||
.padding(params.footerPadding)
|
||||
) {
|
||||
Text(stringResource(R.string.PinEntryScreen__continue))
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 3.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(R.string.PinEntryScreen__continue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,14 @@
|
||||
<string name="PinEntryScreen__skip">Skip</string>
|
||||
<!-- Labels the button to submit the PIN. -->
|
||||
<string name="PinEntryScreen__continue">Continue</string>
|
||||
<!-- Title of the dialog shown when the user taps skip on the PIN entry screen. -->
|
||||
<string name="PinEntryScreen__skip_pin_entry">Skip PIN entry?</string>
|
||||
<!-- Body of the dialog shown when the user taps skip on the PIN entry screen, explaining the consequences of creating a new PIN. -->
|
||||
<string name="PinEntryScreen__skip_pin_entry_message">If you can\'t remember your PIN, you can create a new one. You can register and use your account but you\'ll lose some saved settings like your profile information.</string>
|
||||
<!-- Labels the button in the skip dialog that takes the user to create a new PIN. -->
|
||||
<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>
|
||||
|
||||
<!-- Title for the screen asking the user to grant the notification permission -->
|
||||
<string name="AllowNotificationsScreen__allow_notifications">Allow Notifications</string>
|
||||
|
||||
+5
-4
@@ -227,15 +227,16 @@ class PinEntryForSvrRestoreViewModelTest {
|
||||
// ==================== Skip Tests ====================
|
||||
|
||||
@Test
|
||||
fun `Skip records PIN opt-out and completes registration`() = runTest {
|
||||
fun `Skip navigates to PinCreate`() = runTest {
|
||||
val initialState = PinEntryState(mode = PinEntryState.Mode.SvrRestore)
|
||||
|
||||
viewModel.applyEvent(initialState, PinEntryScreenEvents.Skip, parentEventEmitter, stateEmitter)
|
||||
|
||||
coVerify { mockRepository.setPinOptedOut() }
|
||||
coVerify { mockRepository.setRestoreDecision(RestoreDecision.SKIPPED) }
|
||||
assertThat(emittedParentEvents).hasSize(1)
|
||||
assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.RegistrationComplete)
|
||||
assertThat(emittedParentEvents.first())
|
||||
.isInstanceOf<RegistrationFlowEvent.NavigateToScreen>()
|
||||
.prop(RegistrationFlowEvent.NavigateToScreen::route)
|
||||
.isInstanceOf<RegistrationRoute.PinCreate>()
|
||||
}
|
||||
|
||||
// ==================== ToggleKeyboard Tests ====================
|
||||
|
||||
Reference in New Issue
Block a user