Make regV5 resumable if the app closes.

This commit is contained in:
Greyson Parrelli
2026-03-18 16:53:50 -04:00
committed by Michelle Tang
parent c7ec3ab837
commit f09bf5b14c
15 changed files with 895 additions and 41 deletions

View File

@@ -69,7 +69,7 @@ fun NetworkDebugOverlay(
onClick = { showDialog = true },
dragOffset = dragOffset,
onDrag = { delta -> dragOffset += delta },
modifier = Modifier.align(Alignment.CenterEnd)
modifier = Modifier.align(Alignment.TopEnd)
)
if (showDialog) {

View File

@@ -14,7 +14,9 @@ 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.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.ButtonDefaults
@@ -74,6 +76,7 @@ fun MainScreen(
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
@@ -95,6 +98,30 @@ fun MainScreen(
Spacer(modifier = Modifier.height(32.dp))
}
if (state.pendingFlowState != null) {
PendingFlowStateCard(state.pendingFlowState)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { onEvent(MainScreenEvents.LaunchRegistration) },
modifier = Modifier.fillMaxWidth()
) {
Text("Resume Registration")
}
TextButton(
onClick = { showClearDataDialog = true },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("Clear Pending Data")
}
Spacer(modifier = Modifier.height(16.dp))
}
if (state.existingRegistrationState != null) {
if (state.registrationExpired) {
Row(
@@ -150,7 +177,7 @@ fun MainScreen(
) {
Text("Clear All Data")
}
} else {
} else if (state.pendingFlowState == null) {
Button(
onClick = { onEvent(MainScreenEvents.LaunchRegistration) },
modifier = Modifier.fillMaxWidth()
@@ -213,6 +240,55 @@ private fun RegistrationField(label: String, value: String) {
}
}
@Composable
private fun PendingFlowStateCard(pending: MainScreenState.PendingFlowState) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "In-Progress Registration",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
RegistrationField(label = "Current Screen", value = pending.currentScreen)
RegistrationField(label = "Backstack Depth", value = pending.backstackSize.toString())
if (pending.e164 != null) {
RegistrationField(label = "Phone Number", value = pending.e164)
}
RegistrationField(label = "Has Session", value = if (pending.hasSession) "Yes" else "No")
RegistrationField(label = "Has AEP", value = if (pending.hasAccountEntropyPool) "Yes" else "No")
}
}
}
@Preview(showBackground = true)
@Composable
private fun MainScreenWithPendingFlowStatePreview() {
Previews.Preview {
MainScreen(
state = MainScreenState(
pendingFlowState = MainScreenState.PendingFlowState(
e164 = "+15551234567",
backstackSize = 4,
currentScreen = "VerificationCodeEntry",
hasSession = true,
hasAccountEntropyPool = false
)
),
onEvent = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun MainScreenPreview() {

View File

@@ -7,8 +7,17 @@ package org.signal.registration.sample.screens.main
data class MainScreenState(
val existingRegistrationState: ExistingRegistrationState? = null,
val registrationExpired: Boolean = false
val registrationExpired: Boolean = false,
val pendingFlowState: PendingFlowState? = null
) {
data class PendingFlowState(
val e164: String?,
val backstackSize: Int,
val currentScreen: String,
val hasSession: Boolean,
val hasAccountEntropyPool: Boolean
)
data class ExistingRegistrationState(
val phoneNumber: String,
val aci: String,

View File

@@ -12,9 +12,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.registration.NetworkController
import org.signal.registration.PersistedFlowState
import org.signal.registration.StorageController
import org.signal.registration.sample.storage.RegistrationPreferences
@@ -75,6 +77,7 @@ class MainScreenViewModel(
} else {
null
},
pendingFlowState = loadPendingFlowState(),
registrationExpired = false
)
@@ -84,6 +87,27 @@ class MainScreenViewModel(
}
}
private suspend fun loadPendingFlowState(): MainScreenState.PendingFlowState? {
return try {
val data = storageController.readInProgressRegistrationData()
if (data.flowStateJson.isEmpty()) return null
val json = Json { ignoreUnknownKeys = true }
val persisted = json.decodeFromString(PersistedFlowState.serializer(), data.flowStateJson)
MainScreenState.PendingFlowState(
e164 = persisted.sessionE164,
backstackSize = persisted.backStack.size,
currentScreen = persisted.backStack.lastOrNull()?.let { it::class.simpleName } ?: "Unknown",
hasSession = persisted.sessionMetadata != null,
hasAccountEntropyPool = data.accountEntropyPool.isNotEmpty()
)
} catch (e: Exception) {
Log.w(TAG, "Failed to load pending flow state", e)
null
}
}
private suspend fun checkRegistrationStatus() {
when (val result = networkController.getSvrCredentials()) {
is NetworkController.RegistrationNetworkResult.Success -> {