Implement skip restore on the iOS to android failure screen.

This commit is contained in:
Cody Henthorne
2025-10-14 12:01:36 -04:00
parent 525175f04a
commit 7b25cc399d
4 changed files with 155 additions and 4 deletions

View File

@@ -30,6 +30,7 @@ import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.RestoreTimestampResult
import org.thoughtcrime.securesms.database.model.databaseprotos.LinkedDeviceInfo
@@ -150,6 +151,8 @@ class RegistrationViewModel : ViewModel() {
}
}
var registrationProvisioningMessage: RegistrationProvisionMessage? = null
@SuppressLint("MissingPermission")
fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()")

View File

@@ -5,6 +5,8 @@
package org.thoughtcrime.securesms.registration.ui.restore
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -17,40 +19,97 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.shared.RegistrationScreen
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
import kotlin.getValue
/**
* Shown when the old device is iOS and they are trying to transfer/restore on Android without a Signal Backup.
*/
class NoBackupToRestoreFragment : ComposeFragment() {
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
private val viewModel by viewModel {
NoBackupToRestoreViewModel(sharedViewModel.registrationProvisioningMessage!!)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
sharedViewModel
.state
.map { it.registerAccountError }
.filterNotNull()
.collect {
sharedViewModel.registerAccountErrorShown()
viewModel.handleRegistrationFailure(it)
}
}
}
}
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsState()
NoBackupToRestoreContent(
onSkipRestore = {},
state = state,
onSkipRestore = {
viewModel.skipRestoreAndRegister()
val message = viewModel.state.value.provisioningMessage
sharedViewModel.registerWithBackupKey(
context = requireContext(),
backupKey = message.accountEntropyPool,
e164 = message.e164,
pin = message.pin,
aciIdentityKeyPair = message.aciIdentityKeyPair,
pniIdentityKeyPair = message.pniIdentityKeyPair
)
},
onCancel = {
sharedViewModel.registrationProvisioningMessage = null
findNavController().safeNavigate(NoBackupToRestoreFragmentDirections.restartRegistrationFlow())
}
},
onRegistrationErrorDismiss = viewModel::clearRegistrationError
)
}
}
@Composable
private fun NoBackupToRestoreContent(
state: NoBackupToRestoreViewModel.NoBackupToRestoreState,
onSkipRestore: () -> Unit = {},
onCancel: () -> Unit = {}
onCancel: () -> Unit = {},
onRegistrationErrorDismiss: () -> Unit = {}
) {
RegistrationScreen(
title = stringResource(id = R.string.NoBackupToRestore_title),
@@ -83,6 +142,22 @@ private fun NoBackupToRestoreContent(
StepRow(icon = painterResource(R.drawable.symbol_check_circle_24), text = stringResource(id = R.string.NoBackupToRestore_step3))
}
if (state.isRegistering) {
Dialogs.IndeterminateProgressDialog()
} else if (state.showRegistrationError) {
val message = when (state.registerAccountResult) {
is RegisterAccountResult.IncorrectRecoveryPassword -> stringResource(R.string.RestoreViaQr_registration_error)
is RegisterAccountResult.RateLimited -> stringResource(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
else -> stringResource(R.string.RegistrationActivity_error_connecting_to_service)
}
Dialogs.SimpleMessageDialog(
message = message,
onDismiss = onRegistrationErrorDismiss,
dismiss = stringResource(android.R.string.ok)
)
}
}
}
@@ -113,6 +188,6 @@ private fun StepRow(
@Composable
private fun NoBackupToRestoreContentPreview() {
Previews.Preview {
NoBackupToRestoreContent()
NoBackupToRestoreContent(state = NoBackupToRestoreViewModel.NoBackupToRestoreState(provisioningMessage = RegistrationProvisionMessage()))
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.ui.restore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.signal.registration.proto.RegistrationProvisionMessage
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
import org.thoughtcrime.securesms.registration.data.QuickRegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
import org.whispersystems.signalservice.api.provisioning.RestoreMethod
class NoBackupToRestoreViewModel(decode: RegistrationProvisionMessage) : ViewModel() {
companion object {
private val TAG = Log.tag(NoBackupToRestoreViewModel::class)
}
private val store: MutableStateFlow<NoBackupToRestoreState> = MutableStateFlow(NoBackupToRestoreState(provisioningMessage = decode))
val state: StateFlow<NoBackupToRestoreState> = store
fun skipRestoreAndRegister() {
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
store.update { it.copy(isRegistering = true) }
viewModelScope.launch(Dispatchers.IO) {
QuickRegistrationRepository.setRestoreMethodForOldDevice(RestoreMethod.DECLINE)
}
}
fun handleRegistrationFailure(registerAccountResult: RegisterAccountResult) {
store.update {
if (it.isRegistering) {
Log.w(TAG, "Unable to register [${registerAccountResult::class.simpleName}]", registerAccountResult.getCause(), true)
it.copy(
isRegistering = false,
showRegistrationError = true,
registerAccountResult = registerAccountResult
)
} else {
it
}
}
}
fun clearRegistrationError() {
store.update {
it.copy(
showRegistrationError = false,
registerAccountResult = null
)
}
}
data class NoBackupToRestoreState(
val isRegistering: Boolean = false,
val provisioningMessage: RegistrationProvisionMessage,
val showRegistrationError: Boolean = false,
val registerAccountResult: RegisterAccountResult? = null
)
}

View File

@@ -110,6 +110,7 @@ class RestoreViaQrFragment : ComposeFragment() {
if (message.platform == RegistrationProvisionMessage.Platform.ANDROID || message.tier != null) {
sharedViewModel.registerWithBackupKey(requireContext(), message.accountEntropyPool, message.e164, message.pin, message.aciIdentityKeyPair, message.pniIdentityKeyPair)
} else {
sharedViewModel.registrationProvisioningMessage = message
findNavController().safeNavigate(RestoreViaQrFragmentDirections.goToNoBackupToRestore())
}
}