From e8194734dbbfd94dfafb87d876faa655fee431c4 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 19 Mar 2025 11:32:24 +0100 Subject: [PATCH] DROID-3476 Migration | Enhancement | Update migration UX flow (#2168) --- .../screens/signin/MnemonicPhraseFormatter.kt | 45 +++++ .../OnboardingRecoveryPhraseLoginScreen.kt | 109 +++-------- .../ui/onboarding/screens/signin/Previews.kt | 37 ++++ .../anytype/ui/splash/SplashFragment.kt | 23 ++- .../anytype/ui/update/MigrationErrorScreen.kt | 172 +++++++++++++++++- localization/src/main/res/values/strings.xml | 10 + .../login/OnboardingMnemonicLoginViewModel.kt | 13 +- .../presentation/splash/SplashViewModel.kt | 12 +- 8 files changed, 323 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/MnemonicPhraseFormatter.kt create mode 100644 app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/Previews.kt diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/MnemonicPhraseFormatter.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/MnemonicPhraseFormatter.kt new file mode 100644 index 0000000000..4df1d79d5b --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/MnemonicPhraseFormatter.kt @@ -0,0 +1,45 @@ +package com.anytypeio.anytype.ui.onboarding.screens.signin + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.withStyle +import com.anytypeio.anytype.core_ui.MnemonicPhrasePaletteColors + +object MnemonicPhraseFormatter : VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText { + val transformed = buildAnnotatedString { + var colorIndex = 0 + var isPreviousLetterOrDigit = false + text.forEachIndexed { index, char -> + if (char.isLetterOrDigit()) { + withStyle( + style = SpanStyle( + color = MnemonicPhrasePaletteColors[colorIndex] + ) + ) { + append(char) + } + isPreviousLetterOrDigit = true + } else { + if (isPreviousLetterOrDigit) { + colorIndex = colorIndex.inc() + isPreviousLetterOrDigit = false + } + append(char) + } + if (colorIndex > MnemonicPhrasePaletteColors.lastIndex) { + colorIndex = 0 + } + } + } + return TransformedText( + transformed, + OffsetMapping.Identity + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt index b98afbd6f4..6d2a6cc228 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/OnboardingRecoveryPhraseLoginScreen.kt @@ -1,6 +1,5 @@ package com.anytypeio.anytype.ui.onboarding.screens.signin -import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -27,23 +26,14 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.OffsetMapping -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.anytypeio.anytype.BuildConfig import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_ui.ColorButtonRegular -import com.anytypeio.anytype.core_ui.MnemonicPhrasePaletteColors import com.anytypeio.anytype.core_ui.OnBoardingTextPrimaryColor import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.ButtonSize @@ -57,6 +47,7 @@ import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLog import com.anytypeio.anytype.ui.onboarding.OnboardingMnemonicInput import com.anytypeio.anytype.ui.update.MigrationFailedScreen import com.anytypeio.anytype.ui.update.MigrationInProgressScreen +import com.anytypeio.anytype.ui.update.MigrationStartScreen import kotlin.Unit @Composable @@ -75,7 +66,8 @@ fun RecoveryScreenWrapper( onDebugAccountTraceClicked = { vm.onAccountThraceButtonClicked() }, - onRetryMigrationClicked = vm::onRetryMigrationClicked + onRetryMigrationClicked = vm::onRetryMigrationClicked, + onStartMigrationClicked = vm::onStartMigrationClicked ) } @@ -88,7 +80,8 @@ fun RecoveryScreen( state: SetupState, onEnterMyVaultClicked: () -> Unit, onDebugAccountTraceClicked: () -> Unit, - onRetryMigrationClicked: (Id) -> Unit + onRetryMigrationClicked: (Id) -> Unit, + onStartMigrationClicked: (Id) -> Unit ) { val focus = LocalFocusManager.current val context = LocalContext.current @@ -223,84 +216,30 @@ fun RecoveryScreen( } } ) - if (state is SetupState.Migration.InProgress) { - MigrationInProgressScreen() - } else if(state is SetupState.Migration.Failed) { - MigrationFailedScreen( - state = state.state, - onRetryClicked = { - onRetryMigrationClicked(state.account) + + if (state is SetupState.Migration) { + when(state) { + is SetupState.Migration.Failed -> { + MigrationFailedScreen( + state = state.state, + onRetryClicked = { + onRetryMigrationClicked(state.account) + } + ) } - ) - } - } -} - -typealias Mnemonic = String - -object MnemonicPhraseFormatter : VisualTransformation { - - override fun filter(text: AnnotatedString): TransformedText { - val transformed = buildAnnotatedString { - var colorIndex = 0 - var isPreviousLetterOrDigit = false - text.forEachIndexed { index, char -> - if (char.isLetterOrDigit()) { - withStyle( - style = SpanStyle( - color = MnemonicPhrasePaletteColors[colorIndex] - ) - ) { - append(char) - } - isPreviousLetterOrDigit = true - } else { - if (isPreviousLetterOrDigit) { - colorIndex = colorIndex.inc() - isPreviousLetterOrDigit = false - } - append(char) + is SetupState.Migration.InProgress -> { + MigrationInProgressScreen() } - if (colorIndex > MnemonicPhrasePaletteColors.lastIndex) { - colorIndex = 0 + is SetupState.Migration.AwaitingStart -> { + MigrationStartScreen( + onStartUpdate = { + onStartMigrationClicked(state.account) + } + ) } } } - return TransformedText( - transformed, - OffsetMapping.Identity - ) } } -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode") -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") -@Composable -fun RecoveryScreenPreview() { - RecoveryScreen( - onBackClicked = {}, - onNextClicked = {}, - onActionDoneClicked = {}, - onScanQrClicked = {}, - state = SetupState.Idle, - onEnterMyVaultClicked = {}, - onDebugAccountTraceClicked = {}, - onRetryMigrationClicked = {} - ) -} - -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode") -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") -@Composable -fun RecoveryScreenLoadingPreview() { - RecoveryScreen( - onBackClicked = {}, - onNextClicked = {}, - onActionDoneClicked = {}, - onScanQrClicked = {}, - state = SetupState.InProgress, - onEnterMyVaultClicked = {}, - onDebugAccountTraceClicked = {}, - onRetryMigrationClicked = {} - ) -} \ No newline at end of file +typealias Mnemonic = String \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/Previews.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/Previews.kt new file mode 100644 index 0000000000..2c80e64d1a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/screens/signin/Previews.kt @@ -0,0 +1,37 @@ +package com.anytypeio.anytype.ui.onboarding.screens.signin + +import androidx.compose.runtime.Composable +import com.anytypeio.anytype.core_ui.common.DefaultPreviews +import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel.SetupState + +@DefaultPreviews +@Composable +private fun RecoveryScreenPreview() { + RecoveryScreen( + onBackClicked = {}, + onNextClicked = {}, + onActionDoneClicked = {}, + onScanQrClicked = {}, + state = SetupState.Idle, + onEnterMyVaultClicked = {}, + onDebugAccountTraceClicked = {}, + onRetryMigrationClicked = {}, + onStartMigrationClicked = {} + ) +} + +@DefaultPreviews +@Composable +private fun RecoveryScreenLoadingPreview() { + RecoveryScreen( + onBackClicked = {}, + onNextClicked = {}, + onActionDoneClicked = {}, + onScanQrClicked = {}, + state = SetupState.InProgress, + onEnterMyVaultClicked = {}, + onDebugAccountTraceClicked = {}, + onRetryMigrationClicked = {}, + onStartMigrationClicked = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt index 5490c29e48..bdd4b5f78b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/splash/SplashFragment.kt @@ -32,6 +32,7 @@ import com.anytypeio.anytype.ui.onboarding.OnboardingFragment import com.anytypeio.anytype.ui.sets.ObjectSetFragment import com.anytypeio.anytype.ui.update.MigrationFailedScreen import com.anytypeio.anytype.ui.update.MigrationInProgressScreen +import com.anytypeio.anytype.ui.update.MigrationStartScreen import com.anytypeio.anytype.ui.vault.VaultFragment import javax.inject.Inject import kotlinx.coroutines.launch @@ -81,13 +82,21 @@ class SplashFragment : BaseFragment(R.layout.fragment_spl } is SplashViewModel.State.Migration -> { binding.compose.setContent { - if (state is SplashViewModel.State.Migration.InProgress) { - MigrationInProgressScreen() - } else if (state is SplashViewModel.State.Migration.Failed) { - MigrationFailedScreen( - state = state.state, - onRetryClicked = vm::onRetryMigrationClicked - ) + when(state) { + is SplashViewModel.State.Migration.AwaitingStart -> { + MigrationStartScreen( + onStartUpdate = vm::onStartMigrationClicked + ) + } + is SplashViewModel.State.Migration.InProgress -> { + MigrationInProgressScreen() + } + is SplashViewModel.State.Migration.Failed -> { + MigrationFailedScreen( + state = state.state, + onRetryClicked = vm::onRetryMigrationClicked + ) + } } } binding.compose.visible() diff --git a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt index aa3d6d4407..ef26c0da96 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/update/MigrationErrorScreen.kt @@ -9,9 +9,16 @@ 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.lazy.LazyColumn import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -23,13 +30,174 @@ import com.anytypeio.anytype.R import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.foundation.AlertConfig import com.anytypeio.anytype.core_ui.foundation.AlertIcon +import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.ButtonPrimary +import com.anytypeio.anytype.core_ui.views.ButtonSecondary import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.HeadlineHeading +import com.anytypeio.anytype.core_ui.views.HeadlineSubheading +import com.anytypeio.anytype.core_ui.views.HeadlineTitle import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate + +@Composable +fun MigrationStartScreen( + onStartUpdate: () -> Unit +) { + var showReadMoreView by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .fillMaxSize() + .background(color = colorResource(R.color.background_primary)) + , + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .align(Alignment.Center) + ) { + // TODO add icon + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.migration_screen_new_version_update), + style = HeadlineTitle, + color = colorResource(R.color.text_primary), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.migration_screen_description_1), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.migration_screen_description_2), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .align(Alignment.BottomCenter) + ) { + ButtonPrimary( + modifier = Modifier.fillMaxWidth(), + onClick = onStartUpdate, + text = stringResource(R.string.migration_screen_start_update), + size = ButtonSize.Large + ) + Spacer(modifier = Modifier.height(12.dp)) + ButtonSecondary( + modifier = Modifier.fillMaxWidth(), + onClick = { showReadMoreView = true }, + text = stringResource(R.string.migration_screen_read_more), + size = ButtonSize.Large + ) + Spacer(modifier = Modifier.height(32.dp)) + } + } + + if (showReadMoreView) { + MigrationReadMoreBottomSheet( + onDismissRequest = { + showReadMoreView = false + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MigrationReadMoreBottomSheet( + onDismissRequest: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + dragHandle = { + Dragger( + modifier = Modifier.padding(vertical = 6.dp) + ) + }, + containerColor = colorResource(R.color.background_secondary), + content = { + MigrationReadMoreScreenContent() + } + ) +} + +@Composable +fun MigrationReadMoreScreenContent() { + LazyColumn( + modifier = Modifier.fillMaxWidth().padding( + horizontal = 16.dp + ) + ) { + item { + Spacer(modifier = Modifier.height(44.dp)) + // TODO add icon + Text( + text = stringResource(R.string.migration_screen_what_to_expect), + style = HeadlineSubheading, + color = colorResource(R.color.text_primary) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.migration_screen_what_to_expect_description), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary) + ) + } + item { + Spacer(modifier = Modifier.height(32.dp)) + // TODO add icon + Text( + text = stringResource(R.string.migration_screen_your_data_remains_safe), + style = HeadlineSubheading, + color = colorResource(R.color.text_primary) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.migration_screen_your_data_description), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.migration_screen_your_data_description_2), + style = BodyCalloutRegular, + color = colorResource(R.color.text_secondary) + ) + Spacer(modifier = Modifier.height(44.dp)) + } + } +} + +@DefaultPreviews +@Composable +fun MigrationReadMoreScreenPreview() { + MigrationReadMoreScreenContent() +} + +@DefaultPreviews +@Composable +fun MigrationStartScreenPreview() { + MigrationStartScreen( + onStartUpdate = {} + ) +} + @Composable fun MigrationInProgressScreen() { Box( @@ -115,7 +283,9 @@ fun MigrationFailedScreen( text = description, color = colorResource(R.color.text_secondary), style = BodyCalloutRegular, - modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(), + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), textAlign = TextAlign.Center ) } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index a2d3c81b86..919d541eb2 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1985,6 +1985,16 @@ Please provide specific details of your needs here. Delete from space Unlink from type I have read and want to delete this space + New Version Update + We\'re laying the groundwork for our new chats. Including counters, notifications and other features needed for smooth chat experience. + It might take a little while, but don\'t worry, your data is safe. + Start Update + Read More + What to Expect + Your Data Remains Safe + During this update, your data remains fully secure. The update is performed directly on your device, and your synced data remains unaffected. We’ll just copy it to a new format, and a local backup will be created on your device, containing all your data in the previous format. + The reason we’re retaining this backup is to debug and assist you, in case of unforeseen. + You\'ll see a loading screen during the update. Once finished, you can continue using the app normally. Change icon Remove diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt index f7ac50a874..3052cf5fea 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/onboarding/login/OnboardingMnemonicLoginViewModel.kt @@ -15,7 +15,6 @@ import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationExcep import com.anytypeio.anytype.core_utils.ext.cancel import com.anytypeio.anytype.domain.auth.interactor.ConvertWallet import com.anytypeio.anytype.domain.auth.interactor.Logout -import com.anytypeio.anytype.domain.auth.interactor.MigrateAccount import com.anytypeio.anytype.domain.auth.interactor.ObserveAccounts import com.anytypeio.anytype.domain.auth.interactor.RecoverWallet import com.anytypeio.anytype.domain.auth.interactor.SaveMnemonic @@ -32,7 +31,6 @@ import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate import com.anytypeio.anytype.presentation.extension.proceedWithAccountEvent import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingLoginEvent -import com.anytypeio.anytype.presentation.splash.SplashViewModel.State import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider import javax.inject.Inject import kotlinx.coroutines.Job @@ -272,7 +270,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor( state.value = SetupState.Failed when (e) { is AccountMigrationNeededException -> { - proceedWithAccountMigration(id) + state.value = SetupState.Migration.AwaitingStart(account = id) } is AccountIsDeletedException -> { sideEffects.emit(value = SideEffect.Error.AccountDeletedError) @@ -404,6 +402,14 @@ class OnboardingMnemonicLoginViewModel @Inject constructor( } } + fun onStartMigrationClicked(account: Id) { + viewModelScope.launch { + if (state.value is SetupState.Migration.AwaitingStart) { + proceedWithAccountMigration(account) + } + } + } + override fun onCleared() { super.onCleared() goroutinesJob?.cancel() @@ -428,6 +434,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor( data object Abort: SetupState() sealed class Migration : SetupState() { abstract val account: Id + data class AwaitingStart(override val account: Id) : Migration() data class InProgress(override val account: Id): Migration() data class Failed( val state: MigrationHelperDelegate.State.Failed, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt index 485d07fb4e..d6703ab0a0 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/splash/SplashViewModel.kt @@ -90,6 +90,14 @@ class SplashViewModel( } } + fun onStartMigrationClicked() { + viewModelScope.launch { + if (state.value is State.Migration.AwaitingStart) { + proceedWithAccountMigration() + } + } + } + fun onRetryMigrationClicked() { viewModelScope.launch { migrationRetryCount = migrationRetryCount + 1 @@ -167,7 +175,6 @@ class SplashViewModel( state.value = State.Loading launchAccount(BaseUseCase.None).proceed( success = { analyticsId -> - state.value = State.Loading crashReporter.setUser(analyticsId) updateUserProps(analyticsId) val props = Props.empty() @@ -179,7 +186,7 @@ class SplashViewModel( Timber.e(e, "Error while launching account") when (e) { is AccountMigrationNeededException -> { - proceedWithAccountMigration() + state.value = State.Migration.AwaitingStart } is NeedToUpdateApplicationException -> { state.value = State.Error(ERROR_NEED_UPDATE) @@ -437,6 +444,7 @@ class SplashViewModel( data object Success: State() data class Error(val msg: String): State() sealed class Migration : State() { + data object AwaitingStart: Migration() data object InProgress: Migration() data class Failed(val state: MigrationHelperDelegate.State.Failed) : Migration() }