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:
parent
20a95fbaea
commit
73cd7aba94
38 changed files with 643 additions and 572 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 shouldn’t 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>
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -732,7 +732,7 @@ message Event {
|
|||
FaviconHash faviconHash = 6;
|
||||
Type type = 7;
|
||||
TargetObjectId targetObjectId = 8;
|
||||
|
||||
|
||||
|
||||
message Url {
|
||||
string value = 1;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue