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

DROID-2198 Analytics | Fix | Fix order of events for join flow (#869)

This commit is contained in:
Evgenii Kozlov 2024-02-14 18:20:01 +01:00 committed by GitHub
parent c03d2cd173
commit 54c9fc0389
Signed by: github
GPG key ID: B5690EEEBB952194
11 changed files with 70 additions and 321 deletions

View file

@ -194,10 +194,7 @@ object EventsDictionary {
enum class ScreenOnboardingStep(val value: String) {
VOID("Void"),
PHRASE("Phrase"),
SOUL("Soul"),
SOUL_CREATING("SoulCreating"),
SPACE_CREATING("SpaceCreating")
PHRASE("Phrase")
}
enum class ClickOnboardingButton(val value: String) {

View file

@ -5,6 +5,7 @@ import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingMnemonicViewModel
import dagger.Binds
import dagger.Component
@ -55,6 +56,7 @@ object OnboardingMnemonicModule {
interface OnboardingMnemonicDependencies : ComponentDependencies {
fun authRepository(): AuthRepository
fun analytics(): Analytics
fun config(): ConfigStorage
}
@Scope

View file

@ -466,7 +466,9 @@ class OnboardingFragment : Fragment() {
viewModel = vm,
onCheckLaterClicked = {
findNavController().navigate(R.id.action_openHome)
vm.sendAnalyticsOnboardingScreen()
},
onGoToAppClicked = {
findNavController().navigate(R.id.action_openHome)
},
copyMnemonicToClipboard = ::copyMnemonicToClipboard,
vm = vm,
@ -533,7 +535,6 @@ class OnboardingFragment : Fragment() {
when (navigation) {
is OnboardingStartViewModel.AuthNavigation.ProceedWithSignUp -> {
navController.navigate(OnboardingNavigation.setProfileName)
vm.sendAnalyticsOnboardingScreen()
}
is OnboardingStartViewModel.AuthNavigation.ProceedWithSignIn -> {

View file

@ -11,7 +11,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.preference.Preference
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.NetworkModeConstants
import com.anytypeio.anytype.core_utils.const.FileConstants
@ -27,7 +26,6 @@ import javax.inject.Inject
class OnboardingNetworkSetupDialog : BaseBottomSheetComposeFragment() {
private lateinit var pickerDelegate: PickerDelegate
private lateinit var filePathPreference: Preference
@Inject
lateinit var factory: PreferencesViewModel.Factory

View file

@ -129,7 +129,8 @@ fun NetworkSetupScreen(
AnytypeNetworkCard(onAnytypeNetworkClicked, config)
Section(
title = stringResource(id = R.string.network_settings_networks_section),
color = NetworkSettingDescriptionColor
color = NetworkSettingDescriptionColor,
textPaddingStart = 0.dp
)
SelfHostCard(
config = config,

View file

@ -53,6 +53,7 @@ import com.anytypeio.anytype.ui.onboarding.MnemonicStub
fun MnemonicPhraseScreenWrapper(
viewModel: OnboardingMnemonicViewModel,
onCheckLaterClicked: () -> Unit,
onGoToAppClicked: () -> Unit,
copyMnemonicToClipboard: (String) -> Unit,
vm: OnboardingMnemonicViewModel,
mnemonicColorPalette: List<Color>
@ -67,7 +68,12 @@ fun MnemonicPhraseScreenWrapper(
}
},
copyMnemonicToClipboard = copyMnemonicToClipboard,
mnemonicColorPalette = mnemonicColorPalette
mnemonicColorPalette = mnemonicColorPalette,
onGoToAppClicked = {
onGoToAppClicked().also {
vm.onGoToTheAppClicked()
}
}
)
}
@ -82,7 +88,8 @@ fun PreviewMnemonicPhraseScreen() {
reviewMnemonic = { /*TODO*/ },
onCheckLaterClicked = { /*TODO*/ },
copyMnemonicToClipboard = {},
mnemonicColorPalette = emptyList()
mnemonicColorPalette = emptyList(),
onGoToAppClicked = {}
)
}
@ -92,6 +99,7 @@ fun MnemonicPhraseScreen(
state: OnboardingMnemonicViewModel.State,
reviewMnemonic: () -> Unit,
onCheckLaterClicked: () -> Unit,
onGoToAppClicked: () -> Unit,
copyMnemonicToClipboard: (String) -> Unit,
mnemonicColorPalette: List<Color>
) {
@ -117,6 +125,7 @@ fun MnemonicPhraseScreen(
modifier = Modifier.align(Alignment.BottomCenter),
openMnemonic = reviewMnemonic,
onCheckLaterClicked = onCheckLaterClicked,
onGoToAppClicked = onGoToAppClicked,
state = state
)
}
@ -161,6 +170,7 @@ fun MnemonicButtons(
modifier: Modifier = Modifier,
openMnemonic: () -> Unit,
onCheckLaterClicked: () -> Unit,
onGoToAppClicked: () -> Unit,
state: OnboardingMnemonicViewModel.State
) {
Column(modifier.wrapContentHeight()) {
@ -171,7 +181,7 @@ fun MnemonicButtons(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 24.dp),
onClick = { onCheckLaterClicked.invoke() },
onClick = { onGoToAppClicked() },
size = ButtonSize.Large
)
}

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Name
@ -256,7 +257,8 @@ fun SpaceNameInput(
@Composable
fun Section(
title: String,
color: Color = colorResource(id = R.color.text_secondary)
color: Color = colorResource(id = R.color.text_secondary),
textPaddingStart: Dp = 20.dp
) {
Box(modifier = Modifier
.height(52.dp)
@ -264,7 +266,7 @@ fun Section(
Text(
modifier = Modifier
.padding(
start = 20.dp,
start = textPaddingStart,
bottom = 8.dp
)
.align(Alignment.BottomStart),

View file

@ -5,9 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.EventsDictionary.ScreenOnboardingStep.VOID
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@ -52,10 +50,6 @@ class OnboardingStartViewModel @Inject constructor(
}
}
fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics, VOID)
}
interface AuthNavigation {
object ProceedWithSignUp : AuthNavigation
object ProceedWithSignIn : AuthNavigation

View file

@ -6,8 +6,10 @@ import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingClickEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import com.anytypeio.anytype.presentation.extension.sendOpenAccountEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@ -15,12 +17,16 @@ import timber.log.Timber
class OnboardingMnemonicViewModel @Inject constructor(
private val getMnemonic: GetMnemonic,
private val analytics: Analytics
private val analytics: Analytics,
private val configStorage: ConfigStorage
) : ViewModel() {
val state = MutableStateFlow<State>(State.Idle(""))
init {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.PHRASE
)
viewModelScope.launch {
proceedWithMnemonicPhrase()
}
@ -43,6 +49,25 @@ class OnboardingMnemonicViewModel @Inject constructor(
type = EventsDictionary.ClickOnboardingButton.CHECK_LATER,
step = EventsDictionary.ScreenOnboardingStep.PHRASE
)
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
}
}
}
fun onGoToTheAppClicked() {
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
}
}
}
private suspend fun proceedWithMnemonicPhrase() {
@ -55,12 +80,6 @@ class OnboardingMnemonicViewModel @Inject constructor(
)
}
fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.SOUL
)
}
sealed interface State {
val mnemonicPhrase: String
@ -72,13 +91,15 @@ class OnboardingMnemonicViewModel @Inject constructor(
class Factory @Inject constructor(
private val getMnemonic: GetMnemonic,
private val analytics: Analytics
private val analytics: Analytics,
private val configStorage: ConfigStorage,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return OnboardingMnemonicViewModel(
getMnemonic = getMnemonic,
analytics = analytics
analytics = analytics,
configStorage = configStorage
) as T
}
}

View file

@ -51,6 +51,14 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
private val localeProvider: LocaleProvider
) : BaseViewModel() {
init {
viewModelScope.launch {
sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.VOID
)
}
}
val state = MutableStateFlow<ScreenState>(ScreenState.Idle)
val navigation = MutableSharedFlow<Navigation>()
@ -111,7 +119,6 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
state.value = ScreenState.Idle
},
onSuccess = {
analytics.sendEvent(eventName = EventsDictionary.createSpace)
createAccountAnalytics(startTime)
val config = configStorage.getOrNull()
if (config != null) {
@ -134,14 +141,16 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
val config = configStorage.getOrNull()
if (config != null) {
viewModelScope.launch {
sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.SOUL_CREATING
)
analytics.sendEvent(eventName = EventsDictionary.createSpace)
setSpaceDetails.async(
SetSpaceDetails.Params(
space = SpaceId(config.space),
details = mapOf(Relations.NAME to name)
)
).fold(
onFailure = {
Timber.e(it, "Error while setting space details")
}
)
setObjectDetails.async(
SetObjectDetails.Params(
@ -151,16 +160,14 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
onFailure = {
Timber.e(it, "Error while setting profile name details")
navigation.emit(Navigation.NavigateToMnemonic)
sendAnalyticsOnboardingScreen()
// Workaround for leaving screen in loading state to wait screen transition
delay(OnboardingVoidViewModel.LOADING_AFTER_SUCCESS_DELAY)
delay(LOADING_AFTER_SUCCESS_DELAY)
state.value = ScreenState.Success
},
onSuccess = {
navigation.emit(Navigation.NavigateToMnemonic)
sendAnalyticsOnboardingScreen()
// Workaround for leaving screen in loading state to wait screen transition
delay(OnboardingVoidViewModel.LOADING_AFTER_SUCCESS_DELAY)
delay(LOADING_AFTER_SUCCESS_DELAY)
state.value = ScreenState.Success
}
)
@ -196,12 +203,6 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
}
}
private fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.PHRASE
)
}
class Factory @Inject constructor(
private val setObjectDetails: SetObjectDetails,
private val setSpaceDetails: SetSpaceDetails,

View file

@ -1,278 +0,0 @@
package com.anytypeio.anytype.presentation.onboarding.signup
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.CrashReporter
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.exceptions.CreateAccountException
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.config.ConfigStorage
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.domain.misc.LocaleProvider
import com.anytypeio.anytype.domain.`object`.ImportGetStartedUseCase
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.extension.proceedWithAccountEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class OnboardingVoidViewModel @Inject constructor(
private val createAccount: CreateAccount,
private val setupWallet: SetupWallet,
private val importGetStartedUseCase: ImportGetStartedUseCase,
private val pathProvider: PathProvider,
private val spaceGradientProvider: SpaceGradientProvider,
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout,
private val analytics: Analytics,
private val configStorage: ConfigStorage,
private val crashReporter: CrashReporter,
private val localeProvider: LocaleProvider
): BaseViewModel() {
val state = MutableStateFlow<ScreenState>(ScreenState.Idle)
val navigation = MutableSharedFlow<Navigation>()
fun onNextClicked() {
if (state.value !is ScreenState.Loading) {
proceedWithCreatingWallet()
} else {
sendToast(LOADING_MSG)
}
}
private fun proceedWithCreatingWallet() {
state.value = ScreenState.Loading
setupWallet.invoke(
scope = viewModelScope,
params = SetupWallet.Params(
path = pathProvider.providePath()
)
) { result ->
result.either(
fnL = {
Timber.e(it, "Error while setting up wallet")
},
fnR = {
proceedWithCreatingAccount()
}
)
}
}
@Deprecated("To be deleted")
private fun proceedWithCreatingAccount() {
val startTime = System.currentTimeMillis()
val params = CreateAccount.Params(
name = "",
avatarPath = null,
iconGradientValue = spaceGradientProvider.randomId()
)
viewModelScope.launch {
createAccount.async(params = params).fold(
onFailure = { error ->
Timber.d("Error while creating account: ${error.message ?: "Unknown error"}").also {
when(error) {
CreateAccountException.NetworkError -> {
sendToast(
"Failed to create your account due to a network error: ${error.message ?: "Unknown error"}"
)
}
CreateAccountException.OfflineDevice -> {
sendToast("Your device seems to be offline. Please, check your connection and try again.")
}
else -> {
sendToast("Error: ${error.message ?: "Unknown error"}")
}
}
}
state.value = ScreenState.Idle
},
onSuccess = {
createAccountAnalytics(startTime)
val config = configStorage.getOrNull()
if (config != null) {
crashReporter.setUser(config.analytics)
relationsSubscriptionManager.onStart()
objectTypesSubscriptionManager.onStart()
proceedWithSettingUpMobileUseCase(config.space)
}
},
)
}
}
private fun proceedWithSettingUpMobileUseCase(space: Id) {
viewModelScope.launch {
importGetStartedUseCase.async(ImportGetStartedUseCase.Params(space)).fold(
onFailure = {
Timber.e(it, "Error while importing use case")
navigation.emit(Navigation.NavigateToMnemonic)
sendAnalyticsOnboardingScreen()
// Workaround for leaving screen in loading state to wait screen transition
delay(LOADING_AFTER_SUCCESS_DELAY)
state.value = ScreenState.Success
},
onSuccess = {
navigation.emit(Navigation.NavigateToMnemonic)
sendAnalyticsOnboardingScreen()
// Workaround for leaving screen in loading state to wait screen transition
delay(LOADING_AFTER_SUCCESS_DELAY)
state.value = ScreenState.Success
}
)
}
}
fun onBackButtonPressed() {
Timber.d("onBackButtonPressed!")
resolveBackNavigation()
}
fun onSystemBackPressed() {
resolveBackNavigation()
}
private fun resolveBackNavigation() {
when(state.value) {
is ScreenState.Loading -> {
sendToast(LOADING_MSG)
}
is ScreenState.Exiting -> {
// Do nothing
}
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() {
// N.B. If we clear repository data at this step, user won't be able to login into this account.
logout.invoke(Logout.Params(clearLocalRepositoryData = false)).collect { status ->
when (status) {
is Interactor.Status.Started -> {
state.value = ScreenState.Exiting.Logout.also {
sendToast(EXITING_MSG)
}
}
else -> {
navigation.emit(Navigation.GoBack)
}
}
}
}
private fun createAccountAnalytics(startTime: Long) {
viewModelScope.launch {
analytics.proceedWithAccountEvent(
startTime = startTime,
configStorage = configStorage,
eventName = EventsDictionary.createAccount,
lang = localeProvider.language()
)
}
}
private fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.PHRASE
)
}
sealed class Navigation {
object NavigateToMnemonic: Navigation()
object GoBack: Navigation()
}
class Factory @Inject constructor(
private val createAccount: CreateAccount,
private val setupWallet: SetupWallet,
private val importGetStartedUseCase: ImportGetStartedUseCase,
private val pathProvider: PathProvider,
private val spaceGradientProvider: SpaceGradientProvider,
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout,
private val analytics: Analytics,
private val configStorage: ConfigStorage,
private val crashReporter: CrashReporter,
private val localeProvider: LocaleProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return OnboardingVoidViewModel(
createAccount = createAccount,
setupWallet = setupWallet,
importGetStartedUseCase = importGetStartedUseCase,
pathProvider = pathProvider,
spaceGradientProvider = spaceGradientProvider,
relationsSubscriptionManager = relationsSubscriptionManager,
objectTypesSubscriptionManager = objectTypesSubscriptionManager,
logout = logout,
checkAuthorizationStatus = checkAuthorizationStatus,
analytics = analytics,
crashReporter = crashReporter,
configStorage = configStorage,
localeProvider = localeProvider
) as T
}
}
companion object {
const val LOADING_MSG = "Loading, please wait."
const val EXITING_MSG = "Clearing resources, please wait."
const val LOADING_AFTER_SUCCESS_DELAY = 600L
}
sealed class ScreenState {
object Idle: ScreenState()
object Loading: ScreenState()
object Success: ScreenState()
sealed class Exiting : ScreenState() {
object Status : Exiting()
object Logout: Exiting()
}
}
}