1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3317 Account | Tech | Space store migration + UI (#2074)

This commit is contained in:
Evgenii Kozlov 2025-02-14 11:17:02 +01:00 committed by GitHub
parent 20a95fbaea
commit 73cd7aba94
Signed by: github
GPG key ID: B5690EEEBB952194
38 changed files with 643 additions and 572 deletions

View file

@ -96,7 +96,6 @@ import com.anytypeio.anytype.di.feature.templates.DaggerTemplateSelectComponent
import com.anytypeio.anytype.di.feature.types.DaggerCreateObjectTypeComponent
import com.anytypeio.anytype.di.feature.types.DaggerTypeEditComponent
import com.anytypeio.anytype.di.feature.types.DaggerTypeIconPickComponent
import com.anytypeio.anytype.di.feature.update.DaggerMigrationErrorComponent
import com.anytypeio.anytype.di.feature.vault.DaggerVaultComponent
import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectModule
import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetSourceComponent
@ -834,12 +833,6 @@ class ComponentManager(
.create(findComponentDependencies())
}
val migrationErrorComponent = Component {
DaggerMigrationErrorComponent
.factory()
.create(findComponentDependencies())
}
val onboardingComponent = Component {
DaggerOnboardingComponent
.factory()

View file

@ -33,6 +33,7 @@ import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.splash.SplashViewModelFactory
import com.anytypeio.anytype.ui.splash.SplashFragment
import dagger.Binds
@ -178,6 +179,12 @@ object SplashModule {
@PerScreen
@Binds
fun bindViewModelFactory(factory: SplashViewModelFactory): ViewModelProvider.Factory
@Binds
@PerScreen
fun bindMigrationHelperDelegate(
impl: MigrationHelperDelegate.Impl
): MigrationHelperDelegate
}
}

View file

@ -22,6 +22,7 @@ import com.anytypeio.anytype.domain.platform.InitialParamsProvider
import com.anytypeio.anytype.domain.spaces.SpaceDeletedStatusWatcher
import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
@ -74,6 +75,12 @@ object OnboardingMnemonicLoginModule {
defaultProvider: DefaultUriFileProvider
): UriFileProvider
@Binds
@PerScreen
fun bindMigrationHelperDelegate(
impl: MigrationHelperDelegate.Impl
): MigrationHelperDelegate
@Binds
@PerScreen
fun bindViewModelFactory(

View file

@ -1,48 +0,0 @@
package com.anytypeio.anytype.di.feature.update
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.presentation.update.MigrationErrorViewModel
import com.anytypeio.anytype.ui.update.MigrationErrorFragment
import dagger.Binds
import dagger.Component
import dagger.Module
@Component(
dependencies = [MigrationErrorDependencies::class],
modules = [
MigrationErrorModule::class,
MigrationErrorModule.Declarations::class
]
)
@PerScreen
interface MigrationErrorComponent {
@Component.Factory
interface Factory {
fun create(dependencies: MigrationErrorDependencies): MigrationErrorComponent
}
fun inject(fragment: MigrationErrorFragment)
}
@Module
object MigrationErrorModule {
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(
factory: MigrationErrorViewModel.Factory
): ViewModelProvider.Factory
}
}
interface MigrationErrorDependencies : ComponentDependencies {
fun analytics(): Analytics
}

View file

@ -20,9 +20,9 @@ import com.anytypeio.anytype.di.feature.ObjectTypeChangeSubComponent
import com.anytypeio.anytype.di.feature.PersonalizationSettingsSubComponent
import com.anytypeio.anytype.di.feature.SplashDependencies
import com.anytypeio.anytype.di.feature.auth.DeletedAccountDependencies
import com.anytypeio.anytype.di.feature.chats.ChatComponentDependencies
import com.anytypeio.anytype.di.feature.chats.ChatReactionDependencies
import com.anytypeio.anytype.di.feature.chats.SelectChatReactionDependencies
import com.anytypeio.anytype.di.feature.chats.ChatComponentDependencies
import com.anytypeio.anytype.di.feature.gallery.GalleryInstallationComponentDependencies
import com.anytypeio.anytype.di.feature.home.HomeScreenDependencies
import com.anytypeio.anytype.di.feature.membership.MembershipComponentDependencies
@ -57,7 +57,6 @@ import com.anytypeio.anytype.di.feature.templates.TemplateSelectDependencies
import com.anytypeio.anytype.di.feature.types.CreateObjectTypeDependencies
import com.anytypeio.anytype.di.feature.types.TypeEditDependencies
import com.anytypeio.anytype.di.feature.types.TypeIconPickDependencies
import com.anytypeio.anytype.di.feature.update.MigrationErrorDependencies
import com.anytypeio.anytype.di.feature.vault.VaultComponentDependencies
import com.anytypeio.anytype.di.feature.wallpaper.WallpaperSelectSubComponent
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetSourceDependencies
@ -104,7 +103,6 @@ interface MainComponent :
RelationEditDependencies,
SplashDependencies,
DeletedAccountDependencies,
MigrationErrorDependencies,
BacklinkOrAddToObjectDependencies,
FilesStorageDependencies,
OnboardingDependencies,
@ -218,11 +216,6 @@ abstract class ComponentDependenciesModule {
@ComponentDependenciesKey(DeletedAccountDependencies::class)
abstract fun provideDeletedAccountDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(MigrationErrorDependencies::class)
abstract fun migrationErrorDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(BacklinkOrAddToObjectDependencies::class)

View file

@ -258,18 +258,6 @@ class Navigator : AppNavigation {
navController?.navigate(R.id.actionLogout)
}
override fun migrationErrorScreen() {
navController?.navigate(R.id.migrationNeededScreen)
}
override fun exitFromMigrationScreen() {
navController?.navigate(R.id.onboarding_nav, null, navOptions {
popUpTo(R.id.migrationNeededScreen) {
inclusive = true
}
})
}
override fun openRemoteFilesManageScreen(subscription: Id, space: Id) {
navController?.navigate(
resId = R.id.remoteStorageFragment,

View file

@ -11,7 +11,6 @@ class NavigationRouter(
Timber.d("Navigate to $command")
try {
when (command) {
is AppNavigation.Command.ExitFromMigrationScreen -> navigation.exitFromMigrationScreen()
is AppNavigation.Command.OpenSettings -> navigation.openSpaceSettings()
is AppNavigation.Command.OpenObject -> navigation.openDocument(
target = command.target,
@ -58,7 +57,6 @@ class NavigationRouter(
is AppNavigation.Command.OpenTemplates -> navigation.openTemplatesModal(
typeId = command.typeId
)
is AppNavigation.Command.MigrationErrorScreen -> navigation.migrationErrorScreen()
is AppNavigation.Command.OpenDateObject -> navigation.openDateObject(
objectId = command.objectId,
space = command.space

View file

@ -427,21 +427,6 @@ class OnboardingFragment : Fragment() {
Timber.e(it, "Error while trying to open vault screen from onboarding")
}
}
OnboardingMnemonicLoginViewModel.Command.NavigateToMigrationErrorScreen -> {
runCatching {
findNavController().navigate(
R.id.migrationNeededScreen,
null,
navOptions {
popUpTo(R.id.onboarding_nav) {
inclusive = false
}
}
)
}.onFailure {
Timber.e(it, "Error while trying to open migration screen from onboarding")
}
}
is OnboardingMnemonicLoginViewModel.Command.ShareDebugGoroutines -> {
try {
this@OnboardingFragment.shareFirstFileFromPath(command.path, command.uriFileProvider)

View file

@ -41,6 +41,7 @@ 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
@ -54,6 +55,8 @@ import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel
import com.anytypeio.anytype.presentation.onboarding.login.OnboardingMnemonicLoginViewModel.SetupState
import com.anytypeio.anytype.ui.onboarding.OnboardingMnemonicInput
import com.anytypeio.anytype.ui.update.MigrationFailedScreen
import com.anytypeio.anytype.ui.update.MigrationInProgressScreen
import kotlin.Unit
@Composable
@ -67,11 +70,12 @@ fun RecoveryScreenWrapper(
onNextClicked = vm::onLoginClicked,
onActionDoneClicked = vm::onActionDone,
onScanQrClicked = onScanQrClick,
isLoading = vm.state.collectAsState().value is SetupState.InProgress,
state = vm.state.collectAsState().value,
onEnterMyVaultClicked = vm::onEnterMyVaultClicked,
onDebugAccountTraceClicked = {
vm.onAccountThraceButtonClicked()
}
},
onRetryMigrationClicked = vm::onRetryMigrationClicked
)
}
@ -81,9 +85,10 @@ fun RecoveryScreen(
onNextClicked: (Mnemonic) -> Unit,
onActionDoneClicked: (Mnemonic) -> Unit,
onScanQrClicked: () -> Unit,
isLoading: Boolean,
state: SetupState,
onEnterMyVaultClicked: () -> Unit,
onDebugAccountTraceClicked: () -> Unit
onDebugAccountTraceClicked: () -> Unit,
onRetryMigrationClicked: (Id) -> Unit
) {
val focus = LocalFocusManager.current
val context = LocalContext.current
@ -186,7 +191,7 @@ fun RecoveryScreen(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp),
isLoading = isLoading
isLoading = state is SetupState.InProgress
)
}
item {
@ -207,7 +212,7 @@ fun RecoveryScreen(
onClick = {
onScanQrClicked.invoke()
},
enabled = !isLoading,
enabled = state !is SetupState.InProgress,
disabledBackgroundColor = Color.Transparent,
size = ButtonSize.Large,
modifier = Modifier
@ -218,6 +223,16 @@ fun RecoveryScreen(
}
}
)
if (state is SetupState.Migration.InProgress) {
MigrationInProgressScreen()
} else if(state is SetupState.Migration.Failed) {
MigrationFailedScreen(
state = state.state,
onRetryClicked = {
onRetryMigrationClicked(state.account)
}
)
}
}
}
@ -267,9 +282,10 @@ fun RecoveryScreenPreview() {
onNextClicked = {},
onActionDoneClicked = {},
onScanQrClicked = {},
isLoading = false,
state = SetupState.Idle,
onEnterMyVaultClicked = {},
onDebugAccountTraceClicked = {}
onDebugAccountTraceClicked = {},
onRetryMigrationClicked = {}
)
}
@ -282,8 +298,9 @@ fun RecoveryScreenLoadingPreview() {
onNextClicked = {},
onActionDoneClicked = {},
onScanQrClicked = {},
isLoading = true,
state = SetupState.InProgress,
onEnterMyVaultClicked = {},
onDebugAccountTraceClicked = {}
onDebugAccountTraceClicked = {},
onRetryMigrationClicked = {}
)
}

View file

@ -18,7 +18,6 @@ import com.anytypeio.anytype.core_utils.ext.orNull
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.databinding.FragmentSplashBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.other.DefaultDeepLinkResolver
@ -31,6 +30,8 @@ import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.home.HomeScreenFragment
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.vault.VaultFragment
import javax.inject.Inject
import kotlinx.coroutines.launch
@ -64,31 +65,37 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
launch {
vm.state.collect { state ->
when(state) {
is ViewState.Error -> {
binding.error.text = state.error
is SplashViewModel.State.Init -> {
binding.error.gone()
binding.compose.visibility = View.GONE
}
is SplashViewModel.State.Error -> {
binding.error.text = state.msg
binding.error.visible()
}
else -> {
binding.error.gone()
binding.error.text = ""
}
}
}
}
launch {
vm.loadingState.collect { isLoading ->
when (isLoading) {
true -> {
binding.loadingContainer.setContent {
is SplashViewModel.State.Loading -> {
binding.compose.setContent {
PulsatingCircleScreen()
}
binding.logo.visibility = View.GONE
binding.loadingContainer.visibility = View.VISIBLE
binding.compose.visible()
}
false -> {
binding.logo.visibility = View.GONE
binding.loadingContainer.visibility = View.GONE
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
)
}
}
binding.compose.visible()
}
is SplashViewModel.State.Success -> {
binding.compose.gone()
binding.error.gone()
binding.error.text = ""
}
}
}
@ -271,11 +278,6 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
args = OnboardingFragment.args(deepLink)
)
}
is SplashViewModel.Command.NavigateToMigration -> {
findNavController().navigate(
R.id.migrationNeededScreen
)
}
is SplashViewModel.Command.CheckAppStartIntent -> {
val intent = activity?.intent
if (intent != null && (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND)) {

View file

@ -1,82 +0,0 @@
package com.anytypeio.anytype.ui.update
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.update.MigrationErrorViewModel
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
class MigrationErrorFragment : BaseComposeFragment() {
@Inject
lateinit var factory: MigrationErrorViewModel.Factory
private val vm by viewModels<MigrationErrorViewModel> { factory }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = ComposeView(
context = requireContext()
).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
MigrationErrorScreen(vm::onAction)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect { command ->
when(command) {
is MigrationErrorViewModel.Command.Browse -> {
browseUrl(command)
}
is MigrationErrorViewModel.Command.Exit -> {
navigation().exitFromMigrationScreen()
}
}
}
}
}
}
private fun browseUrl(command: MigrationErrorViewModel.Command.Browse) {
try {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(command.url)
}.let(::startActivity)
} catch (e: Exception) {
Timber.e(e, "Error while browsing url")
}
}
override fun injectDependencies() {
componentManager().migrationErrorComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().migrationErrorComponent.release()
}
}

View file

@ -1,219 +1,159 @@
package com.anytypeio.anytype.ui.update
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Card
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyCallout
import com.anytypeio.anytype.core_ui.views.BodyRegular
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.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.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.HeadlineSubheading
import com.anytypeio.anytype.presentation.update.MigrationErrorViewModel.ViewAction
import kotlinx.coroutines.launch
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
@Composable
fun MigrationErrorScreen(onViewAction: (ViewAction) -> Unit) {
fun MigrationInProgressScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(id = R.color.background_primary)),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
CircularProgressIndicator(
modifier = Modifier
.size(96.dp)
.align(Alignment.CenterHorizontally)
,
backgroundColor = colorResource(R.color.shape_secondary),
color = Color(0xFFFFB522),
strokeWidth = 8.dp
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.migration_migration_is_in_progress),
style = HeadlineHeading,
color = colorResource(R.color.text_primary),
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 44.dp)
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.migration_this_shouldn_t_take_long),
style = BodyCalloutRegular,
color = colorResource(R.color.text_secondary),
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 44.dp)
.fillMaxWidth()
)
}
}
}
@Composable
fun MigrationFailedScreen(
state: MigrationHelperDelegate.State.Failed,
onRetryClicked: () -> Unit
) {
val description = when(state) {
MigrationHelperDelegate.State.Failed.NotEnoughSpace -> {
stringResource(R.string.migration_error_please_free_up_space_and_run_the_process_again)
}
is MigrationHelperDelegate.State.Failed.UnknownError -> {
state.error.message ?: stringResource(R.string.unknown_error)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(id = R.color.background_primary))
) {
Cards(onViewAction)
CloseButton(closeClicks = { onViewAction(ViewAction.CloseScreen) })
BackHandler(enabled = true) { onViewAction(ViewAction.CloseScreen) }
}
}
@Composable
fun Cards(onViewAction: (ViewAction) -> Unit) {
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
Text(
text = stringResource(id = R.string.almost_there),
style = HeadlineHeading,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.padding(top = 56.dp)
)
Text(
text = stringResource(id = R.string.almost_there_subtitle),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.padding(top = 12.dp)
)
InfoCard(
modifier = Modifier.padding(top = 32.dp),
title = stringResource(id = R.string.i_did_not_not_complete_migration),
toggleClick = { onViewAction(ViewAction.ToggleMigrationNotReady) },
expanded = true,
content = {
val hereText = stringResource(id = R.string.here)
val text = buildAnnotatedString {
append(stringResource(id = R.string.update_steps_first))
append(" ")
pushStringAnnotation(
tag = ANNOTATION_TAG,
annotation = hereText
)
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(hereText)
}
pop()
append(stringResource(R.string.update_steps_last))
}
ClickableText(
modifier = Modifier.padding(top = 12.dp),
text = text,
style = BodyCallout.copy(
color = colorResource(id = R.color.text_primary)
),
onClick = { offset ->
text.getStringAnnotations(
tag = ANNOTATION_TAG,
start = offset,
end = offset
).firstOrNull().let {
if (it?.item == hereText) {
onViewAction(ViewAction.DownloadDesktop)
}
}
},
Column(
modifier = Modifier.align(Alignment.Center)
) {
AlertIcon(
icon = AlertConfig.Icon(
gradient = GRADIENT_TYPE_RED,
icon = R.drawable.ic_alert_error
)
},
)
InfoCard(
modifier = Modifier.padding(top = 20.dp),
title = stringResource(id = R.string.i_completed_migration),
expanded = false,
toggleClick = { onViewAction(ViewAction.ToggleMigrationReady) },
content = {
Column {
Text(
modifier = Modifier.padding(top = 12.dp),
text = stringResource(id = R.string.migration_error_msg),
style = BodyCallout,
color = colorResource(id = R.color.text_primary)
)
ButtonPrimary(
text = stringResource(id = R.string.visit_forum),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
onClick = { onViewAction(ViewAction.VisitForum) },
size = ButtonSize.Large
)
}
}
)
}
}
@Composable
fun InfoCard(
modifier: Modifier = Modifier,
title: String,
expanded: Boolean,
toggleClick: () -> Unit,
content: @Composable AnimatedVisibilityScope.() -> Unit
) {
val cardOpened = remember { mutableStateOf(expanded) }
val rotationDegree = remember {
Animatable(
if (expanded) ROTATION_CLOSED else ROTATION_OPENED
)
}
val coroutineScope = rememberCoroutineScope()
Card(
modifier = modifier,
backgroundColor = colorResource(id = R.color.shape_transparent),
elevation = 0.dp,
shape = RoundedCornerShape(16.dp)
) {
Box {
Image(
painter = painterResource(id = R.drawable.icon_migration_card_arrow),
contentDescription = "",
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 22.dp, end = 12.dp)
.rotate(rotationDegree.value)
.noRippleClickable {
cardOpened.value = !cardOpened.value
coroutineScope.launch {
if (cardOpened.value) {
toggleClick()
rotationDegree.animateTo(ROTATION_CLOSED)
} else {
rotationDegree.animateTo(ROTATION_OPENED)
}
}
}
)
Column(
Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.migration_migration_failed),
style = HeadlineHeading,
color = colorResource(R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
if (description.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
style = HeadlineSubheading,
color = colorResource(id = R.color.text_primary)
text = description,
color = colorResource(R.color.text_secondary),
style = BodyCalloutRegular,
modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
textAlign = TextAlign.Center
)
AnimatedVisibility(visible = cardOpened.value) {
content()
}
}
}
ButtonPrimary(
modifier = Modifier
.padding(20.dp)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
text = stringResource(R.string.migration_error_try_again),
size = ButtonSize.Large,
onClick = onRetryClicked
)
}
}
private const val ANNOTATION_TAG = "here_text_tag"
private const val ROTATION_OPENED = 0F
private const val ROTATION_CLOSED = 180F
@DefaultPreviews
@Composable
private fun CloseButton(closeClicks: () -> Unit) {
Box(modifier = Modifier.fillMaxSize()) {
Image(painter = painterResource(id = R.drawable.ic_navigation_close),
contentDescription = "close image",
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 12.dp, end = 12.dp)
.noRippleClickable { closeClicks.invoke() }
)
}
fun MigrationInProgressScreenPreview() {
MigrationInProgressScreen()
}
@DefaultPreviews
@Composable
fun MigrationFailedScreenPreview() {
MigrationFailedScreen(
state = MigrationHelperDelegate.State.Failed.NotEnoughSpace,
onRetryClicked = {}
)
}
@DefaultPreviews
@Composable
fun MigrationFailedGenericScreenPreview() {
MigrationFailedScreen(
state = MigrationHelperDelegate.State.Failed.UnknownError(
Exception(stringResource(R.string.default_text_placeholder))
),
onRetryClicked = {}
)
}

View file

@ -5,13 +5,6 @@
android:layout_height="match_parent"
android:background="@color/background_primary">
<ImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_logo" />
<TextView
android:id="@+id/version"
style="@style/TextView.UXStyle.Body"
@ -41,10 +34,9 @@
tools:visibility="visible" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/loadingContainer"
android:id="@+id/compose"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center"/>
android:visibility="gone" />
</FrameLayout>

View file

@ -631,12 +631,6 @@
android:label="TemplateSelectScreen"
tools:layout="@layout/fragment_template_select" />
<fragment
android:id="@+id/migrationNeededScreen"
android:name="com.anytypeio.anytype.ui.update.MigrationErrorFragment"
android:label="Migration-needed screen">
</fragment>
<dialog
android:id="@+id/shareSpaceScreen"
android:name="com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment"/>

View file

@ -1,5 +1,9 @@
package com.anytypeio.anytype.core_models.exceptions
class AccountIsDeletedException : Exception()
class MigrationNeededException: Exception()
class NeedToUpdateApplicationException: Exception()
class NeedToUpdateApplicationException: Exception()
class AccountMigrationNeededException: Exception()
sealed class MigrationFailedException : Exception() {
class NotEnoughSpace : MigrationFailedException()
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.data.auth.repo
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.data.auth.model.AccountEntity
import com.anytypeio.anytype.data.auth.model.WalletEntity
@ -93,4 +94,15 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
override suspend fun debugExportLogs(dir: String): String {
throw UnsupportedOperationException()
}
override suspend fun migrateAccount(
account: Id,
path: String
) {
throw UnsupportedOperationException()
}
override suspend fun cancelAccountMigration(account: Id) {
throw UnsupportedOperationException()
}
}

View file

@ -4,6 +4,7 @@ import com.anytypeio.anytype.core_models.Account
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.data.auth.mapper.toDomain
import com.anytypeio.anytype.data.auth.mapper.toEntity
@ -93,6 +94,22 @@ class AuthDataRepository(
override suspend fun getVersion(): String = factory.remote.getVersion()
override suspend fun migrateAccount(
account: Id,
path: String
) {
factory.remote.migrateAccount(
account = account,
path = path
)
}
override suspend fun cancelAccountMigration(account: Id) {
factory.remote.cancelAccountMigration(
account = account
)
}
override suspend fun getNetworkMode(): NetworkModeConfig {
return factory.cache.getNetworkMode()
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.data.auth.repo
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.data.auth.model.AccountEntity
import com.anytypeio.anytype.data.auth.model.WalletEntity
@ -16,6 +17,9 @@ interface AuthDataStore {
suspend fun deleteAccount() : AccountStatus
suspend fun restoreAccount() : AccountStatus
suspend fun migrateAccount(account: Id, path: String)
suspend fun cancelAccountMigration(account: Id)
suspend fun recoverAccount()
suspend fun saveAccount(account: AccountEntity)

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.data.auth.repo
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.data.auth.model.AccountEntity
import com.anytypeio.anytype.data.auth.model.WalletEntity
import kotlinx.coroutines.flow.Flow
@ -10,6 +11,10 @@ import kotlinx.coroutines.flow.Flow
interface AuthRemote {
suspend fun selectAccount(command: Command.AccountSelect): AccountSetup
suspend fun createAccount(command: Command.AccountCreate): AccountSetup
suspend fun migrateAccount(account: Id, path: String)
suspend fun cancelAccountMigration(account: Id)
suspend fun deleteAccount() : AccountStatus
suspend fun restoreAccount() : AccountStatus
suspend fun recoverAccount()

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.data.auth.repo
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.data.auth.model.AccountEntity
import com.anytypeio.anytype.data.auth.model.WalletEntity
@ -23,6 +24,22 @@ class AuthRemoteDataStore(
override suspend fun restoreAccount(): AccountStatus = authRemote.restoreAccount()
override suspend fun migrateAccount(
account: Id,
path: String
) {
authRemote.migrateAccount(
account = account,
path = path
)
}
override suspend fun cancelAccountMigration(account: Id) {
authRemote.cancelAccountMigration(
account = account
)
}
override suspend fun recoverAccount() {
authRemote.recoverAccount()
}

View file

@ -0,0 +1,34 @@
package com.anytypeio.anytype.domain.auth.interactor
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import javax.inject.Inject
class CancelAccountMigration @Inject constructor(
private val repo: AuthRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<CancelAccountMigration.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
when(params) {
is Params.Current -> {
val acc = repo.getCurrentAccount()
repo.cancelAccountMigration(
account = acc.id
)
}
is Params.Other -> {
repo.cancelAccountMigration(
account = params.acc
)
}
}
}
sealed class Params {
data object Current : Params()
data class Other(val acc: Id) : Params()
}
}

View file

@ -0,0 +1,40 @@
package com.anytypeio.anytype.domain.auth.interactor
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.device.PathProvider
import javax.inject.Inject
class MigrateAccount @Inject constructor(
private val repo: AuthRepository,
private val path: PathProvider,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<MigrateAccount.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
when(params) {
is Params.Current -> {
val acc = repo.getCurrentAccount()
val path = path.providePath()
repo.migrateAccount(
account = acc.id,
path = path
)
}
is Params.Other -> {
val path = path.providePath()
repo.migrateAccount(
account = params.acc,
path = path
)
}
}
}
sealed class Params {
data object Current : Params()
data class Other(val acc: Id) : Params()
}
}

View file

@ -4,6 +4,7 @@ import com.anytypeio.anytype.core_models.Account
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.domain.auth.model.Wallet
import kotlinx.coroutines.flow.Flow
@ -20,6 +21,9 @@ interface AuthRepository {
suspend fun selectAccount(command: Command.AccountSelect): AccountSetup
suspend fun createAccount(command: Command.AccountCreate): AccountSetup
suspend fun migrateAccount(account: Id, path: String)
suspend fun cancelAccountMigration(account: Id)
suspend fun deleteAccount() : AccountStatus
suspend fun restoreAccount() : AccountStatus

View file

@ -1,5 +1,5 @@
[versions]
middlewareVersion = "v0.39.10"
middlewareVersion = "v0.40.0-alpha01"
kotlinVersion = '2.0.21'
kspVersion = "2.0.21-1.0.25"

View file

@ -1876,5 +1876,10 @@ Please provide specific details of your needs here.</string>
<string name="chats_alert_delete_this_message_description">It cannot be restored after confirmation</string>
<string name="chats_alert_delete_this_message">Delete this message?</string>
<string name="migration_migration_is_in_progress">Migration is in progress</string>
<string name="migration_this_shouldn_t_take_long">This shouldnt take long. Thanks for your patience.</string>
<string name="migration_error_try_again">Try again</string>
<string name="migration_migration_failed">Migration failed</string>
<string name="migration_error_please_free_up_space_and_run_the_process_again">Please free up space and run the process again.</string>
</resources>

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.middleware.auth
import com.anytypeio.anytype.core_models.AccountSetup
import com.anytypeio.anytype.core_models.AccountStatus
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.data.auth.model.WalletEntity
import com.anytypeio.anytype.data.auth.repo.AuthRemote
import com.anytypeio.anytype.middleware.EventProxy
@ -28,6 +29,22 @@ class AuthMiddleware(
command: Command.AccountCreate
) : AccountSetup = middleware.accountCreate(command)
override suspend fun migrateAccount(
account: Id,
path: String
) {
middleware.accountMigrate(
account = account,
path = path
)
}
override suspend fun cancelAccountMigration(account: Id) {
middleware.accountMigrateCancel(
account = account
)
}
override suspend fun deleteAccount(): AccountStatus = middleware.accountDelete()
override suspend fun restoreAccount(): AccountStatus = middleware.accountRestore()

View file

@ -129,6 +129,25 @@ class Middleware @Inject constructor(
return status.core()
}
@Throws(Exception::class)
fun accountMigrate(account: Id, path: String) {
val request = Rpc.Account.Migrate.Request(
id = account,
rootPath = path
)
logRequestIfDebug(request)
val (response, time) = measureTimedValue { service.accountMigrate(request) }
logResponseIfDebug(response, time)
}
@Throws(Exception::class)
fun accountMigrateCancel(account: Id) {
val request = Rpc.Account.MigrateCancel.Request(id = account)
logRequestIfDebug(request)
val (response, time) = measureTimedValue { service.accountMigrateCancel(request) }
logResponseIfDebug(response, time)
}
@Throws(Exception::class)
fun accountRecover() {
val request = Rpc.Account.Recover.Request()

View file

@ -50,6 +50,12 @@ interface MiddlewareService {
@Throws(Exception::class)
fun accountStop(request: Rpc.Account.Stop.Request): Rpc.Account.Stop.Response
@Throws(Exception::class)
fun accountMigrate(request: Rpc.Account.Migrate.Request): Rpc.Account.Migrate.Response
@Throws(Exception::class)
fun accountMigrateCancel(request: Rpc.Account.MigrateCancel.Request): Rpc.Account.MigrateCancel.Response
//endregion
//region OBJECT commands

View file

@ -2,7 +2,9 @@ package com.anytypeio.anytype.middleware.service
import anytype.Rpc
import com.anytypeio.anytype.core_models.exceptions.AccountIsDeletedException
import com.anytypeio.anytype.core_models.exceptions.AccountMigrationNeededException
import com.anytypeio.anytype.core_models.exceptions.LoginException
import com.anytypeio.anytype.core_models.exceptions.MigrationFailedException
import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationException
import com.anytypeio.anytype.core_models.multiplayer.MultiplayerError
import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteError
@ -80,6 +82,9 @@ class MiddlewareServiceImplementation @Inject constructor(
Rpc.Account.Select.Response.Error.Code.ACCOUNT_IS_DELETED -> {
throw AccountIsDeletedException()
}
Rpc.Account.Select.Response.Error.Code.ACCOUNT_STORE_NOT_MIGRATED -> {
throw AccountMigrationNeededException()
}
Rpc.Account.Select.Response.Error.Code.FAILED_TO_FETCH_REMOTE_NODE_HAS_INCOMPATIBLE_PROTO_VERSION -> {
throw NeedToUpdateApplicationException()
}
@ -104,6 +109,33 @@ class MiddlewareServiceImplementation @Inject constructor(
}
}
override fun accountMigrate(request: Rpc.Account.Migrate.Request): Rpc.Account.Migrate.Response {
val encoded = Service.accountMigrate(Rpc.Account.Migrate.Request.ADAPTER.encode(request))
val response = Rpc.Account.Migrate.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Account.Migrate.Response.Error.Code.NULL) {
when(error.code) {
Rpc.Account.Migrate.Response.Error.Code.NOT_ENOUGH_FREE_SPACE -> {
throw MigrationFailedException.NotEnoughSpace()
}
else -> throw Exception(error.description)
}
} else {
return response
}
}
override fun accountMigrateCancel(request: Rpc.Account.MigrateCancel.Request): Rpc.Account.MigrateCancel.Response {
val encoded = Service.accountMigrateCancel(Rpc.Account.MigrateCancel.Request.ADAPTER.encode(request))
val response = Rpc.Account.MigrateCancel.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Account.MigrateCancel.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun blockBookmarkCreateAndFetch(request: Rpc.BlockBookmark.CreateAndFetch.Request): Rpc.BlockBookmark.CreateAndFetch.Response {
val encoded = Service.blockBookmarkCreateAndFetch(
Rpc.BlockBookmark.CreateAndFetch.Request.ADAPTER.encode(request)

View file

@ -0,0 +1,52 @@
package com.anytypeio.anytype.presentation.auth.account
import com.anytypeio.anytype.core_models.exceptions.MigrationFailedException
import com.anytypeio.anytype.domain.auth.interactor.MigrateAccount
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.Resultat
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
interface MigrationHelperDelegate {
suspend fun proceedWithMigration() : Flow<State>
class Impl @Inject constructor(
private val migrateAccount: MigrateAccount,
private val dispatchers: AppCoroutineDispatchers
) : MigrationHelperDelegate {
override suspend fun proceedWithMigration(): Flow<State> {
return migrateAccount
.stream(MigrateAccount.Params.Current)
.map { result ->
when(result) {
is Resultat.Failure -> {
if (result.exception is MigrationFailedException.NotEnoughSpace) {
State.Failed.NotEnoughSpace
} else {
State.Failed.UnknownError(result.exception)
}
}
is Resultat.Loading -> State.InProgress
is Resultat.Success -> State.Migrated
}
}
.flowOn(dispatchers.io)
}
}
sealed class State {
data object Init: State()
data object InProgress : State()
sealed class Failed : State() {
data class UnknownError(val error: Throwable) : Failed()
data object NotEnoughSpace : Failed()
}
data object Migrated : State()
}
}

View file

@ -7,8 +7,6 @@ import com.anytypeio.anytype.presentation.widgets.collection.Subscription
interface AppNavigation {
fun exitFromMigrationScreen()
fun openSpaceSettings()
fun openObjectSet(
@ -59,8 +57,6 @@ interface AppNavigation {
fun logout()
fun migrationErrorScreen()
fun openTemplatesModal(typeId: Id)
fun openAllContent(space: Id)
@ -75,9 +71,6 @@ interface AppNavigation {
data object ExitToDesktop : Command()
data object ExitToVault : Command()
data object ExitToSpaceHome : Command()
data object ExitFromMigrationScreen : Command()
data class OpenObject(val target: Id, val space: Id) : Command()
data class OpenChat(val target: Id, val space: Id) : Command()
data class LaunchDocument(val target: Id, val space: Id) : Command()
@ -89,7 +82,6 @@ interface AppNavigation {
) : Command()
object OpenSettings : Command()
object MigrationErrorScreen: Command()
data class OpenShareScreen(
val space: SpaceId

View file

@ -7,13 +7,15 @@ import com.anytypeio.anytype.CrashReporter
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.exceptions.AccountIsDeletedException
import com.anytypeio.anytype.core_models.exceptions.AccountMigrationNeededException
import com.anytypeio.anytype.core_models.exceptions.LoginException
import com.anytypeio.anytype.core_models.exceptions.MigrationNeededException
import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationException
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
@ -27,9 +29,10 @@ import com.anytypeio.anytype.domain.debugging.DebugGoroutines
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
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
import com.anytypeio.anytype.presentation.splash.SplashViewModel.State
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import javax.inject.Inject
import kotlinx.coroutines.Job
@ -55,8 +58,9 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private val uriFileProvider: UriFileProvider,
private val logout: Logout,
private val globalSubscriptionManager: GlobalSubscriptionManager,
private val debugAccountSelectTrace: DebugAccountSelectTrace
) : ViewModel() {
private val debugAccountSelectTrace: DebugAccountSelectTrace,
private val migrationDelegate: MigrationHelperDelegate
) : ViewModel(), MigrationHelperDelegate by migrationDelegate {
private val jobs = mutableListOf<Job>()
private var goroutinesJob : Job? = null
@ -67,6 +71,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
val command = MutableSharedFlow<Command>(replay = 0)
private var debugClickCount = 0
private var migrationRetryCount: Int = 0
private val _fiveClicks = MutableStateFlow(false)
init {
@ -177,7 +182,7 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
else -> SideEffect.Error.Unknown("Error while login: ${exception.message}")
}
sideEffects.emit(error).also {
Timber.e(exception, "Error while selecting account")
Timber.e(exception, "Error while recovering wallet")
}
}
}
@ -266,8 +271,8 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
Timber.e(e, "Error while selecting account with id: $id")
state.value = SetupState.Failed
when (e) {
is MigrationNeededException -> {
navigateToMigrationErrorScreen()
is AccountMigrationNeededException -> {
proceedWithAccountMigration(id)
}
is AccountIsDeletedException -> {
sideEffects.emit(value = SideEffect.Error.AccountDeletedError)
@ -306,9 +311,27 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
}
}
private fun navigateToMigrationErrorScreen() {
viewModelScope.launch {
command.emit(Command.NavigateToMigrationErrorScreen)
private suspend fun proceedWithAccountMigration(id: String) {
proceedWithMigration().collect { migrationState ->
when (migrationState) {
is MigrationHelperDelegate.State.Failed -> {
state.value = SetupState.Migration.Failed(
state = migrationState,
account = id
)
}
MigrationHelperDelegate.State.InProgress -> {
state.value = SetupState.Migration.InProgress(
account = id
)
}
MigrationHelperDelegate.State.Migrated -> {
proceedWithSelectingAccount(id)
}
MigrationHelperDelegate.State.Init -> {
// Do nothing.
}
}
}
}
@ -372,6 +395,15 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
}
}
fun onRetryMigrationClicked(account: Id) {
if (state.value !is SetupState.InProgress) {
migrationRetryCount = migrationRetryCount + 1
viewModelScope.launch {
proceedWithAccountMigration(account)
}
}
}
override fun onCleared() {
super.onCleared()
goroutinesJob?.cancel()
@ -394,11 +426,18 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
data object InProgress: SetupState()
data object Failed: SetupState()
data object Abort: SetupState()
sealed class Migration : SetupState() {
abstract val account: Id
data class InProgress(override val account: Id): Migration()
data class Failed(
val state: MigrationHelperDelegate.State.Failed,
override val account: Id
) : Migration()
}
}
sealed class Command {
data object Exit : Command()
data object NavigateToMigrationErrorScreen : Command()
data object NavigateToVaultScreen: Command()
data class ShowToast(val message: String) : Command()
data class ShareDebugGoroutines(val path: String, val uriFileProvider: UriFileProvider) : Command()
@ -420,7 +459,8 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
private val uriFileProvider: UriFileProvider,
private val logout: Logout,
private val globalSubscriptionManager: GlobalSubscriptionManager,
private val debugAccountSelectTrace: DebugAccountSelectTrace
private val debugAccountSelectTrace: DebugAccountSelectTrace,
private val migrationHelperDelegate: MigrationHelperDelegate
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -440,7 +480,8 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
uriFileProvider = uriFileProvider,
logout = logout,
globalSubscriptionManager = globalSubscriptionManager,
debugAccountSelectTrace = debugAccountSelectTrace
debugAccountSelectTrace = debugAccountSelectTrace,
migrationDelegate = migrationHelperDelegate
) as T
}
}

View file

@ -15,7 +15,9 @@ import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds.SET
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds.COLLECTION
import com.anytypeio.anytype.core_models.exceptions.MigrationNeededException
import com.anytypeio.anytype.core_models.SupportedLayouts
import com.anytypeio.anytype.core_models.exceptions.AccountMigrationNeededException
import com.anytypeio.anytype.core_models.exceptions.MigrationFailedException
import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationException
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.primitives.TypeKey
@ -28,24 +30,22 @@ import com.anytypeio.anytype.domain.auth.model.AuthStatus
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.page.CreateObjectByTypeAndTemplate
import com.anytypeio.anytype.domain.spaces.GetLastOpenedSpace
import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
import com.anytypeio.anytype.core_models.SupportedLayouts
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.spaces.GetSpaceView
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.confgs.ChatConfig
import com.anytypeio.anytype.presentation.extension.sendAnalyticsObjectCreateEvent
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import timber.log.Timber
@ -68,13 +68,16 @@ class SplashViewModel(
private val getLastOpenedSpace: GetLastOpenedSpace,
private val createObjectByTypeAndTemplate: CreateObjectByTypeAndTemplate,
private val spaceViews: SpaceViewSubscriptionContainer,
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
val state = MutableStateFlow<ViewState<Any>>(ViewState.Init)
private val migration: MigrationHelperDelegate
) : ViewModel(),
AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate,
MigrationHelperDelegate by migration
{
val state = MutableStateFlow<State>(State.Init)
val commands = MutableSharedFlow<Command>(replay = 0)
val loadingState = MutableStateFlow(false)
private var migrationRetryCount: Int = 0
init {
Timber.i("SplashViewModel, init")
@ -82,12 +85,41 @@ class SplashViewModel(
}
fun onErrorClicked() {
if (BuildConfig.DEBUG && state.value is ViewState.Error) {
state.value = ViewState.Loading
if (state.value is State.Error) {
proceedWithLaunchingAccount()
}
}
fun onRetryMigrationClicked() {
viewModelScope.launch {
migrationRetryCount = migrationRetryCount + 1
proceedWithAccountMigration()
}
}
private suspend fun proceedWithAccountMigration() {
if (migrationRetryCount <= 1) {
proceedWithMigration().collect { migrationState ->
when (migrationState) {
is MigrationHelperDelegate.State.Failed -> {
state.value = State.Migration.Failed(migrationState)
}
is MigrationHelperDelegate.State.Init -> {
// Do nothing.
}
is MigrationHelperDelegate.State.InProgress -> {
state.value = State.Migration.InProgress
}
is MigrationHelperDelegate.State.Migrated -> {
proceedWithLaunchingAccount()
}
}
}
} else {
Timber.e("Failed to migration account after retry")
}
}
private fun checkAuthorizationStatus() {
viewModelScope.launch {
checkAuthorizationStatus(Unit).process(
@ -120,7 +152,7 @@ class SplashViewModel(
failure = { e ->
Timber.e(e, "Error while retrying launching wallet")
val msg = "Error while launching account: ${e.message}"
state.value = ViewState.Error(msg)
state.value = State.Error(msg)
},
success = {
proceedWithLaunchingAccount()
@ -132,10 +164,10 @@ class SplashViewModel(
private fun proceedWithLaunchingAccount() {
val startTime = System.currentTimeMillis()
viewModelScope.launch {
loadingState.value = true
state.value = State.Loading
launchAccount(BaseUseCase.None).proceed(
success = { analyticsId ->
loadingState.value = false
state.value = State.Loading
crashReporter.setUser(analyticsId)
updateUserProps(analyticsId)
val props = Props.empty()
@ -144,18 +176,17 @@ class SplashViewModel(
commands.emit(Command.CheckAppStartIntent)
},
failure = { e ->
loadingState.value = false
Timber.e(e, "Error while launching account")
when (e) {
is MigrationNeededException -> {
commands.emit(Command.NavigateToMigration)
is AccountMigrationNeededException -> {
proceedWithAccountMigration()
}
is NeedToUpdateApplicationException -> {
state.value = ViewState.Error(ERROR_NEED_UPDATE)
state.value = State.Error(ERROR_NEED_UPDATE)
}
else -> {
val msg = "$ERROR_MESSAGE : ${e.message ?: "Unknown error"}"
state.value = ViewState.Error(msg)
state.value = State.Error(msg)
}
}
}
@ -386,7 +417,6 @@ class SplashViewModel(
) : Command()
data class NavigateToVault(val deeplink: String? = null) : Command()
data object NavigateToAuthStart : Command()
data object NavigateToMigration: Command()
data object CheckAppStartIntent : Command()
data class NavigateToObject(val id: Id, val space: Id, val chat: Id?) : Command()
data class NavigateToObjectSet(val id: Id, val space: Id, val chat: Id?) : Command()
@ -399,4 +429,15 @@ class SplashViewModel(
const val ERROR_NEED_UPDATE = "Unable to retrieve account. Please update Anytype to the latest version."
const val ERROR_CREATE_OBJECT = "Error while creating object: object type not found"
}
sealed class State {
data object Init : State()
data object Loading : State()
data object Success: State()
data class Error(val msg: String): State()
sealed class Migration : State() {
data object InProgress: Migration()
data class Failed(val state: MigrationHelperDelegate.State.Failed) : Migration()
}
}
}

View file

@ -16,6 +16,7 @@ import com.anytypeio.anytype.domain.spaces.GetLastOpenedSpace
import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import javax.inject.Inject
/**
@ -36,7 +37,8 @@ class SplashViewModelFactory @Inject constructor(
private val globalSubscriptionManager: GlobalSubscriptionManager,
private val getLastOpenedSpace: GetLastOpenedSpace,
private val createObjectByTypeAndTemplate: CreateObjectByTypeAndTemplate,
private val spaceViews: SpaceViewSubscriptionContainer
private val spaceViews: SpaceViewSubscriptionContainer,
private val migration: MigrationHelperDelegate
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -54,6 +56,7 @@ class SplashViewModelFactory @Inject constructor(
globalSubscriptionManager = globalSubscriptionManager,
getLastOpenedSpace = getLastOpenedSpace,
createObjectByTypeAndTemplate = createObjectByTypeAndTemplate,
spaceViews = spaceViews
spaceViews = spaceViews,
migration = migration
) as T
}

View file

@ -1,114 +0,0 @@
package com.anytypeio.anytype.presentation.update
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.analytics.props.Props
import com.anytypeio.anytype.core_models.Url
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class MigrationErrorViewModel(
val analytics: Analytics
) : ViewModel() {
val commands = MutableSharedFlow<Command>()
private val viewActions = MutableSharedFlow<ViewAction>()
init {
viewModelScope.launch {
viewActions.collect { action ->
when (action) {
ViewAction.CloseScreen -> {
sendAnalyticsEvent(ANALYTICS_TYPE_EXIT)
proceedWithCloseScreen()
}
ViewAction.VisitForum -> {
sendAnalyticsEvent(ANALYTICS_TYPE_CHECK_INSTRUCTIONS)
proceedWithVisitingForum()
}
ViewAction.DownloadDesktop -> {
sendAnalyticsEvent(ANALYTICS_TYPE_DESKTOP_DOWNLOAD)
proceedWithDesktopDownload()
}
ViewAction.ToggleMigrationNotReady -> {
// Do nothing
}
ViewAction.ToggleMigrationReady -> {
sendAnalyticsEvent(ANALYTICS_TYPE_MIGRATION_COMPLETED)
}
}
}
}
}
fun onAction(action: ViewAction) {
viewModelScope.launch {
viewActions.emit(action)
}
}
private fun proceedWithCloseScreen() {
viewModelScope.launch {
commands.emit(Command.Exit)
}
}
private fun proceedWithVisitingForum() {
viewModelScope.launch {
commands.emit(Command.Browse(VISIT_FORUM_URL))
}
}
private fun proceedWithDesktopDownload() {
viewModelScope.launch {
commands.emit(Command.Browse(DOWNLOAD_DESKTOP_URL))
}
}
private fun sendAnalyticsEvent(type: String) {
viewModelScope.sendEvent(
analytics = analytics,
eventName = ANALYTICS_EVENT_SCREEN,
props = Props(mapOf("type" to type))
)
}
sealed interface Command {
object Exit: Command
data class Browse(val url: Url): Command
}
sealed interface ViewAction {
object CloseScreen: ViewAction
object ToggleMigrationNotReady: ViewAction
object ToggleMigrationReady: ViewAction
object VisitForum: ViewAction
object DownloadDesktop: ViewAction
}
class Factory @Inject constructor(
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MigrationErrorViewModel(
analytics = analytics
) as T
}
}
companion object {
const val DOWNLOAD_DESKTOP_URL = "https://download.anytype.io/"
const val VISIT_FORUM_URL = "https://community.anytype.io/migration"
}
}
private const val ANALYTICS_EVENT_SCREEN = "MigrationGoneWrong"
private const val ANALYTICS_TYPE_MIGRATION_COMPLETED = "complete"
private const val ANALYTICS_TYPE_CHECK_INSTRUCTIONS = "instructions"
private const val ANALYTICS_TYPE_DESKTOP_DOWNLOAD = "download"
private const val ANALYTICS_TYPE_EXIT = "exit"

View file

@ -20,6 +20,7 @@ import com.anytypeio.anytype.domain.spaces.GetLastOpenedSpace
import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.auth.account.MigrationHelperDelegate
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import java.util.Locale
import kotlinx.coroutines.runBlocking
@ -89,6 +90,9 @@ class SplashViewModelTest {
@Mock
lateinit var spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer
@Mock
lateinit var migrationHelperDelegate: MigrationHelperDelegate
lateinit var vm: SplashViewModel
private val defaultSpaceConfig = StubConfig()
@ -119,7 +123,8 @@ class SplashViewModelTest {
globalSubscriptionManager = globalSubscriptionManager,
getLastOpenedSpace = getLastOpenedSpace,
createObjectByTypeAndTemplate = createObjectByTypeAndTemplate,
spaceViews = spaceViewSubscriptionContainer
spaceViews = spaceViewSubscriptionContainer,
migration = migrationHelperDelegate
)
}

View file

@ -812,6 +812,54 @@ message Rpc {
}
}
message Migrate {
message Request {
option (no_auth) = true;
string id = 1; // Id of a selected account
string rootPath = 2;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0; // No error
UNKNOWN_ERROR = 1; // Any other errors
BAD_INPUT = 2; // Id or root path is wrong
ACCOUNT_NOT_FOUND = 101;
CANCELED = 102;
NOT_ENOUGH_FREE_SPACE = 103;
// TODO: [storage] Add specific error codes for migration problems
}
}
}
}
message MigrateCancel {
message Request {
option (no_auth) = true;
string id = 1; // Id of a selected account
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0; // No error
UNKNOWN_ERROR = 1; // Any other errors
BAD_INPUT = 2; // Id or root path is wrong
}
}
}
}
message Select {
/**
* Front end to middleware request-to-launch-a specific account using account id and a root path
@ -854,6 +902,7 @@ message Rpc {
FAILED_TO_FETCH_REMOTE_NODE_HAS_INCOMPATIBLE_PROTO_VERSION = 110;
ACCOUNT_IS_DELETED = 111;
ACCOUNT_LOAD_IS_CANCELED = 112;
ACCOUNT_STORE_NOT_MIGRATED = 113;
CONFIG_FILE_NOT_FOUND = 200;
CONFIG_FILE_INVALID = 201;

View file

@ -732,7 +732,7 @@ message Event {
FaviconHash faviconHash = 6;
Type type = 7;
TargetObjectId targetObjectId = 8;
message Url {
string value = 1;