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

DROID-1475 Onboarding | Enhancement | Blocking back navigation when logging out after during sign-up (#171)

This commit is contained in:
Evgenii Kozlov 2023-07-11 22:40:31 +02:00 committed by uburoiubu
parent c2a9d50f14
commit e74a56fc0f
No known key found for this signature in database
GPG key ID: C8FB80E0A595FBB6
10 changed files with 129 additions and 86 deletions

View file

@ -964,6 +964,8 @@ class ComponentManager(
fun release() {
instance = null
}
fun isInitialized() = instance != null
}
class ComponentMap<T>(private val builder: () -> T) {

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.`object`.SetupMobileUseCaseSkip
import com.anytypeio.anytype.domain.platform.MetricsProvider
@ -93,6 +94,7 @@ object OnboardingVoidModule {
interface OnboardingVoidDependencies : ComponentDependencies {
fun authRepository(): AuthRepository
fun userSettings(): UserSettingsRepository
fun blockRepository(): BlockRepository
fun configStorage(): ConfigStorage
fun workspaceManager(): WorkspaceManager

View file

@ -61,7 +61,6 @@ import com.anytypeio.anytype.core_utils.insets.RootViewDeferringInsetsCallback
import com.anytypeio.anytype.di.common.ComponentManager
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ext.daggerViewModel
import com.anytypeio.anytype.presentation.common.ScreenState
import com.anytypeio.anytype.presentation.onboarding.OnboardingStartViewModel
import com.anytypeio.anytype.presentation.onboarding.OnboardingStartViewModel.SideEffect
import com.anytypeio.anytype.presentation.onboarding.OnboardingViewModel
@ -127,6 +126,10 @@ class OnboardingFragment : Fragment() {
private fun OnboardingScreen() {
MaterialTheme {
val navController = rememberAnimatedNavController()
val defaultBackCallback : BackButtonCallback = { navController.popBackStack() }
val signUpBackButtonCallback = remember {
mutableStateOf(defaultBackCallback)
}
Box(
modifier = Modifier
.fillMaxSize()
@ -136,27 +139,31 @@ class OnboardingFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BackgroundCircle()
}
Onboarding(currentPage, navController)
Onboarding(
currentPage = currentPage,
navController = navController,
backButtonCallback = signUpBackButtonCallback
)
PagerIndicator(
pageCount = OnboardingPage.values().filter { it.visible }.size,
page = currentPage,
onBackClick = onBoardingViewModel::onBackPressed
)
}
LaunchedEffect(Unit) {
onBoardingViewModel.navigation.collect { navigation ->
when (navigation) {
OnboardingViewModel.Navigation.Back -> {
navController.popBackStack()
}
onBackClick = {
signUpBackButtonCallback.value?.invoke()
}
}
)
}
LaunchedEffect(Unit) {
onBoardingViewModel.toasts.collect {
toast(it)
}
}
DisposableEffect(
viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.DESTROYED)
) {
onDispose {
signUpBackButtonCallback.value = null
}
}
}
}
@ -179,7 +186,11 @@ class OnboardingFragment : Fragment() {
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun Onboarding(currentPage: MutableState<OnboardingPage>, navController: NavHostController) {
private fun Onboarding(
currentPage: MutableState<OnboardingPage>,
backButtonCallback: MutableState<BackButtonCallback>,
navController: NavHostController
) {
AnimatedNavHost(navController, startDestination = OnboardingNavigation.auth) {
composable(
route = OnboardingNavigation.auth,
@ -227,19 +238,22 @@ class OnboardingFragment : Fragment() {
}
) {
currentPage.value = OnboardingPage.VOID
val component = componentManager().onboardingNewVoidComponent.ReleaseOn(
viewLifecycleOwner = viewLifecycleOwner,
state = Lifecycle.State.DESTROYED
)
val vm = daggerViewModel { component.get().getViewModel() }
VoidScreenWrapper(
contentPaddingTop = ContentPaddingTop(),
onNextClicked = vm::onNextClicked,
screenState = vm.state.collectAsState().value
)
BackHandler {
vm.onSystemBackPressed()
}
backButtonCallback.value = vm::onBackButtonPressed
BackHandler { vm.onSystemBackPressed() }
LaunchedEffect(Unit) {
vm.navigation.collect { navigation ->
when(navigation) {
@ -252,13 +266,6 @@ class OnboardingFragment : Fragment() {
}
}
}
LaunchedEffect(Unit) {
vm.state.collect { state ->
onBoardingViewModel.onLoadingStateChanged(
isLoading = state is ScreenState.Loading
)
}
}
LaunchedEffect(Unit) {
vm.toasts.collect { toast(it) }
}
@ -287,6 +294,7 @@ class OnboardingFragment : Fragment() {
}
) {
currentPage.value = OnboardingPage.MNEMONIC
backButtonCallback.value = { navController.popBackStack() }
Mnemonic(navController, ContentPaddingTop())
}
composable(
@ -304,6 +312,7 @@ class OnboardingFragment : Fragment() {
}
) {
currentPage.value = OnboardingPage.SOUL_CREATION
backButtonCallback.value = { navController.popBackStack() }
CreateSoul(
navController = navController,
contentPaddingTop = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
@ -319,9 +328,10 @@ class OnboardingFragment : Fragment() {
}
) {
currentPage.value = OnboardingPage.SOUL_CREATION_ANIM
backButtonCallback.value = null
CreateSoulAnimation(ContentPaddingTop())
BackHandler {
Timber.d("OnBackHandler")
// Intercepting back-press event but doing nothing.
}
}
composable(
@ -649,4 +659,6 @@ class OnboardingFragment : Fragment() {
}
private const val ANIMATION_LENGTH_SLIDE = 300
private const val ANIMATION_LENGTH_FADE = 700
private const val ANIMATION_LENGTH_FADE = 700
typealias BackButtonCallback = (() -> Unit)?

View file

@ -25,12 +25,12 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingDescription
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.common.ScreenState
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingVoidViewModel
@Composable
fun VoidScreenWrapper(
screenState: ScreenState,
screenState: OnboardingVoidViewModel.ScreenState,
contentPaddingTop: Int,
onNextClicked: () -> Unit
) {
@ -43,7 +43,7 @@ fun VoidScreenWrapper(
@Composable
fun VoidScreen(
screenState: ScreenState,
screenState: OnboardingVoidViewModel.ScreenState,
onNextClicked: () -> Unit,
contentPaddingTop: Int
) {
@ -70,7 +70,7 @@ fun VoidScreen(
onClick = { onNextClicked() },
size = ButtonSize.Large
)
if (screenState is ScreenState.Loading) {
if (screenState is OnboardingVoidViewModel.ScreenState.Loading) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterEnd)

View file

@ -391,4 +391,7 @@ Do the computation of an expensive paragraph of text on a background thread:
<string name="mail_more_space_body">Hi, Anytype team. I am reaching out to request an increase in my file storage capacity as I have run out of storage. My current limits is %1$s. My account id is %2$s. Cheers, %3$s</string>
<string name="your_recovery_phrase_can_t_be_empty">Your recovery phrase can\'t be empty</string>
<string name="go_to_the_app">Go to the app</string>
<string name="exiting_please_wait">Exiting... please wait</string>
<string name="loading_please_wait">Loading... please wait</string>
</resources>

View file

@ -3,25 +3,23 @@ package com.anytypeio.anytype.domain.auth.interactor
import com.anytypeio.anytype.domain.auth.model.AuthStatus
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import javax.inject.Inject
/**
* Use case for checking authorisation status.
* User can be either authorized or unauthorized based on number of available accounts.
* @param repository repository containing information about available accounts
*/
class CheckAuthorizationStatus(
class CheckAuthorizationStatus @Inject constructor(
private val repository: AuthRepository
) : BaseUseCase<AuthStatus, Unit>() {
override suspend fun run(params: Unit) = try {
override suspend fun run(params: Unit) = safe {
repository.getAccounts().let { accounts ->
if (accounts.isNotEmpty())
Either.Right(AuthStatus.AUTHORIZED)
AuthStatus.AUTHORIZED
else
Either.Right(AuthStatus.UNAUTHORIZED)
AuthStatus.UNAUTHORIZED
}
} catch (e: Throwable) {
Either.Left(e)
}
}

View file

@ -5,11 +5,12 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.Interactor
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import javax.inject.Inject
/**
* Use case for logging out.
*/
class Logout(
class Logout @Inject constructor(
private val repo: AuthRepository,
private val config: ConfigStorage,
private val user: UserSettingsRepository,

View file

@ -1,8 +0,0 @@
package com.anytypeio.anytype.presentation.common
sealed class ScreenState {
object Idle: ScreenState()
object Loading: ScreenState()
object Success: ScreenState()
data class Failure(val msg: String): ScreenState()
}

View file

@ -2,36 +2,14 @@ package com.anytypeio.anytype.presentation.onboarding
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.presentation.common.BaseViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class OnboardingViewModel(
private val analytics: Analytics
) : BaseViewModel() {
private val isInProgress = MutableStateFlow(false)
val navigation = MutableSharedFlow<Navigation>()
fun onBackPressed() {
if (!isInProgress.value) {
viewModelScope.launch {
navigation.emit(Navigation.Back)
}
} else {
sendToast(LOADING_MSG)
}
}
fun onLoadingStateChanged(isLoading: Boolean) {
isInProgress.value = isLoading
}
class Factory @Inject constructor(
private val analytics: Analytics
) : ViewModelProvider.Factory {
@ -43,12 +21,4 @@ class OnboardingViewModel(
) as T
}
}
sealed class Navigation {
object Back: Navigation()
}
companion object {
const val LOADING_MSG = "Loading... please wait."
}
}

View file

@ -3,15 +3,18 @@ package com.anytypeio.anytype.presentation.onboarding.signup
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.anytypeio.anytype.domain.auth.interactor.CreateAccount
import com.anytypeio.anytype.domain.auth.interactor.Logout
import com.anytypeio.anytype.domain.auth.interactor.SetupWallet
import com.anytypeio.anytype.domain.auth.model.AuthStatus
import com.anytypeio.anytype.domain.base.Interactor
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.`object`.SetupMobileUseCaseSkip
import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager
import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.common.ScreenState
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@ -26,7 +29,9 @@ class OnboardingVoidViewModel @Inject constructor(
private val pathProvider: PathProvider,
private val spaceGradientProvider: SpaceGradientProvider,
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout
): BaseViewModel() {
val state = MutableStateFlow<ScreenState>(ScreenState.Idle)
@ -99,19 +104,62 @@ class OnboardingVoidViewModel @Inject constructor(
}
}
fun onBackButtonPressed() {
Timber.d("onBackButtonPressed!")
resolveBackNavigation()
}
fun onSystemBackPressed() {
resolveBackNavigation()
}
private fun resolveBackNavigation() {
if (state.value !is ScreenState.Loading) {
viewModelScope.launch {
navigation.emit(
Navigation.GoBack
)
when(state.value) {
is ScreenState.Loading -> {
sendToast(LOADING_MSG)
}
is ScreenState.Exiting -> {
sendToast(EXITING_MSG)
}
else -> {
viewModelScope.launch {
state.value = ScreenState.Exiting.Status
Timber.d("Checking auth status")
checkAuthorizationStatus(Unit).proceed(
failure = { e ->
navigation.emit(Navigation.GoBack).also {
Timber.e(e, "Error while checking auth status")
}
},
success = { status ->
when(status) {
AuthStatus.AUTHORIZED -> {
Timber.d("Proceeding with logout")
proceedWithLogout()
}
AuthStatus.UNAUTHORIZED -> {
navigation.emit(Navigation.GoBack)
}
}
}
)
}
}
}
}
private suspend fun proceedWithLogout() {
logout.invoke(Logout.Params(clearLocalRepositoryData = true)).collect { status ->
when (status) {
is Interactor.Status.Started -> {
state.value = ScreenState.Exiting.Logout.also {
sendToast(EXITING_MSG)
}
}
else -> {
navigation.emit(Navigation.GoBack)
}
}
} else {
sendToast(LOADING_MSG)
}
}
@ -127,7 +175,9 @@ class OnboardingVoidViewModel @Inject constructor(
private val pathProvider: PathProvider,
private val spaceGradientProvider: SpaceGradientProvider,
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -138,12 +188,25 @@ class OnboardingVoidViewModel @Inject constructor(
pathProvider = pathProvider,
spaceGradientProvider = spaceGradientProvider,
relationsSubscriptionManager = relationsSubscriptionManager,
objectTypesSubscriptionManager = objectTypesSubscriptionManager
objectTypesSubscriptionManager = objectTypesSubscriptionManager,
logout = logout,
checkAuthorizationStatus = checkAuthorizationStatus
) as T
}
}
companion object {
const val LOADING_MSG = "Loading, please wait."
const val EXITING_MSG = "Clearing resources, please wait."
}
sealed class ScreenState {
object Idle: ScreenState()
object Loading: ScreenState()
object Success: ScreenState()
sealed class Exiting : ScreenState() {
object Status : Exiting()
object Logout: Exiting()
}
}
}