mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 02:39:55 +01:00
Revamp restore decisions state and flesh out post registration restore options.
This commit is contained in:
committed by
Greyson Parrelli
parent
b78747fda2
commit
fe44789d88
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreActivity
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
@@ -88,7 +89,7 @@ class RegistrationActivity : BaseActivity() {
|
||||
|
||||
val nextIntent: Intent? = when {
|
||||
needsPin -> CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationActivity)
|
||||
!SignalStore.registration.hasSkippedTransferOrRestore() && RemoteConfig.messageBackups -> RemoteRestoreActivity.getIntent(this@RegistrationActivity)
|
||||
SignalStore.registration.restoreDecisionState.isDecisionPending && RemoteConfig.messageBackups -> RemoteRestoreActivity.getIntent(this@RegistrationActivity)
|
||||
needsProfile -> CreateProfileActivity.getIntentForUserProfile(this@RegistrationActivity)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@ enum class RegistrationCheckpoint {
|
||||
PIN_ENTERED,
|
||||
VERIFICATION_CODE_VALIDATED,
|
||||
SERVICE_REGISTRATION_COMPLETED,
|
||||
BACKUP_TIER_NOT_RESTORED,
|
||||
LOCAL_REGISTRATION_COMPLETE
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -26,6 +27,7 @@ import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
||||
@@ -33,7 +35,13 @@ import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob
|
||||
import org.thoughtcrime.securesms.keyvalue.NewAccount
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.Skipped
|
||||
import org.thoughtcrime.securesms.keyvalue.Start
|
||||
import org.thoughtcrime.securesms.keyvalue.intendToRestore
|
||||
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
|
||||
import org.thoughtcrime.securesms.keyvalue.isWantingManualRemoteRestore
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository
|
||||
import org.thoughtcrime.securesms.pin.SvrWrongPinException
|
||||
@@ -80,6 +88,7 @@ import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@@ -863,9 +872,9 @@ class RegistrationViewModel : ViewModel() {
|
||||
SignalStore.registration.localRegistrationMetadata = metadata
|
||||
RegistrationRepository.registerAccountLocally(context, metadata)
|
||||
|
||||
if (!remoteResult.storageCapable && !SignalStore.registration.hasCompletedRestore()) {
|
||||
if (!remoteResult.storageCapable && SignalStore.registration.restoreDecisionState.isDecisionPending) {
|
||||
// Not being storage capable is a high signal that account is new and there's no data to restore
|
||||
SignalStore.registration.markSkippedTransferOrRestore()
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
|
||||
}
|
||||
|
||||
if (reglockEnabled || SignalStore.svr.hasOptedInWithAccess()) {
|
||||
@@ -892,13 +901,59 @@ class RegistrationViewModel : ViewModel() {
|
||||
|
||||
refreshRemoteConfig()
|
||||
|
||||
val checkpoint = if (SignalStore.registration.restoreDecisionState.isDecisionPending &&
|
||||
SignalStore.registration.restoreDecisionState.isWantingManualRemoteRestore &&
|
||||
!SignalStore.backup.isBackupTierRestored
|
||||
) {
|
||||
RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED
|
||||
} else {
|
||||
RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE
|
||||
registrationCheckpoint = checkpoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetRestoreDecision() {
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Start
|
||||
}
|
||||
|
||||
fun intendToRestore(hasOldDevice: Boolean, fromRemote: Boolean? = null) {
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.intendToRestore(hasOldDevice, fromRemote)
|
||||
}
|
||||
|
||||
fun skipRestoreAfterRegistration() {
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreBackupTier() {
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val start = System.currentTimeMillis()
|
||||
val tierUnknown = BackupRepository.restoreBackupTier(SignalStore.account.requireAci()) == null
|
||||
delay(max(0L, 500L - (System.currentTimeMillis() - start)))
|
||||
|
||||
if (tierUnknown) {
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED)
|
||||
}
|
||||
} else {
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = RegistrationCheckpoint.LOCAL_REGISTRATION_COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun completeRegistration() {
|
||||
AppDependencies.jobManager.startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import org.thoughtcrime.securesms.restore.enterbackupkey.PostRegistrationEnterBackupKeyViewModel
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
/**
|
||||
* Help verify a potential string could be an [AccountEntropyPool] string. Intended only
|
||||
* for use in [EnterBackupKeyViewModel] and [PostRegistrationEnterBackupKeyViewModel].
|
||||
*/
|
||||
object AccountEntropyPoolVerification {
|
||||
|
||||
/**
|
||||
* Given a backup key and metadata around it's previous verification state, provide an updated or new state.
|
||||
*
|
||||
* @param backupKey key to verify
|
||||
* @param changed if the key has changed from the previous verification attempt
|
||||
* @param previousAEPValidationError the error if any of the previous verification attempt
|
||||
* @return [Pair] of is contents generally valid and any still present or new validation error
|
||||
*/
|
||||
fun verifyAEP(backupKey: String, changed: Boolean, previousAEPValidationError: AEPValidationError?): Pair<Boolean, AEPValidationError?> {
|
||||
val isValid = validateContents(backupKey)
|
||||
val isShort = backupKey.length < AccountEntropyPool.LENGTH
|
||||
val isExact = backupKey.length == AccountEntropyPool.LENGTH
|
||||
|
||||
var updatedError: AEPValidationError? = checkErrorStillApplies(backupKey, previousAEPValidationError, isShort || isExact, isValid, changed)
|
||||
if (updatedError == null) {
|
||||
updatedError = checkForNewError(backupKey, isShort, isExact, isValid)
|
||||
}
|
||||
|
||||
return isValid to updatedError
|
||||
}
|
||||
|
||||
private fun validateContents(backupKey: String): Boolean {
|
||||
return AccountEntropyPool.isFullyValid(backupKey)
|
||||
}
|
||||
|
||||
private fun checkErrorStillApplies(backupKey: String, error: AEPValidationError?, isShortOrExact: Boolean, isValid: Boolean, isChanged: Boolean): AEPValidationError? {
|
||||
return when (error) {
|
||||
is AEPValidationError.TooLong -> if (isShortOrExact) null else error.copy(count = backupKey.length)
|
||||
AEPValidationError.Invalid -> if (isValid) null else error
|
||||
AEPValidationError.Incorrect -> if (isChanged) null else error
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForNewError(backupKey: String, isShort: Boolean, isExact: Boolean, isValid: Boolean): AEPValidationError? {
|
||||
return if (!isShort && !isExact) {
|
||||
AEPValidationError.TooLong(backupKey.length, AccountEntropyPool.LENGTH)
|
||||
} else if (!isValid && isExact) {
|
||||
AEPValidationError.Invalid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface AEPValidationError {
|
||||
data class TooLong(val count: Int, val max: Int) : AEPValidationError
|
||||
data object Invalid : AEPValidationError
|
||||
data object Incorrect : AEPValidationError
|
||||
}
|
||||
}
|
||||
@@ -5,51 +5,11 @@
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -57,24 +17,17 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationState
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.phonenumber.EnterPhoneNumberMode
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.EnterBackupKeyViewModel.EnterBackupKeyState
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -105,6 +58,17 @@ class EnterBackupKeyFragment : ComposeFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
sharedViewModel
|
||||
.state
|
||||
.filter { it.registrationCheckpoint == RegistrationCheckpoint.BACKUP_TIER_NOT_RESTORED }
|
||||
.collect {
|
||||
viewModel.handleBackupTierNotRestored()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -114,8 +78,10 @@ class EnterBackupKeyFragment : ComposeFragment() {
|
||||
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = viewModel.backupKey,
|
||||
state = state,
|
||||
sharedState = sharedState,
|
||||
inProgress = sharedState.inProgress,
|
||||
isBackupKeyValid = state.backupKeyValid,
|
||||
chunkLength = state.chunkLength,
|
||||
aepValidationError = state.aepValidationError,
|
||||
onBackupKeyChanged = viewModel::updateBackupKey,
|
||||
onNextClicked = {
|
||||
viewModel.registering()
|
||||
@@ -126,276 +92,64 @@ class EnterBackupKeyFragment : ComposeFragment() {
|
||||
pin = null
|
||||
)
|
||||
},
|
||||
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
|
||||
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
|
||||
|
||||
onLearnMore = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) },
|
||||
onSkip = { findNavController().safeNavigate(EnterBackupKeyFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreen(
|
||||
backupKey: String,
|
||||
state: EnterBackupKeyState,
|
||||
sharedState: RegistrationState,
|
||||
onBackupKeyChanged: (String) -> Unit = {},
|
||||
onRegistrationErrorDismiss: () -> Unit = {},
|
||||
onBackupKeyHelp: () -> Unit = {},
|
||||
onNextClicked: () -> Unit = {},
|
||||
onLearnMore: () -> Unit = {},
|
||||
onSkip: () -> Unit = {}
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
RegistrationScreen(
|
||||
title = stringResource(R.string.EnterBackupKey_title),
|
||||
subtitle = stringResource(R.string.EnterBackupKey_subtitle),
|
||||
bottomContent = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !sharedState.inProgress,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_no_backup_key)
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state.isRegistering,
|
||||
label = "next-progress"
|
||||
) { isRegistering ->
|
||||
if (isRegistering) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
} else {
|
||||
Buttons.LargeTonal(
|
||||
enabled = state.backupKeyValid && state.aepValidationError == null,
|
||||
onClick = onNextClicked
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val visualTransform = remember(state.chunkLength) { BackupKeyVisualTransformation(chunkSize = state.chunkLength) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
TextField(
|
||||
value = backupKey,
|
||||
onValueChange = onBackupKeyChanged,
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.EnterBackupKey_backup_key))
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontFamily = FontFamily(typeface = Typeface.MONOSPACE),
|
||||
lineHeight = 36.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Next,
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (state.backupKeyValid) {
|
||||
keyboardController?.hide()
|
||||
onNextClicked()
|
||||
}
|
||||
}
|
||||
),
|
||||
supportingText = { state.aepValidationError?.ValidationErrorMessage() },
|
||||
isError = state.aepValidationError != null,
|
||||
minLines = 4,
|
||||
visualTransformation = visualTransform,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
if (sheetState.isVisible) {
|
||||
ModalBottomSheet(
|
||||
dragHandle = null,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
}
|
||||
}
|
||||
) {
|
||||
NoBackupKeyBottomSheet(
|
||||
onLearnMore = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
}
|
||||
onLearnMore()
|
||||
},
|
||||
onSkip = onSkip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showRegistrationError) {
|
||||
if (state.registerAccountResult is RegisterAccountResult.IncorrectRecoveryPassword) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.EnterBackupKey_incorrect_backup_key_title),
|
||||
body = stringResource(R.string.EnterBackupKey_incorrect_backup_key_message),
|
||||
confirm = stringResource(R.string.EnterBackupKey_try_again),
|
||||
dismiss = stringResource(R.string.EnterBackupKey_backup_key_help),
|
||||
onConfirm = {},
|
||||
onDeny = onBackupKeyHelp,
|
||||
onDismiss = onRegistrationErrorDismiss
|
||||
)
|
||||
} else {
|
||||
val message = when (state.registerAccountResult) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnterBackupKeyViewModel.AEPValidationError.ValidationErrorMessage() {
|
||||
when (this) {
|
||||
is EnterBackupKeyViewModel.AEPValidationError.TooLong -> Text(text = stringResource(R.string.EnterBackupKey_too_long_error, this.count, this.max))
|
||||
EnterBackupKeyViewModel.AEPValidationError.Invalid -> Text(text = stringResource(R.string.EnterBackupKey_invalid_backup_key_error))
|
||||
EnterBackupKeyViewModel.AEPValidationError.Incorrect -> Text(text = stringResource(R.string.EnterBackupKey_incorrect_backup_key_error))
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreenPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
state = EnterBackupKeyState(requiredLength = 64, chunkLength = 4),
|
||||
sharedState = RegistrationState(phoneNumber = null, recoveryPassword = null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreenErrorPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
state = EnterBackupKeyState(requiredLength = 64, chunkLength = 4, aepValidationError = EnterBackupKeyViewModel.AEPValidationError.Invalid),
|
||||
sharedState = RegistrationState(phoneNumber = null, recoveryPassword = null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoBackupKeyBottomSheet(
|
||||
onLearnMore: () -> Unit = {},
|
||||
onSkip: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_key_24),
|
||||
tint = BackupsIconColors.Success.foreground,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 18.dp, bottom = 16.dp)
|
||||
.size(88.dp)
|
||||
.background(
|
||||
color = BackupsIconColors.Success.background,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(20.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_backup_key),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_key_paragraph_1),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_key_paragraph_1),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onLearnMore
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_learn_more)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onSkip
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_skip_and_dont_restore)
|
||||
)
|
||||
}
|
||||
ErrorContent(
|
||||
state = state,
|
||||
onBackupTierRetry = { sharedViewModel.restoreBackupTier() },
|
||||
onSkipRestoreAfterRegistration = sharedViewModel::skipRestoreAfterRegistration,
|
||||
onBackupTierNotRestoredDismiss = viewModel::hideRestoreBackupKeyFailed,
|
||||
onRegistrationErrorDismiss = viewModel::clearRegistrationError,
|
||||
onBackupKeyHelp = { CommunicationActions.openBrowserLink(requireContext(), LEARN_MORE_URL) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NoBackupKeyBottomSheetPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
NoBackupKeyBottomSheet()
|
||||
private fun ErrorContent(
|
||||
state: EnterBackupKeyViewModel.EnterBackupKeyState,
|
||||
onBackupTierRetry: () -> Unit = {},
|
||||
onSkipRestoreAfterRegistration: () -> Unit = {},
|
||||
onBackupTierNotRestoredDismiss: () -> Unit = {},
|
||||
onRegistrationErrorDismiss: () -> Unit = {},
|
||||
onBackupKeyHelp: () -> Unit = {}
|
||||
) {
|
||||
if (state.showBackupTierNotRestoreError) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.EnterBackupKey_backup_not_found),
|
||||
body = stringResource(R.string.EnterBackupKey_backup_key_you_entered_is_correct_but_no_backup),
|
||||
confirm = stringResource(R.string.EnterBackupKey_try_again),
|
||||
dismiss = stringResource(R.string.EnterBackupKey_skip_restore),
|
||||
onConfirm = onBackupTierRetry,
|
||||
onDeny = onSkipRestoreAfterRegistration,
|
||||
onDismiss = onBackupTierNotRestoredDismiss
|
||||
)
|
||||
} else if (state.showRegistrationError) {
|
||||
if (state.registerAccountResult is RegisterAccountResult.IncorrectRecoveryPassword) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.EnterBackupKey_incorrect_backup_key_title),
|
||||
body = stringResource(R.string.EnterBackupKey_incorrect_backup_key_message),
|
||||
confirm = stringResource(R.string.EnterBackupKey_try_again),
|
||||
dismiss = stringResource(R.string.EnterBackupKey_backup_key_help),
|
||||
onConfirm = {},
|
||||
onDeny = onBackupKeyHelp,
|
||||
onDismiss = onRegistrationErrorDismiss
|
||||
)
|
||||
} else {
|
||||
val message = when (state.registerAccountResult) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.restore
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
/**
|
||||
* Shared screen infrastructure for entering an [AccountEntropyPool].
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EnterBackupKeyScreen(
|
||||
backupKey: String,
|
||||
inProgress: Boolean,
|
||||
isBackupKeyValid: Boolean,
|
||||
chunkLength: Int,
|
||||
aepValidationError: AccountEntropyPoolVerification.AEPValidationError?,
|
||||
onBackupKeyChanged: (String) -> Unit = {},
|
||||
onNextClicked: () -> Unit = {},
|
||||
onLearnMore: () -> Unit = {},
|
||||
onSkip: () -> Unit = {},
|
||||
errorContent: @Composable () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
RegistrationScreen(
|
||||
title = stringResource(R.string.EnterBackupKey_title),
|
||||
subtitle = stringResource(R.string.EnterBackupKey_subtitle),
|
||||
bottomContent = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextButton(
|
||||
enabled = !inProgress,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_no_backup_key)
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = inProgress,
|
||||
label = "next-progress"
|
||||
) { inProgress ->
|
||||
if (inProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
} else {
|
||||
Buttons.LargeTonal(
|
||||
enabled = isBackupKeyValid && aepValidationError == null,
|
||||
onClick = onNextClicked
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RegistrationActivity_next)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val visualTransform = remember(chunkLength) { BackupKeyVisualTransformation(chunkSize = chunkLength) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
TextField(
|
||||
value = backupKey,
|
||||
onValueChange = onBackupKeyChanged,
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.EnterBackupKey_backup_key))
|
||||
},
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontFamily = FontFamily(typeface = Typeface.MONOSPACE),
|
||||
lineHeight = 36.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Next,
|
||||
autoCorrectEnabled = false
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
if (isBackupKeyValid) {
|
||||
keyboardController?.hide()
|
||||
onNextClicked()
|
||||
}
|
||||
}
|
||||
),
|
||||
supportingText = { aepValidationError?.ValidationErrorMessage() },
|
||||
isError = aepValidationError != null,
|
||||
minLines = 4,
|
||||
visualTransformation = visualTransform,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
if (sheetState.isVisible) {
|
||||
ModalBottomSheet(
|
||||
dragHandle = null,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
}
|
||||
}
|
||||
) {
|
||||
NoBackupKeyBottomSheet(
|
||||
onLearnMore = {
|
||||
coroutineScope.launch {
|
||||
sheetState.hide()
|
||||
}
|
||||
onLearnMore()
|
||||
},
|
||||
onSkip = onSkip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
errorContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountEntropyPoolVerification.AEPValidationError.ValidationErrorMessage() {
|
||||
when (this) {
|
||||
is AccountEntropyPoolVerification.AEPValidationError.TooLong -> Text(text = stringResource(R.string.EnterBackupKey_too_long_error, this.count, this.max))
|
||||
AccountEntropyPoolVerification.AEPValidationError.Invalid -> Text(text = stringResource(R.string.EnterBackupKey_invalid_backup_key_error))
|
||||
AccountEntropyPoolVerification.AEPValidationError.Incorrect -> Text(text = stringResource(R.string.EnterBackupKey_incorrect_backup_key_error))
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreenPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
isBackupKeyValid = true,
|
||||
inProgress = false,
|
||||
chunkLength = 4,
|
||||
aepValidationError = null
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun EnterBackupKeyScreenErrorPreview() {
|
||||
Previews.Preview {
|
||||
EnterBackupKeyScreen(
|
||||
backupKey = "UY38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t",
|
||||
isBackupKeyValid = true,
|
||||
inProgress = false,
|
||||
chunkLength = 4,
|
||||
aepValidationError = AccountEntropyPoolVerification.AEPValidationError.Invalid
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoBackupKeyBottomSheet(
|
||||
onLearnMore: () -> Unit = {},
|
||||
onSkip: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_key_24),
|
||||
tint = BackupsIconColors.Success.foreground,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 18.dp, bottom = 16.dp)
|
||||
.size(88.dp)
|
||||
.background(
|
||||
color = BackupsIconColors.Success.background,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(20.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_backup_key),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_key_paragraph_1),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.EnterBackupKey_no_key_paragraph_1),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onLearnMore
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_learn_more)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onSkip
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.EnterBackupKey_skip_and_dont_restore)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NoBackupKeyBottomSheetPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
NoBackupKeyBottomSheet()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.AccountEntropyPoolVerification.AEPValidationError
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
class EnterBackupKeyViewModel : ViewModel() {
|
||||
@@ -36,46 +37,19 @@ class EnterBackupKeyViewModel : ViewModel() {
|
||||
val state: StateFlow<EnterBackupKeyState> = store
|
||||
|
||||
fun updateBackupKey(key: String) {
|
||||
val newKey = AccountEntropyPool.removeIllegalCharacters(key).lowercase()
|
||||
val newKey = AccountEntropyPool.removeIllegalCharacters(key).take(AccountEntropyPool.LENGTH + 16).lowercase()
|
||||
val changed = newKey != backupKey
|
||||
backupKey = newKey
|
||||
store.update {
|
||||
val isValid = validateContents(backupKey)
|
||||
val isShort = backupKey.length < it.requiredLength
|
||||
val isExact = backupKey.length == it.requiredLength
|
||||
|
||||
var updatedError: AEPValidationError? = checkErrorStillApplies(it.aepValidationError, isShort || isExact, isValid, changed)
|
||||
if (updatedError == null) {
|
||||
updatedError = checkForNewError(isShort, isExact, isValid, it.requiredLength)
|
||||
}
|
||||
|
||||
val (isValid, updatedError) = AccountEntropyPoolVerification.verifyAEP(
|
||||
backupKey = backupKey,
|
||||
changed = changed,
|
||||
previousAEPValidationError = it.aepValidationError
|
||||
)
|
||||
it.copy(backupKeyValid = isValid, aepValidationError = updatedError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateContents(backupKey: String): Boolean {
|
||||
return AccountEntropyPool.isFullyValid(backupKey)
|
||||
}
|
||||
|
||||
private fun checkErrorStillApplies(error: AEPValidationError?, isShortOrExact: Boolean, isValid: Boolean, isChanged: Boolean): AEPValidationError? {
|
||||
return when (error) {
|
||||
is AEPValidationError.TooLong -> if (isShortOrExact) null else error.copy(count = backupKey.length)
|
||||
AEPValidationError.Invalid -> if (isValid) null else error
|
||||
AEPValidationError.Incorrect -> if (isChanged) null else error
|
||||
null -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForNewError(isShort: Boolean, isExact: Boolean, isValid: Boolean, requiredLength: Int): AEPValidationError? {
|
||||
return if (!isShort && !isExact) {
|
||||
AEPValidationError.TooLong(backupKey.length, requiredLength)
|
||||
} else if (!isValid && isExact) {
|
||||
AEPValidationError.Invalid
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun registering() {
|
||||
store.update { it.copy(isRegistering = true) }
|
||||
}
|
||||
@@ -111,19 +85,30 @@ class EnterBackupKeyViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleBackupTierNotRestored() {
|
||||
store.update {
|
||||
it.copy(
|
||||
showBackupTierNotRestoreError = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideRestoreBackupKeyFailed() {
|
||||
store.update {
|
||||
it.copy(
|
||||
showBackupTierNotRestoreError = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class EnterBackupKeyState(
|
||||
val backupKeyValid: Boolean = false,
|
||||
val requiredLength: Int,
|
||||
val chunkLength: Int,
|
||||
val isRegistering: Boolean = false,
|
||||
val showRegistrationError: Boolean = false,
|
||||
val showBackupTierNotRestoreError: Boolean = false,
|
||||
val registerAccountResult: RegisterAccountResult? = null,
|
||||
val aepValidationError: AEPValidationError? = null
|
||||
)
|
||||
|
||||
sealed interface AEPValidationError {
|
||||
data class TooLong(val count: Int, val max: Int) : AEPValidationError
|
||||
data object Invalid : AEPValidationError
|
||||
data object Incorrect : AEPValidationError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,16 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
|
||||
import org.thoughtcrime.securesms.keyvalue.Completed
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.Skipped
|
||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registrationv3.data.QuickRegistrationRepository
|
||||
import org.thoughtcrime.securesms.registrationv3.data.RegistrationRepository
|
||||
@@ -103,7 +106,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
|
||||
when (state) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.i(TAG, "Restore successful")
|
||||
SignalStore.registration.markRestoreCompleted()
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Completed
|
||||
|
||||
if (!RegistrationRepository.isMissingProfileData()) {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
@@ -135,7 +138,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
SignalStore.registration.markSkippedTransferOrRestore()
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
@@ -143,7 +146,7 @@ class RemoteRestoreViewModel(isOnlyRestoreOption: Boolean) : ViewModel() {
|
||||
}
|
||||
|
||||
fun skipRestore() {
|
||||
SignalStore.registration.markSkippedTransferOrRestore()
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -53,8 +53,14 @@ class SelectManualRestoreMethodFragment : ComposeFragment() {
|
||||
|
||||
private fun startRestoreMethod(method: RestoreMethod) {
|
||||
when (method) {
|
||||
RestoreMethod.FROM_SIGNAL_BACKUPS -> findNavController().safeNavigate(SelectManualRestoreMethodFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.COLLECT_FOR_MANUAL_SIGNAL_BACKUPS_RESTORE))
|
||||
RestoreMethod.FROM_LOCAL_BACKUP_V1 -> launchRestoreActivity.launch(RestoreActivity.getLocalRestoreIntent(requireContext()))
|
||||
RestoreMethod.FROM_SIGNAL_BACKUPS -> {
|
||||
sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = true)
|
||||
findNavController().safeNavigate(SelectManualRestoreMethodFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.COLLECT_FOR_MANUAL_SIGNAL_BACKUPS_RESTORE))
|
||||
}
|
||||
RestoreMethod.FROM_LOCAL_BACKUP_V1 -> {
|
||||
sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false)
|
||||
launchRestoreActivity.launch(RestoreActivity.getLocalRestoreIntent(requireContext()))
|
||||
}
|
||||
RestoreMethod.FROM_OLD_DEVICE -> error("Device transfer not supported in manual restore flow")
|
||||
RestoreMethod.FROM_LOCAL_BACKUP_V2 -> error("Not currently supported")
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
}
|
||||
|
||||
private fun navigateToNextScreenViaContinue() {
|
||||
sharedViewModel.resetRestoreDecision()
|
||||
sharedViewModel.maybePrefillE164(requireContext())
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.NORMAL))
|
||||
}
|
||||
@@ -109,8 +110,14 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
|
||||
when (userSelection) {
|
||||
WelcomeUserSelection.CONTINUE -> throw IllegalArgumentException()
|
||||
WelcomeUserSelection.RESTORE_WITH_OLD_PHONE -> findNavController().safeNavigate(WelcomeFragmentDirections.goToRestoreViaQr())
|
||||
WelcomeUserSelection.RESTORE_WITH_NO_PHONE -> findNavController().safeNavigate(WelcomeFragmentDirections.goToSelectRestoreMethod(userSelection))
|
||||
WelcomeUserSelection.RESTORE_WITH_OLD_PHONE -> {
|
||||
sharedViewModel.intendToRestore(hasOldDevice = true, fromRemote = true)
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.goToRestoreViaQr())
|
||||
}
|
||||
WelcomeUserSelection.RESTORE_WITH_NO_PHONE -> {
|
||||
sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = true)
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.goToSelectRestoreMethod(userSelection))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user