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

DROID-1392 Onboarding | Analytics | Linear onboarding events (#185)

This commit is contained in:
Konstantin Ivanov 2023-07-12 21:26:43 +02:00 committed by GitHub
parent ffb3086db6
commit 4c62d96eed
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 189 additions and 18 deletions

View file

@ -148,6 +148,29 @@ object EventsDictionary {
const val screenHome = "ScreenHome"
const val selectHomeTab = "SelectHomeTab"
// Onboarding events
const val screenOnboarding = "ScreenOnboarding"
const val clickOnboarding = "ClickOnboarding"
const val clickLogin = "ClickLogin"
enum class ScreenOnboardingStep(val value: String) {
VOID("Void"),
PHRASE("Phrase"),
SOUL("Soul"),
SOUL_CREATING("SoulCreating"),
SPACE_CREATING("SpaceCreating")
}
enum class ClickOnboardingButton(val value: String) {
SHOW_AND_COPY("ShowAndCopy"),
CHECK_LATER("CheckLater")
}
enum class ClickLoginButton(val value: String) {
PHRASE("Phrase"),
QR("Qr"),
}
// Routes
object Routes {
const val home = "ScreenHome"
@ -216,4 +239,5 @@ object EventsPropertiesKey {
const val align = "align"
const val originalId = "originalId"
const val view = "view"
const val step = "step"
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.di.feature.onboarding.signup
import androidx.lifecycle.ViewModelProvider
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
@ -53,6 +54,7 @@ object OnboardingMnemonicModule {
interface OnboardingMnemonicDependencies : ComponentDependencies {
fun authRepository(): AuthRepository
fun analytics(): Analytics
}
@Scope

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.di.feature.onboarding.signup
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
@ -60,6 +61,7 @@ interface OnboardingSoulCreationDependencies : ComponentDependencies {
fun blockRepository(): BlockRepository
fun dispatchers(): AppCoroutineDispatchers
fun configStorage(): ConfigStorage
fun analytics(): Analytics
}
@Scope

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.di.feature.onboarding.signup
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.domain.auth.interactor.CreateAccount
@ -103,4 +104,5 @@ interface OnboardingVoidDependencies : ComponentDependencies {
fun pathProvider(): PathProvider
fun metricsProvider(): MetricsProvider
fun dispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
}

View file

@ -394,7 +394,10 @@ class OnboardingFragment : Fragment() {
RecoveryScreenWrapper(
vm = vm,
onBackClicked = vm::onBackButtonPressed,
onScanQrClick = { isQrWarningDialogVisible.value = true },
onScanQrClick = {
isQrWarningDialogVisible.value = true
vm.onScanQrCodeClicked()
},
)
LaunchedEffect(Unit) {
vm.sideEffects.collect { effect ->
@ -525,6 +528,7 @@ class OnboardingFragment : Fragment() {
navController.navigate(
route = OnboardingNavigation.createSoulAnim
)
vm.sendAnalyticsOnboardingScreen()
}
}
}
@ -542,13 +546,16 @@ class OnboardingFragment : Fragment() {
viewLifecycleOwner = viewLifecycleOwner,
state = Lifecycle.State.DESTROYED
)
val vm = daggerViewModel { component.get().getViewModel() }
MnemonicPhraseScreenWrapper(
contentPaddingTop = contentPaddingTop,
viewModel = daggerViewModel { component.get().getViewModel() },
viewModel = vm,
openSoulCreation = {
navController.navigate(OnboardingNavigation.createSoul)
vm.sendAnalyticsOnboardingScreen()
},
copyMnemonicToClipboard = ::copyMnemonicToClipboard
copyMnemonicToClipboard = ::copyMnemonicToClipboard,
vm = vm
)
}
@ -607,6 +614,7 @@ class OnboardingFragment : Fragment() {
when (navigation) {
is OnboardingStartViewModel.AuthNavigation.ProceedWithSignUp -> {
navController.navigate(OnboardingNavigation.void)
vm.sendAnalyticsOnboardingScreen()
}
is OnboardingStartViewModel.AuthNavigation.ProceedWithSignIn -> {

View file

@ -43,7 +43,8 @@ fun MnemonicPhraseScreenWrapper(
viewModel: OnboardingMnemonicViewModel,
openSoulCreation: () -> Unit,
copyMnemonicToClipboard: (String) -> Unit,
contentPaddingTop: Int
contentPaddingTop: Int,
vm: OnboardingMnemonicViewModel
) {
val state = viewModel.state.collectAsStateWithLifecycle().value
MnemonicPhraseScreen(
@ -51,7 +52,8 @@ fun MnemonicPhraseScreenWrapper(
reviewMnemonic = { viewModel.openMnemonic() },
openSoulCreation = openSoulCreation,
copyMnemonicToClipboard = copyMnemonicToClipboard,
contentPaddingTop = contentPaddingTop
contentPaddingTop = contentPaddingTop,
vm = vm
)
}
@ -61,7 +63,8 @@ fun MnemonicPhraseScreen(
reviewMnemonic: () -> Unit,
openSoulCreation: () -> Unit,
copyMnemonicToClipboard: (String) -> Unit,
contentPaddingTop: Int
contentPaddingTop: Int,
vm: OnboardingMnemonicViewModel
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
@ -79,7 +82,8 @@ fun MnemonicPhraseScreen(
modifier = Modifier.align(Alignment.BottomCenter),
openMnemonic = reviewMnemonic,
openSoulCreation = openSoulCreation,
state = state
state = state,
vm = vm
)
}
}
@ -89,7 +93,8 @@ fun MnemonicButtons(
modifier: Modifier = Modifier,
openMnemonic: () -> Unit,
openSoulCreation: () -> Unit,
state: OnboardingMnemonicViewModel.State
state: OnboardingMnemonicViewModel.State,
vm: OnboardingMnemonicViewModel
) {
Column(modifier.wrapContentHeight()) {
when (state) {
@ -126,6 +131,7 @@ fun MnemonicButtons(
),
onClick = {
openSoulCreation.invoke()
vm.onCheckLaterClicked()
},
size = ButtonSize.Large,
textColor = ColorButtonRegular

View file

@ -1606,4 +1606,51 @@ suspend fun Analytics.sendHideKeyboardEvent() {
sendEvent(
eventName = EventsDictionary.hideKeyboard
)
}
fun CoroutineScope.sendAnalyticsOnboardingScreenEvent(
analytics: Analytics,
step: EventsDictionary.ScreenOnboardingStep
) {
sendEvent(
analytics = analytics,
eventName = EventsDictionary.screenOnboarding,
props = Props(
buildMap {
put(EventsPropertiesKey.step, step.value)
}
)
)
}
fun CoroutineScope.sendAnalyticsOnboardingClickEvent(
analytics: Analytics,
type: EventsDictionary.ClickOnboardingButton,
step: EventsDictionary.ScreenOnboardingStep
) {
sendEvent(
analytics = analytics,
eventName = EventsDictionary.clickOnboarding,
props = Props(
buildMap {
put(EventsPropertiesKey.type, type.value)
put(EventsPropertiesKey.step, step.value)
}
)
)
}
fun CoroutineScope.sendAnalyticsOnboardingLoginEvent(
analytics: Analytics,
type: EventsDictionary.ClickLoginButton
) {
sendEvent(
analytics = analytics,
eventName = EventsDictionary.clickLogin,
props = Props(
buildMap {
put(EventsPropertiesKey.type, type.value)
}
)
)
}

View file

@ -4,6 +4,10 @@ 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.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
@ -12,7 +16,12 @@ class OnboardingStartViewModel @Inject constructor(
private val analytics: Analytics
) : ViewModel() {
// TODO send analytics.
init {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.authScreenShow
)
}
val sideEffects = MutableSharedFlow<SideEffect>()
val navigation: MutableSharedFlow<AuthNavigation> = MutableSharedFlow()
@ -39,6 +48,10 @@ class OnboardingStartViewModel @Inject constructor(
}
}
fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics, VOID)
}
interface AuthNavigation {
object ProceedWithSignUp : AuthNavigation
object ProceedWithSignIn : AuthNavigation

View file

@ -11,6 +11,7 @@ import com.anytypeio.anytype.domain.auth.interactor.RecoverWallet
import com.anytypeio.anytype.domain.auth.interactor.SaveMnemonic
import com.anytypeio.anytype.domain.device.PathProvider
import com.anytypeio.anytype.presentation.common.ViewState
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingLoginEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@ -36,6 +37,10 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
fun onLoginClicked(chain: String) {
proceedWithRecoveringWallet(chain.trim())
viewModelScope.sendAnalyticsOnboardingLoginEvent(
analytics = analytics,
type = EventsDictionary.ClickLoginButton.PHRASE
)
}
fun onActionDone(chain: String) {
@ -114,6 +119,13 @@ class OnboardingMnemonicLoginViewModel @Inject constructor(
}
}
fun onScanQrCodeClicked() {
viewModelScope.sendAnalyticsOnboardingLoginEvent(
analytics = analytics,
type = EventsDictionary.ClickLoginButton.QR
)
}
sealed class SideEffect {
object ProceedWithLogin : SideEffect()
data class Error(val msg: String): SideEffect()

View file

@ -3,14 +3,19 @@ package com.anytypeio.anytype.presentation.onboarding.signup
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.EventsDictionary
import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingClickEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class OnboardingMnemonicViewModel @Inject constructor(
private val getMnemonic: GetMnemonic
private val getMnemonic: GetMnemonic,
private val analytics: Analytics
) : ViewModel() {
val state = MutableStateFlow<State>(State.Idle(""))
@ -25,6 +30,19 @@ class OnboardingMnemonicViewModel @Inject constructor(
if (state.value is State.Mnemonic) {
state.value = State.MnemonicOpened((state.value as State.Mnemonic).mnemonicPhrase)
}
viewModelScope.sendAnalyticsOnboardingClickEvent(
analytics = analytics,
type = EventsDictionary.ClickOnboardingButton.SHOW_AND_COPY,
step = EventsDictionary.ScreenOnboardingStep.PHRASE
)
}
fun onCheckLaterClicked() {
viewModelScope.sendAnalyticsOnboardingClickEvent(
analytics = analytics,
type = EventsDictionary.ClickOnboardingButton.CHECK_LATER,
step = EventsDictionary.ScreenOnboardingStep.PHRASE
)
}
private suspend fun proceedWithMnemonicPhrase() {
@ -37,6 +55,11 @@ class OnboardingMnemonicViewModel @Inject constructor(
)
}
fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.SOUL
)
}
sealed interface State {
@ -48,12 +71,14 @@ class OnboardingMnemonicViewModel @Inject constructor(
}
class Factory @Inject constructor(
private val getMnemonic: GetMnemonic
private val getMnemonic: GetMnemonic,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return OnboardingMnemonicViewModel(
getMnemonic = getMnemonic
getMnemonic = getMnemonic,
analytics = analytics
) as T
}
}

View file

@ -3,10 +3,13 @@ package com.anytypeio.anytype.presentation.onboarding.signup
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.EventsDictionary
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@ -15,7 +18,8 @@ import timber.log.Timber
class OnboardingSoulCreationViewModel @Inject constructor(
private val setObjectDetails: SetObjectDetails,
private val configStorage: ConfigStorage
private val configStorage: ConfigStorage,
private val analytics: Analytics
) : ViewModel() {
val toasts = MutableSharedFlow<String>()
@ -65,6 +69,10 @@ class OnboardingSoulCreationViewModel @Inject constructor(
Timber.e(it, "Error while updating object details")
},
onSuccess = {
sendAnalyticsOnboardingScreenEvent(
analytics = analytics,
step = EventsDictionary.ScreenOnboardingStep.SPACE_CREATING
)
_navigationFlow.emit(Navigation.OpenSoulCreationAnim(name))
}
)
@ -80,19 +88,27 @@ class OnboardingSoulCreationViewModel @Inject constructor(
viewModelScope.launch { toasts.emit(msg) }
}
fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.SOUL_CREATING
)
}
sealed interface Navigation {
class OpenSoulCreationAnim(val name: String): Navigation
}
class Factory @Inject constructor(
private val setObjectDetails: SetObjectDetails,
private val configStorage: ConfigStorage
private val configStorage: ConfigStorage,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return OnboardingSoulCreationViewModel(
setObjectDetails = setObjectDetails,
configStorage = configStorage
configStorage = configStorage,
analytics = analytics
) as T
}
}

View file

@ -3,6 +3,8 @@ package com.anytypeio.anytype.presentation.onboarding.signup
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.EventsDictionary
import com.anytypeio.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.anytypeio.anytype.domain.auth.interactor.CreateAccount
import com.anytypeio.anytype.domain.auth.interactor.Logout
@ -15,6 +17,7 @@ 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.extension.sendAnalyticsOnboardingScreenEvent
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
import kotlinx.coroutines.delay
@ -32,7 +35,8 @@ class OnboardingVoidViewModel @Inject constructor(
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout
private val logout: Logout,
private val analytics: Analytics
): BaseViewModel() {
val state = MutableStateFlow<ScreenState>(ScreenState.Idle)
@ -95,12 +99,14 @@ class OnboardingVoidViewModel @Inject constructor(
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
@ -169,6 +175,12 @@ class OnboardingVoidViewModel @Inject constructor(
}
}
private fun sendAnalyticsOnboardingScreen() {
viewModelScope.sendAnalyticsOnboardingScreenEvent(analytics,
EventsDictionary.ScreenOnboardingStep.PHRASE
)
}
sealed class Navigation {
object NavigateToMnemonic: Navigation()
object GoBack: Navigation()
@ -183,7 +195,8 @@ class OnboardingVoidViewModel @Inject constructor(
private val relationsSubscriptionManager: RelationsSubscriptionManager,
private val objectTypesSubscriptionManager: ObjectTypesSubscriptionManager,
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val logout: Logout
private val logout: Logout,
private val analytics: Analytics
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -196,7 +209,8 @@ class OnboardingVoidViewModel @Inject constructor(
relationsSubscriptionManager = relationsSubscriptionManager,
objectTypesSubscriptionManager = objectTypesSubscriptionManager,
logout = logout,
checkAuthorizationStatus = checkAuthorizationStatus
checkAuthorizationStatus = checkAuthorizationStatus,
analytics = analytics
) as T
}
}