diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 4d8c0f04b1..a537f836a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -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()") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreFragment.kt index 42263da521..05ca3fa994 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreFragment.kt @@ -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() + 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())) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreViewModel.kt new file mode 100644 index 0000000000..1d2bfbca8a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/NoBackupToRestoreViewModel.kt @@ -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 = MutableStateFlow(NoBackupToRestoreState(provisioningMessage = decode)) + + val state: StateFlow = 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 + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RestoreViaQrFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RestoreViaQrFragment.kt index d5f7dae52a..c0530c9844 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RestoreViaQrFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/RestoreViaQrFragment.kt @@ -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()) } }