Skip to content

feat(new-registration): add flows anonymous usage data (WPB-17529) #4088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1db38d7
fix: general fixes about navigation and copies
yamilmedina Jun 18, 2025
eab79c6
feat: implement analytics changes for new reg
yamilmedina Jun 18, 2025
7c09f70
feat: pr comments
yamilmedina Jun 18, 2025
2731926
feat: detekt fixes
yamilmedina Jun 18, 2025
ba8df2d
Merge branch 'fix/copy-new-registration-flow' into feat/new-regs-anal…
yamilmedina Jun 18, 2025
bd4aaef
feat: wip analytics
yamilmedina Jun 18, 2025
f68ea4e
feat: wip analytics, sending events
yamilmedina Jun 19, 2025
d671984
feat: wip analytics, sending events
yamilmedina Jun 19, 2025
0d528c7
feat: wip analytics, sending events
yamilmedina Jun 19, 2025
bd87787
feat: add analytics to registration flows screens
yamilmedina Jun 23, 2025
328e087
feat: add analytics to registration flows screens, add warmup for sdk
yamilmedina Jun 23, 2025
c01e5c3
feat: add analytics to registration flows screens, add warmup for sdk
yamilmedina Jun 23, 2025
98ff06a
Merge branch 'develop' into feat/new-regs-analytics
yamilmedina Jun 26, 2025
d6d667b
chore: update kalium ref
yamilmedina Jun 26, 2025
06f52f5
feat: init countly only when is allowed to
yamilmedina Jun 27, 2025
e432433
feat: perform cleanup and transfer id for user after registratiobn
yamilmedina Jun 27, 2025
3c2a4ed
feat: perform cleanup and transfer id for user after registratiobn
yamilmedina Jun 27, 2025
a49a2d0
feat: perform cleanup and write test coverage
yamilmedina Jun 27, 2025
7601e83
feat: write test coverage
yamilmedina Jun 27, 2025
ad6a7d1
feat: rename usecase
yamilmedina Jun 27, 2025
f7ad846
Merge branch 'develop' into feat/new-regs-analytics
yamilmedina Jun 27, 2025
9974f4e
empty commit
yamilmedina Jun 27, 2025
b14e65a
feat: test coverage
yamilmedina Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
coreLogic.get().getSessionScope(userId).analyticsIdentifierManager
},
userDataStoreProvider = userDataStoreProvider.get(),
globalDataStore = globalDataStore.get(),

Check warning on line 233 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L233

Added line #L233 was not covered by tests
currentBackend = { userId ->
coreLogic.get().getSessionScope(userId).users.serverLinks()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.analytics

import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject

/**
* Finalize the registration process and analytics metadata in case there was enabled in the process.
* Transfers the identifier to the logged in user and clean up the registration process.
*/
class FinalizeRegistrationAnalyticsMetadataUseCase @Inject constructor(
private val globalDataStore: GlobalDataStore,
@CurrentAccount private val currentAccount: UserId,
@KaliumCoreLogic private val coreLogic: CoreLogic
) {
suspend operator fun invoke() {
if (globalDataStore.isAnonymousRegistrationEnabled().firstOrNull() == false) return

val trackId = globalDataStore.getAnonymousRegistrationTrackId()
if (!trackId.isNullOrBlank()) {
coreLogic.getSessionScope(currentAccount).setNewUserTrackingIdentifier(trackId)
globalDataStore.clearAnonymousRegistrationTrackId()
globalDataStore.setAnonymousRegistrationEnabled(false)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
*/
package com.wire.android.analytics

import com.wire.android.datastore.GlobalDataStore
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.feature.analytics.model.AnalyticsProfileProperties
import com.wire.android.feature.analytics.model.AnalyticsResult
import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.android.util.isHostValidForAnalytics
import com.wire.kalium.logic.data.analytics.AnalyticsIdentifierResult
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.analytics.AnalyticsContactsData
Expand Down Expand Up @@ -53,18 +54,42 @@ fun ObserveCurrentSessionAnalyticsUseCase(
observeAnalyticsTrackingIdentifierStatusFlow: suspend (UserId) -> Flow<AnalyticsIdentifierResult>,
analyticsIdentifierManagerProvider: (UserId) -> AnalyticsIdentifierManager,
userDataStoreProvider: UserDataStoreProvider,
globalDataStore: GlobalDataStore,
currentBackend: suspend (UserId) -> SelfServerConfigUseCase.Result
) = object : ObserveCurrentSessionAnalyticsUseCase {

private var previousAnalyticsResult: AnalyticsIdentifierResult? = null

@Suppress("LongMethod")
override fun invoke(): Flow<AnalyticsResult<AnalyticsIdentifierManager>> = currentSessionFlow
.flatMapLatest {
if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) {
val userId = it.accountInfo.userId
val analyticsIdentifierManager = analyticsIdentifierManagerProvider(userId)
override fun invoke(): Flow<AnalyticsResult<AnalyticsIdentifierManager>> {
return combine(
currentSessionFlow,
globalDataStore.isAnonymousRegistrationEnabled()
) { currentSession, isAnonymousRegistrationEnabled ->
currentSession to isAnonymousRegistrationEnabled
}.flatMapLatest { (currentSession, isAnonymousRegistrationEnabled) ->
if (isAnonymousRegistrationEnabled) {
val anonymousRegistrationTrackId = globalDataStore.getOrCreateAnonymousRegistrationTrackId()
return@flatMapLatest flowOf(
AnalyticsResult<AnalyticsIdentifierManager>(
identifierResult = AnalyticsIdentifierResult.RegistrationIdentifier(anonymousRegistrationTrackId),
profileProperties = {
AnalyticsProfileProperties(
isTeamMember = false,
teamId = null,
contactsAmount = null,
teamMembersAmount = null,
isEnterprise = null
)
},
manager = null
)
)
}

if (currentSession is CurrentSessionResult.Success && currentSession.accountInfo.isValid()) {
val userId = currentSession.accountInfo.userId
val analyticsIdentifierManager = analyticsIdentifierManagerProvider(userId)
combine(
observeAnalyticsTrackingIdentifierStatusFlow(userId)
.filter { currentIdentifierResult ->
Expand All @@ -74,15 +99,12 @@ fun ObserveCurrentSessionAnalyticsUseCase(
currentIdentifierResult != previousAnalyticsResult &&
currentResult?.identifier != previousResult?.identifier
},
userDataStoreProvider.getOrCreate(userId).isAnonymousUsageDataEnabled()
userDataStoreProvider.getOrCreate(userId).isAnonymousUsageDataEnabled(),
) { analyticsIdentifierResult, enabled ->
previousAnalyticsResult = analyticsIdentifierResult

val isProdBackend = when (val serverConfig = currentBackend(userId)) {
is SelfServerConfigUseCase.Result.Success ->
serverConfig.serverLinks.links.api == ServerConfig.PRODUCTION.api
|| serverConfig.serverLinks.links.api == ServerConfig.STAGING.api

is SelfServerConfigUseCase.Result.Success -> serverConfig.serverLinks.isHostValidForAnalytics()
is SelfServerConfigUseCase.Result.Failure -> false
}

Expand Down Expand Up @@ -125,6 +147,6 @@ fun ObserveCurrentSessionAnalyticsUseCase(
)
)
}
}
.distinctUntilChanged()
}.distinctUntilChanged()
}
}
36 changes: 36 additions & 0 deletions app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -55,6 +56,8 @@
private val APP_LOCK_SOURCE = intPreferencesKey("app_lock_source")
private val ENTER_TO_SENT = booleanPreferencesKey("enter_to_sent")
private val WIRE_CELLS = booleanPreferencesKey("wire_cells")
private val ANONYMOUS_REGISTRATION_TRACK_ID = stringPreferencesKey("anonymous_registration_track_id")
private val IS_ANONYMOUS_REGISTRATION_ENABLED = booleanPreferencesKey("is_anonymous_registration_enabled")

val APP_THEME_OPTION = stringPreferencesKey("app_theme_option")
val RECORD_AUDIO_EFFECTS_CHECKBOX = booleanPreferencesKey("record_audio_effects_checkbox")
Expand Down Expand Up @@ -99,6 +102,39 @@
context.dataStore.edit { it[userDoubleTapToastStatusKey(userId)] = shouldShow }
}

fun isAnonymousRegistrationEnabled(): Flow<Boolean> =
getBooleanPreference(IS_ANONYMOUS_REGISTRATION_ENABLED, false)

Check warning on line 106 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L106

Added line #L106 was not covered by tests

suspend fun setAnonymousRegistrationEnabled(enabled: Boolean) {
context.dataStore.edit { it[IS_ANONYMOUS_REGISTRATION_ENABLED] = enabled }

Check warning on line 109 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L109

Added line #L109 was not covered by tests
}

suspend fun clearAnonymousRegistrationTrackId() {
context.dataStore.edit {
it.remove(ANONYMOUS_REGISTRATION_TRACK_ID)

Check warning on line 114 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L113-L114

Added lines #L113 - L114 were not covered by tests
}
}

private suspend fun setAnonymousRegistrationTrackId(trackId: String) {
context.dataStore.edit {
it[ANONYMOUS_REGISTRATION_TRACK_ID] = trackId

Check warning on line 120 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L119-L120

Added lines #L119 - L120 were not covered by tests
}
}

suspend fun getAnonymousRegistrationTrackId(): String? {
return context.dataStore.data.firstOrNull()?.get(ANONYMOUS_REGISTRATION_TRACK_ID)
}

suspend fun getOrCreateAnonymousRegistrationTrackId(): String {
val trackId = context.dataStore.data.first()[ANONYMOUS_REGISTRATION_TRACK_ID]

Check warning on line 129 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L129

Added line #L129 was not covered by tests
if (trackId.isNullOrBlank()) {
val newTrackId = UUID.randomUUID().toString()
setAnonymousRegistrationTrackId(newTrackId)
return newTrackId

Check warning on line 133 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L131-L133

Added lines #L131 - L133 were not covered by tests
}
return trackId

Check warning on line 135 in app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt#L135

Added line #L135 was not covered by tests
}

suspend fun getShouldShowDoubleTapToast(userId: String): Boolean =
getBooleanPreference(userDoubleTapToastStatusKey(userId), true).first()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.analytics.FinalizeRegistrationAnalyticsMetadataUseCase
import com.wire.android.feature.analytics.AnonymousAnalyticsManager
import com.wire.android.feature.analytics.model.AnalyticsEvent
import com.wire.android.ui.authentication.create.common.handle.HandleUpdateErrorState
import com.wire.android.ui.common.textfield.textAsFlow
import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult
Expand All @@ -39,7 +42,9 @@ import javax.inject.Inject
@HiltViewModel
class CreateAccountUsernameViewModel @Inject constructor(
private val validateUserHandleUseCase: ValidateUserHandleUseCase,
private val setUserHandleUseCase: SetUserHandleUseCase
private val setUserHandleUseCase: SetUserHandleUseCase,
private val anonymousAnalyticsManager: AnonymousAnalyticsManager,
private val finalizeRegistrationAnalyticsMetadata: FinalizeRegistrationAnalyticsMetadataUseCase
) : ViewModel() {

val textState: TextFieldState = TextFieldState()
Expand All @@ -48,6 +53,7 @@ class CreateAccountUsernameViewModel @Inject constructor(

init {
viewModelScope.launch {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.Username)
textState.textAsFlow()
.dropWhile { it.isEmpty() } // ignore first empty value to not show the error before the user typed anything
.collectLatest { newHandle ->
Expand Down Expand Up @@ -79,7 +85,11 @@ class CreateAccountUsernameViewModel @Inject constructor(
is SetUserHandleResult.Failure.Generic -> HandleUpdateErrorState.DialogError.GenericError(result.error)
SetUserHandleResult.Failure.HandleExists -> HandleUpdateErrorState.TextFieldError.UsernameTakenError
SetUserHandleResult.Failure.InvalidHandle -> HandleUpdateErrorState.TextFieldError.UsernameInvalidError
SetUserHandleResult.Success -> HandleUpdateErrorState.None
SetUserHandleResult.Success -> {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.CreationCompleted)
finalizeRegistrationAnalyticsMetadata()
HandleUpdateErrorState.None
}
}
state = state.copy(
loading = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import com.wire.android.config.orDefault
import com.wire.android.di.ClientScopeProvider
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.analytics.AnonymousAnalyticsManager
import com.wire.android.feature.analytics.model.AnalyticsEvent
import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs
import com.wire.android.ui.common.textfield.textAsFlow
import com.wire.android.ui.navArgs
Expand All @@ -53,6 +55,7 @@
savedStateHandle: SavedStateHandle,
@KaliumCoreLogic private val coreLogic: CoreLogic,
private val addAuthenticatedUser: AddAuthenticatedUserUseCase,
private val anonymousAnalyticsManager: AnonymousAnalyticsManager,

Check warning on line 58 in app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt#L58

Added line #L58 was not covered by tests
private val clientScopeProviderFactory: ClientScopeProvider.Factory,
) : ViewModel() {

Expand All @@ -67,6 +70,7 @@

init {
viewModelScope.launch {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.CodeVerification)

Check warning on line 73 in app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt#L73

Added line #L73 was not covered by tests
codeTextState.textAsFlow().collectLatest {
if (it.length == codeState.codeLength) onCodeContinue()
}
Expand Down Expand Up @@ -147,6 +151,7 @@
val registerResult = authScope.registerScope.register(registerParam).let {
when (it) {
is RegisterResult.Failure -> {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.CodeVerificationFailed)

Check warning on line 154 in app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt#L154

Added line #L154 was not covered by tests
updateCodeErrorState(it.toCodeError())
return@launch
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.CustomTabsHelper
import com.wire.android.util.EMPTY
import com.wire.android.util.isHostValidForAnalytics
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.configuration.server.ServerConfig

Expand Down Expand Up @@ -275,12 +276,14 @@ private fun AccountDetailsContent(
autoFill = false,
)

Row(modifier = Modifier.padding(end = MaterialTheme.wireDimensions.spacing16x)) {
WireCheckbox(
checked = state.privacyPolicyAccepted,
onCheckedChange = onPrivacyPolicyAccepted,
)
WirePrivacyPolicyLink()
if (serverConfig.isHostValidForAnalytics()) {
Row(modifier = Modifier.padding(end = MaterialTheme.wireDimensions.spacing16x)) {
WireCheckbox(
checked = state.privacyPolicyAccepted,
onCheckedChange = onPrivacyPolicyAccepted,
)
WirePrivacyPolicyLink()
}
}
}

Expand Down
Loading
Loading