From f2e8b8360495a44ba6e4882f01d4892802581d91 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Wed, 20 May 2026 15:58:12 -0400 Subject: [PATCH] Update reg v5 UI for quick restore. --- .../quickrestore/QuickRestoreQrScreen.kt | 348 +++++++++++------- .../src/main/res/values/strings.xml | 10 +- 2 files changed, 223 insertions(+), 135 deletions(-) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt index 5ae57f56ee..650cef5f86 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/quickrestore/QuickRestoreQrScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -23,7 +24,6 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -40,11 +40,15 @@ import androidx.compose.ui.res.stringResource 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.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.QrCode import org.signal.core.ui.compose.QrCodeData import org.signal.core.ui.compose.SignalIcons import org.signal.registration.R +import org.signal.registration.screens.OnePaneRegistrationScaffold +import org.signal.registration.screens.RegistrationScaffold +import org.signal.registration.screens.TwoPaneRegistrationScaffold /** * Screen to display QR code for restoring from an old device. @@ -55,170 +59,240 @@ fun QuickRestoreQrScreen( state: QuickRestoreQrState, onEvent: (QuickRestoreQrEvents) -> Unit, modifier: Modifier = Modifier +) { + when (val layoutParams = RegistrationScaffold.rememberLayoutParams()) { + is RegistrationScaffold.Params.OnePane -> OnePaneLayout(layoutParams, state, onEvent, modifier) + is RegistrationScaffold.Params.TwoPane -> TwoPaneLayout(layoutParams, state, onEvent, modifier) + } + + StateDialogs(state = state, onEvent = onEvent) +} + +@Composable +private fun OnePaneLayout( + params: RegistrationScaffold.Params.OnePane, + state: QuickRestoreQrState, + onEvent: (QuickRestoreQrEvents) -> Unit, + modifier: Modifier ) { val scrollState = rememberScrollState() - - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(R.string.QuickRestoreQRScreen__scan), - style = MaterialTheme.typography.headlineMedium, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(32.dp)) - - // QR Code display area - Box( - modifier = Modifier - .widthIn(max = 280.dp) - .aspectRatio(1f) - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - Box( + OnePaneRegistrationScaffold( + modifier = modifier.fillMaxSize(), + params = params, + content = { paddingValues -> + Column( modifier = Modifier .fillMaxSize() - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surface) - .padding(16.dp), - contentAlignment = Alignment.Center + .verticalScroll(scrollState) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally ) { - AnimatedContent( - targetState = state.qrState, - contentKey = { it::class }, - label = "qr-code-state" - ) { qrState -> - when (qrState) { - is QrState.Loaded -> { - QrCode( - data = qrState.qrCodeData, - foregroundColor = Color(0xFF2449C0), - modifier = Modifier.fillMaxSize() + Heading() + Spacer(modifier = Modifier.height(32.dp)) + QrCodePane(state = state, onEvent = onEvent) + Spacer(modifier = Modifier.height(32.dp)) + Instructions() + } + }, + footer = { + CancelFooter(onEvent) + } + ) +} + +@Composable +private fun TwoPaneLayout( + params: RegistrationScaffold.Params.TwoPane, + state: QuickRestoreQrState, + onEvent: (QuickRestoreQrEvents) -> Unit, + modifier: Modifier +) { + val firstPaneScrollState = rememberScrollState() + val secondPaneScrollState = rememberScrollState() + + TwoPaneRegistrationScaffold( + modifier = modifier.fillMaxSize(), + params = params, + firstPane = { paddingValues -> + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(firstPaneScrollState) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Heading() + Spacer(modifier = Modifier.height(32.dp)) + Instructions() + } + }, + secondPane = { paddingValues -> + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .verticalScroll(secondPaneScrollState) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally + ) { + QrCodePane(state = state, onEvent = onEvent) + } + }, + footer = { + CancelFooter(onEvent) + } + ) +} + +@Composable +private fun Heading() { + Text( + text = stringResource(R.string.QuickRestoreQRScreen__scan), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) +} + +@Composable +private fun QrCodePane( + state: QuickRestoreQrState, + onEvent: (QuickRestoreQrEvents) -> Unit +) { + Box( + modifier = Modifier + .widthIn(max = 280.dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + AnimatedContent( + targetState = state.qrState, + contentKey = { it::class }, + label = "qr-code-state" + ) { qrState -> + when (qrState) { + is QrState.Loaded -> { + QrCode( + data = qrState.qrCodeData, + foregroundColor = Color(0xFF2449C0), + modifier = Modifier.fillMaxSize() + ) + } + + QrState.Loading -> { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + } + + QrState.Scanned -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.QuickRestoreQRScreen__scanned), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center ) - } - QrState.Loading -> { - CircularProgressIndicator(modifier = Modifier.size(48.dp)) - } + Spacer(modifier = Modifier.height(8.dp)) - QrState.Scanned -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.QuickRestoreQRScreen__scanned), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { - Text(stringResource(R.string.QuickRestoreQRScreen__retry)) - } + Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { + Text(stringResource(R.string.QuickRestoreQRScreen__retry)) } } + } - QrState.Failed -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.QuickRestoreQRScreen__failed), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) + QrState.Failed -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.QuickRestoreQRScreen__failed), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { - Text(stringResource(R.string.QuickRestoreQRScreen__retry)) - } + Button(onClick = { onEvent(QuickRestoreQrEvents.RetryQrCode) }) { + Text(stringResource(R.string.QuickRestoreQRScreen__retry)) } } } } } } + } +} - Spacer(modifier = Modifier.height(32.dp)) +@Composable +private fun Instructions() { + Column( + modifier = Modifier.widthIn(max = 320.dp) + ) { + InstructionRow( + vector = SignalIcons.Phone.imageVector, + instruction = stringResource(R.string.QuickRestoreQRScreen__step_1) + ) - // Instructions - Column( - modifier = Modifier.widthIn(max = 320.dp) - ) { - InstructionRow( - vector = SignalIcons.Phone.imageVector, - instruction = stringResource(R.string.QuickRestoreQRScreen__step_1) - ) + InstructionRow( + vector = SignalIcons.Camera.imageVector, + instruction = stringResource(R.string.QuickRestoreQRScreen__step_2) + ) - InstructionRow( - vector = SignalIcons.Camera.imageVector, - instruction = stringResource(R.string.QuickRestoreQRScreen__step_2) - ) - - InstructionRow( - vector = SignalIcons.QrCode.imageVector, - instruction = stringResource(R.string.QuickRestoreQRScreen__step_3) - ) - } - - Spacer(modifier = Modifier.weight(1f)) + InstructionRow( + vector = SignalIcons.QrCode.imageVector, + instruction = stringResource(R.string.QuickRestoreQRScreen__step_3) + ) + } +} +@Composable +private fun CancelFooter(onEvent: (QuickRestoreQrEvents) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center + ) { TextButton( onClick = { onEvent(QuickRestoreQrEvents.Cancel) } ) { Text(stringResource(android.R.string.cancel)) } - - Spacer(modifier = Modifier.height(16.dp)) } +} - // Loading dialog +@Composable +private fun StateDialogs( + state: QuickRestoreQrState, + onEvent: (QuickRestoreQrEvents) -> Unit +) { if (state.isRegistering) { - AlertDialog( - onDismissRequest = { }, - confirmButton = { }, - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() - ) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - Spacer(modifier = Modifier.width(16.dp)) - Text(stringResource(R.string.QuickRestoreQRScreen__reregister)) - } - } + Dialogs.IndeterminateProgressDialog( + stringResource(R.string.QuickRestoreQRScreen__reregister) ) } - // Error dialog if (state.showRegistrationError) { - AlertDialog( - onDismissRequest = { onEvent(QuickRestoreQrEvents.DismissError) }, - confirmButton = { - TextButton(onClick = { onEvent(QuickRestoreQrEvents.DismissError) }) { - Text(stringResource(android.R.string.ok)) - } - }, - text = { - Text(state.errorMessage ?: stringResource(R.string.QuickRestoreQRScreen__error)) - } + Dialogs.SimpleMessageDialog( + message = state.errorMessage ?: stringResource(R.string.QuickRestoreQRScreen__error), + dismiss = stringResource(android.R.string.ok), + onDismiss = { onEvent(QuickRestoreQrEvents.DismissError) } ) } } @@ -296,3 +370,17 @@ private fun QuickRestoreQrScreenRegisteringPreview() { ) } } + +@AllDevicePreviews +@Composable +private fun QuickRestoreQrScreenRegisteringFailedPreview() { + Previews.Preview { + QuickRestoreQrScreen( + state = QuickRestoreQrState( + qrState = QrState.Scanned, + showRegistrationError = true + ), + onEvent = {} + ) + } +} diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml index 646c0e7523..bea4b1e8d3 100644 --- a/feature/registration/src/main/res/values/strings.xml +++ b/feature/registration/src/main/res/values/strings.xml @@ -287,21 +287,21 @@ Your account has been locked to protect your privacy and security. After %1$d days of inactivity in your account you\'ll be able to re-register this phone number without needing your PIN. All content will be deleted. - On your old phone, open Signal + Open Signal on your old device - Go to Settings > Transfer account + Tap the camera icon - Scan this QR code + Scan this code with the camera Registering… An error occurred during registration - Scan from old device + Scan this code with your old phone QR code scanned Retry - "Failed to generate QR code" + Unable to generate QR code