diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
index 2bcf73d7df..92f8c1a9a6 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
@@ -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 = {}
+ )
+ }
+}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
index c2e49f55a2..238f7949a8 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
@@ -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()})"
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
index be4c7947f9..8b9b0c65b9 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
@@ -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)
}
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
index ead0e0a555..4a1bdd6b8d 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModel.kt
@@ -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(
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
index a37f760885..c8b9d8e333 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryScreen.kt
@@ -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))
+ }
}
}
}
diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml
index 1fa42e73f0..b9fee67a5a 100644
--- a/feature/registration/src/main/res/values/strings.xml
+++ b/feature/registration/src/main/res/values/strings.xml
@@ -365,6 +365,14 @@
Skip
Continue
+
+ Skip PIN entry?
+
+ 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.
+
+ Create New PIN
+
+ Cancel
Allow Notifications
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt
index de13176734..7c8de18395 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForSvrRestoreViewModelTest.kt
@@ -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()
+ .prop(RegistrationFlowEvent.NavigateToScreen::route)
+ .isInstanceOf()
}
// ==================== ToggleKeyboard Tests ====================