Update reg v5 UI for quick restore.

This commit is contained in:
Michelle Tang
2026-05-21 10:38:23 -04:00
committed by jeffrey-signal
parent 46b8ac6561
commit f2e8b83604
2 changed files with 223 additions and 135 deletions
@@ -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 = {}
)
}
}
@@ -287,21 +287,21 @@
<string name="AccountLockedScreen__your_account">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.</string>
<!-- Step 1 to quick restore -->
<string name="QuickRestoreQRScreen__step_1">On your old phone, open Signal</string>
<string name="QuickRestoreQRScreen__step_1">Open Signal on your old device</string>
<!-- Step 2 to quick restore -->
<string name="QuickRestoreQRScreen__step_2">Go to Settings > Transfer account</string>
<string name="QuickRestoreQRScreen__step_2">Tap the camera icon </string>
<!-- Step 3 to quick restore -->
<string name="QuickRestoreQRScreen__step_3">Scan this QR code</string>
<string name="QuickRestoreQRScreen__step_3">Scan this code with the camera</string>
<!-- Loading dialog text when registering -->
<string name="QuickRestoreQRScreen__reregister">Registering…</string>
<!-- Error message text shown if registering fails -->
<string name="QuickRestoreQRScreen__error">An error occurred during registration</string>
<!-- Title for quick restoring -->
<string name="QuickRestoreQRScreen__scan">Scan from old device</string>
<string name="QuickRestoreQRScreen__scan">Scan this code with your old phone</string>
<!-- Confirmation dialog when qr code is successfully scanned -->
<string name="QuickRestoreQRScreen__scanned">QR code scanned</string>
<!-- Button text to retry scanning the QR code -->
<string name="QuickRestoreQRScreen__retry">Retry</string>
<!-- Failure text when a code could not be generated -->
<string name="QuickRestoreQRScreen__failed">"Failed to generate QR code"</string>
<string name="QuickRestoreQRScreen__failed">Unable to generate QR code</string>
</resources>