Merge pull request #5009 from vector-im/feature/adm/storing-use-case
Storing and tracking the onboarding messaging use case
This commit is contained in:
commit
986d9f92e9
@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import im.vector.app.core.services.VectorSyncService
|
import im.vector.app.core.services.VectorSyncService
|
||||||
|
import im.vector.app.features.session.VectorSessionStore
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.sync.FilterService
|
import org.matrix.android.sdk.api.session.sync.FilterService
|
||||||
@ -76,3 +77,5 @@ fun Session.cannotLogoutSafely(): Boolean {
|
|||||||
// That are not backed up
|
// That are not backed up
|
||||||
!sharedSecretStorageService.isRecoverySetup())
|
!sharedSecretStorageService.isRecoverySetup())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Session.vectorStore(context: Context) = VectorSessionStore(context, myUserId)
|
||||||
|
@ -29,6 +29,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.extensions.startSyncing
|
import im.vector.app.core.extensions.startSyncing
|
||||||
|
import im.vector.app.core.extensions.vectorStore
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.core.utils.deleteAllFiles
|
import im.vector.app.core.utils.deleteAllFiles
|
||||||
import im.vector.app.databinding.ActivityMainBinding
|
import im.vector.app.databinding.ActivityMainBinding
|
||||||
@ -40,6 +41,7 @@ import im.vector.app.features.pin.PinCodeStore
|
|||||||
import im.vector.app.features.pin.PinLocker
|
import im.vector.app.features.pin.PinLocker
|
||||||
import im.vector.app.features.pin.UnlockedActivity
|
import im.vector.app.features.pin.UnlockedActivity
|
||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
|
import im.vector.app.features.session.VectorSessionStore
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.signout.hard.SignedOutActivity
|
import im.vector.app.features.signout.hard.SignedOutActivity
|
||||||
import im.vector.app.features.themes.ActivityOtherThemes
|
import im.vector.app.features.themes.ActivityOtherThemes
|
||||||
@ -143,13 +145,15 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
|||||||
startNextActivityAndFinish()
|
startNextActivityAndFinish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onboardingStore = session.vectorStore(this)
|
||||||
when {
|
when {
|
||||||
args.isAccountDeactivated -> {
|
args.isAccountDeactivated -> {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Just do the local cleanup
|
// Just do the local cleanup
|
||||||
Timber.w("Account deactivated, start app")
|
Timber.w("Account deactivated, start app")
|
||||||
sessionHolder.clearActiveSession()
|
sessionHolder.clearActiveSession()
|
||||||
doLocalCleanup(clearPreferences = true)
|
doLocalCleanup(clearPreferences = true, onboardingStore)
|
||||||
startNextActivityAndFinish()
|
startNextActivityAndFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,14 +167,14 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
|||||||
}
|
}
|
||||||
Timber.w("SIGN_OUT: success, start app")
|
Timber.w("SIGN_OUT: success, start app")
|
||||||
sessionHolder.clearActiveSession()
|
sessionHolder.clearActiveSession()
|
||||||
doLocalCleanup(clearPreferences = true)
|
doLocalCleanup(clearPreferences = true, onboardingStore)
|
||||||
startNextActivityAndFinish()
|
startNextActivityAndFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args.clearCache -> {
|
args.clearCache -> {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
session.clearCache()
|
session.clearCache()
|
||||||
doLocalCleanup(clearPreferences = false)
|
doLocalCleanup(clearPreferences = false, onboardingStore)
|
||||||
session.startSyncing(applicationContext)
|
session.startSyncing(applicationContext)
|
||||||
startNextActivityAndFinish()
|
startNextActivityAndFinish()
|
||||||
}
|
}
|
||||||
@ -183,7 +187,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
|||||||
Timber.w("Ignoring invalid token global error")
|
Timber.w("Ignoring invalid token global error")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doLocalCleanup(clearPreferences: Boolean) {
|
private suspend fun doLocalCleanup(clearPreferences: Boolean, vectorSessionStore: VectorSessionStore) {
|
||||||
// On UI Thread
|
// On UI Thread
|
||||||
Glide.get(this@MainActivity).clearMemory()
|
Glide.get(this@MainActivity).clearMemory()
|
||||||
|
|
||||||
@ -193,6 +197,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
|||||||
pinLocker.unlock()
|
pinLocker.unlock()
|
||||||
pinCodeStore.deleteEncodedPin()
|
pinCodeStore.deleteEncodedPin()
|
||||||
vectorAnalytics.onSignOut()
|
vectorAnalytics.onSignOut()
|
||||||
|
vectorSessionStore.clear()
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// On BG thread
|
// On BG thread
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.analytics
|
|||||||
|
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
|
import im.vector.app.features.analytics.plan.Identity
|
||||||
|
|
||||||
interface AnalyticsTracker {
|
interface AnalyticsTracker {
|
||||||
/**
|
/**
|
||||||
@ -29,4 +30,9 @@ interface AnalyticsTracker {
|
|||||||
* Track a displayed screen
|
* Track a displayed screen
|
||||||
*/
|
*/
|
||||||
fun screen(screen: VectorAnalyticsScreen)
|
fun screen(screen: VectorAnalyticsScreen)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user specific properties
|
||||||
|
*/
|
||||||
|
fun updateUserProperties(identity: Identity)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.analytics.extensions
|
||||||
|
|
||||||
|
import im.vector.app.features.analytics.plan.Identity
|
||||||
|
import im.vector.app.features.onboarding.FtueUseCase
|
||||||
|
|
||||||
|
fun FtueUseCase.toTrackingValue(): Identity.FtueUseCaseSelection {
|
||||||
|
return when (this) {
|
||||||
|
FtueUseCase.FRIENDS_FAMILY -> Identity.FtueUseCaseSelection.PersonalMessaging
|
||||||
|
FtueUseCase.TEAMS -> Identity.FtueUseCaseSelection.WorkMessaging
|
||||||
|
FtueUseCase.COMMUNITIES -> Identity.FtueUseCaseSelection.CommunityMessaging
|
||||||
|
FtueUseCase.SKIP -> Identity.FtueUseCaseSelection.Skip
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.app.features.analytics.impl
|
package im.vector.app.features.analytics.impl
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.posthog.android.Options
|
||||||
import com.posthog.android.PostHog
|
import com.posthog.android.PostHog
|
||||||
import com.posthog.android.Properties
|
import com.posthog.android.Properties
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
@ -25,6 +26,7 @@ import im.vector.app.features.analytics.VectorAnalytics
|
|||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.log.analyticsTag
|
import im.vector.app.features.analytics.log.analyticsTag
|
||||||
|
import im.vector.app.features.analytics.plan.Identity
|
||||||
import im.vector.app.features.analytics.store.AnalyticsStore
|
import im.vector.app.features.analytics.store.AnalyticsStore
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -34,6 +36,9 @@ import timber.log.Timber
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private val REUSE_EXISTING_ID: String? = null
|
||||||
|
private val IGNORED_OPTIONS: Options? = null
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class DefaultVectorAnalytics @Inject constructor(
|
class DefaultVectorAnalytics @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -170,6 +175,10 @@ class DefaultVectorAnalytics @Inject constructor(
|
|||||||
?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties())
|
?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateUserProperties(identity: Identity) {
|
||||||
|
posthog?.identify(REUSE_EXISTING_ID, identity.getProperties().toPostHogProperties(), IGNORED_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Map<String, Any>?.toPostHogProperties(): Properties? {
|
private fun Map<String, Any>?.toPostHogProperties(): Properties? {
|
||||||
if (this == null) return null
|
if (this == null) return null
|
||||||
|
|
||||||
|
@ -16,9 +16,13 @@
|
|||||||
|
|
||||||
package im.vector.app.features.onboarding
|
package im.vector.app.features.onboarding
|
||||||
|
|
||||||
enum class FtueUseCase {
|
enum class FtueUseCase(val persistableValue: String) {
|
||||||
FRIENDS_FAMILY,
|
FRIENDS_FAMILY("friends_family"),
|
||||||
TEAMS,
|
TEAMS("teams"),
|
||||||
COMMUNITIES,
|
COMMUNITIES("communities"),
|
||||||
SKIP
|
SKIP("skip");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(persistedValue: String) = values().first { it.persistableValue == persistedValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,14 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.extensions.configureAndStart
|
import im.vector.app.core.extensions.configureAndStart
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.vectorStore
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.ensureTrailingSlash
|
import im.vector.app.core.utils.ensureTrailingSlash
|
||||||
import im.vector.app.features.VectorFeatures
|
import im.vector.app.features.VectorFeatures
|
||||||
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
|
import im.vector.app.features.analytics.extensions.toTrackingValue
|
||||||
|
import im.vector.app.features.analytics.plan.Identity
|
||||||
import im.vector.app.features.login.HomeServerConnectionConfigFactory
|
import im.vector.app.features.login.HomeServerConnectionConfigFactory
|
||||||
import im.vector.app.features.login.LoginConfig
|
import im.vector.app.features.login.LoginConfig
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
@ -73,7 +77,8 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
private val reAuthHelper: ReAuthHelper,
|
private val reAuthHelper: ReAuthHelper,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val homeServerHistoryService: HomeServerHistoryService,
|
private val homeServerHistoryService: HomeServerHistoryService,
|
||||||
private val vectorFeatures: VectorFeatures
|
private val vectorFeatures: VectorFeatures,
|
||||||
|
private val analyticsTracker: AnalyticsTracker
|
||||||
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ -125,7 +130,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
when (action) {
|
when (action) {
|
||||||
is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
||||||
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow)
|
||||||
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase()
|
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action)
|
||||||
OnboardingAction.ResetUseCase -> resetUseCase()
|
OnboardingAction.ResetUseCase -> resetUseCase()
|
||||||
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
|
||||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
@ -458,13 +463,15 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateUseCase() {
|
private fun handleUpdateUseCase(action: OnboardingAction.UpdateUseCase) {
|
||||||
// TODO act on the use case selection
|
setState { copy(useCase = action.useCase) }
|
||||||
|
analyticsTracker.updateUserProperties(Identity(ftueUseCaseSelection = action.useCase.toTrackingValue()))
|
||||||
_viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
_viewEvents.post(OnboardingViewEvents.OpenServerSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetUseCase() {
|
private fun resetUseCase() {
|
||||||
// TODO remove stored use case
|
setState { copy(useCase = null) }
|
||||||
|
analyticsTracker.updateUserProperties(Identity(ftueUseCaseSelection = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) {
|
private fun handleUpdateServerType(action: OnboardingAction.UpdateServerType) {
|
||||||
@ -745,6 +752,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onSessionCreated(session: Session) {
|
private suspend fun onSessionCreated(session: Session) {
|
||||||
|
awaitState().useCase?.let { useCase ->
|
||||||
|
session.vectorStore(applicationContext).setUseCase(useCase)
|
||||||
|
}
|
||||||
activeSessionHolder.setActiveSession(session)
|
activeSessionHolder.setActiveSession(session)
|
||||||
|
|
||||||
authenticationService.reset()
|
authenticationService.reset()
|
||||||
|
@ -40,6 +40,8 @@ data class OnboardingViewState(
|
|||||||
@PersistState
|
@PersistState
|
||||||
val serverType: ServerType = ServerType.Unknown,
|
val serverType: ServerType = ServerType.Unknown,
|
||||||
@PersistState
|
@PersistState
|
||||||
|
val useCase: FtueUseCase? = null,
|
||||||
|
@PersistState
|
||||||
val signMode: SignMode = SignMode.Unknown,
|
val signMode: SignMode = SignMode.Unknown,
|
||||||
@PersistState
|
@PersistState
|
||||||
val resetPasswordEmail: String? = null,
|
val resetPasswordEmail: String? = null,
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.session
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import im.vector.app.features.onboarding.FtueUseCase
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.matrix.android.sdk.internal.util.md5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local storage for:
|
||||||
|
* - messaging use case (Enum/String)
|
||||||
|
*/
|
||||||
|
class VectorSessionStore constructor(
|
||||||
|
private val context: Context,
|
||||||
|
myUserId: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_session_store_${myUserId.md5()}")
|
||||||
|
private val useCaseKey = stringPreferencesKey("use_case")
|
||||||
|
|
||||||
|
suspend fun readUseCase() = context.dataStore.data.first().let { preferences ->
|
||||||
|
preferences[useCaseKey]?.let { FtueUseCase.from(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setUseCase(useCase: FtueUseCase) {
|
||||||
|
context.dataStore.edit { settings ->
|
||||||
|
settings[useCaseKey] = useCase.persistableValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resetUseCase() {
|
||||||
|
context.dataStore.edit { settings ->
|
||||||
|
settings.remove(useCaseKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clear() {
|
||||||
|
context.dataStore.edit { settings -> settings.clear() }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user