Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 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
73d3830
Merge branch 'develop' into feat/new-regs-analytics
yamilmedina Jul 3, 2025
91ac6c0
chore: update kalium ref
yamilmedina Jul 3, 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 @@ class WireApplication : BaseApp() {
coreLogic.get().getSessionScope(userId).analyticsIdentifierManager
},
userDataStoreProvider = userDataStoreProvider.get(),
globalDataStore = globalDataStore.get(),
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.first
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 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
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 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex
context.dataStore.edit { it[userDoubleTapToastStatusKey(userId)] = shouldShow }
}

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

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

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

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

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]
if (trackId.isNullOrBlank()) {
val newTrackId = UUID.randomUUID().toString()
setAnonymousRegistrationTrackId(newTrackId)
return newTrackId
}
return trackId
}

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.BuildConfig
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 @@ class CreateAccountVerificationCodeViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@KaliumCoreLogic private val coreLogic: CoreLogic,
private val addAuthenticatedUser: AddAuthenticatedUserUseCase,
private val anonymousAnalyticsManager: AnonymousAnalyticsManager,
private val clientScopeProviderFactory: ClientScopeProvider.Factory,
) : ViewModel() {

Expand All @@ -67,6 +70,7 @@ class CreateAccountVerificationCodeViewModel @Inject constructor(

init {
viewModelScope.launch {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.CodeVerification)
codeTextState.textAsFlow().collectLatest {
if (it.length == codeState.codeLength) onCodeContinue()
}
Expand Down Expand Up @@ -147,6 +151,7 @@ class CreateAccountVerificationCodeViewModel @Inject constructor(
val registerResult = authScope.registerScope.register(registerParam).let {
when (it) {
is RegisterResult.Failure -> {
anonymousAnalyticsManager.sendEvent(AnalyticsEvent.RegistrationPersonalAccount.CodeVerificationFailed)
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