Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 apps/flipcash/features/menu/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation(libs.bundles.haze)

implementation(project(":apps:flipcash:shared:analytics"))
testImplementation(testFixtures(project(":apps:flipcash:shared:analytics")))
implementation(project(":apps:flipcash:shared:appupdates"))
implementation(project(":apps:flipcash:shared:authentication"))
implementation(project(":apps:flipcash:shared:bills"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.flipcash.app.menu.internal

import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.FlipcashAnalytics
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -30,13 +32,15 @@ internal class ProfileCardViewModel @AssistedInject constructor(
private val tipCodeExporter: TipCodeExporter,
private val shareable: ShareSheetController,
private val resources: ResourceHelper,
private val analytics: FlipcashAnalytics,
) : ViewModel() {

/** Null until the card resolves. */
var card: Scannable.TipCard? by mutableStateOf(null)
private set

init {
analytics.track(ProfileEvents.cardShown())
viewModelScope.launch {
tippingCoordinator.profileCard(userId).onSuccess { card = it }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.flipcash.app.menu.internal

import com.flipcash.analytics.AnalyticsEvent
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.RecordingAnalytics
import com.flipcash.app.core.MainCoroutineRule
import com.flipcash.app.core.bill.Scannable
import com.flipcash.app.core.share.TipCodeExporter
Expand Down Expand Up @@ -55,6 +58,7 @@ class ProfileCardViewModelTest {
tipCodeExporter = exporter,
shareable = mockk<ShareSheetController>(relaxed = true),
resources = resources,
analytics = RecordingAnalytics(),
)
vm.download()
BottomBarManager.messages.value.first().actions.first().onClick()
Expand Down Expand Up @@ -83,4 +87,22 @@ class ProfileCardViewModelTest {
fun `a blank name falls back to the generic name`() = runTest(mainCoroutineRule.dispatcher) {
assertEquals("Profile Card", exportedBaseName(" "))
}

@Test
fun `opening the card sends Card Shown even when the card fails to load`() = runTest(mainCoroutineRule.dispatcher) {
val tipping = mockk<TippingCoordinator>()
coEvery { tipping.profileCard(any()) } returns Result.failure(IllegalStateException("offline"))
val analytics = RecordingAnalytics()

ProfileCardViewModel(
userId = listOf<Byte>(1),
tippingCoordinator = tipping,
tipCodeExporter = exporter,
shareable = mockk<ShareSheetController>(relaxed = true),
resources = resources,
analytics = analytics,
)

assertEquals(listOf<AnalyticsEvent>(ProfileEvents.cardShown()), analytics.events)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ internal fun PersonProfileScreen(
anchorHeight = menuAnchorHeight,
onDismiss = { menuOpen = false },
onItem = { item ->
viewModel.dispatchEvent(ChatProfileViewModel.Event.MenuActionPicked(item))
when (item) {
ChatProfileAction.Block ->
viewModel.dispatchEvent(ChatProfileViewModel.Event.BlockUser)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.flipcash.app.messenger.internal.screens.profile

import androidx.lifecycle.viewModelScope
import com.flipcash.analytics.ProfileMenuAction
import com.flipcash.analytics.ProfilePinnedAction as PinnedButton
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.FlipcashAnalytics
import com.flipcash.app.blocklist.BlocklistCoordinator
import com.flipcash.app.contacts.ContactCoordinator
import com.flipcash.app.core.AppRoute
Expand Down Expand Up @@ -69,6 +73,7 @@ internal class ChatProfileViewModel @Inject constructor(
private val startChattingPayer: StartChattingPayer,
private val chatController: ChatController,
private val featuredGroupsStore: FeaturedGroupsStore,
private val analytics: FlipcashAnalytics,
) : BaseViewModel<ChatProfileViewModel.State, ChatProfileViewModel.Event>(
initialState = State(selfId = userManager.accountId),
updateStateForEvent = updateStateForEvent,
Expand Down Expand Up @@ -181,6 +186,9 @@ internal class ChatProfileViewModel @Inject constructor(

/** The pinned button was tapped; what it does is [State.pinnedAction]'s. */
data object PinnedActionTapped : Event

/** A row of the profile menu was tapped; the screen routes it separately. */
data class MenuActionPicked(val action: ChatProfileAction) : Event
data object Unblock : Event
/** The slide on the fee sheet was confirmed. */
data object ConfirmStartChatting : Event
Expand Down Expand Up @@ -380,14 +388,38 @@ internal class ChatProfileViewModel @Inject constructor(
.onEach {
val state = stateFlow.value
when (val action = state.pinnedAction) {
ProfilePinnedAction.Unblock -> dispatchEvent(Event.Unblock)
ProfilePinnedAction.OpenChat -> state.dmChatId?.let { dispatchEvent(Event.OpenChat(it)) }
is ProfilePinnedAction.StartChatting -> startChatting()
ProfilePinnedAction.Unblock -> {
analytics.track(ProfileEvents.pinnedButtonTapped(PinnedButton.UNBLOCK))
dispatchEvent(Event.Unblock)
}
ProfilePinnedAction.OpenChat -> {
analytics.track(ProfileEvents.pinnedButtonTapped(PinnedButton.OPEN_CHAT))
state.dmChatId?.let { dispatchEvent(Event.OpenChat(it)) }
}
is ProfilePinnedAction.StartChatting -> {
analytics.track(ProfileEvents.pinnedButtonTapped(PinnedButton.START_CHATTING))
startChatting()
}
ProfilePinnedAction.OpeningChat, null -> Unit
}
}
.launchIn(viewModelScope)

// Silence and Remove are group moderation, not in the Profile menu list, so they send nothing.
eventFlow
.filterIsInstance<Event.MenuActionPicked>()
.mapNotNull { event ->
when (event.action) {
ChatProfileAction.Mute -> ProfileMenuAction.MUTE
ChatProfileAction.Report -> ProfileMenuAction.REPORT
ChatProfileAction.Block -> ProfileMenuAction.BLOCK
ChatProfileAction.Unblock -> ProfileMenuAction.UNBLOCK
ChatProfileAction.Silence, ChatProfileAction.Remove -> null
}
}
.onEach { analytics.track(ProfileEvents.menuAction(it)) }
.launchIn(viewModelScope)

eventFlow
.filterIsInstance<Event.Unblock>()
.mapNotNull { (stateFlow.value.participant as? ChatParticipant.TipUser)?.userId }
Expand Down Expand Up @@ -553,6 +585,7 @@ internal class ChatProfileViewModel @Inject constructor(
is Event.BlockConfirmed,
is Event.BlockSuccessful,
Event.PinnedActionTapped,
is Event.MenuActionPicked,
Event.Unblock,
Event.ConfirmStartChatting,
Event.PresentDepositOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.flipcash.app.messenger.internal.screens.profile

import com.flipcash.analytics.AnalyticsEvent
import com.flipcash.analytics.ProfileMenuAction
import com.flipcash.analytics.ProfilePinnedAction as PinnedButton
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.RecordingAnalytics
import com.flipcash.app.blocklist.BlocklistCoordinator
import com.flipcash.app.contacts.ContactCoordinator
import com.flipcash.app.core.MainCoroutineRule
Expand Down Expand Up @@ -88,6 +93,8 @@ class ChatProfileViewModelTest {
coEvery { mayProceed(any(), any()) } returns true
}

private val analytics = RecordingAnalytics()

private fun viewModel() = ChatProfileViewModel(
contactCoordinator = mockk<ContactCoordinator>(relaxed = true),
userManager = userManager,
Expand All @@ -102,6 +109,7 @@ class ChatProfileViewModelTest {
startChattingPayer = payer,
chatController = chatController,
featuredGroupsStore = featuredGroupsStore,
analytics = analytics,
)

private fun openFull(model: ChatProfileViewModel) =
Expand Down Expand Up @@ -625,4 +633,106 @@ class ChatProfileViewModelTest {

assertEquals(onBo, next)
}

// region analytics

private fun pinnedTapSends(expected: PinnedButton) {
assertEquals(listOf<AnalyticsEvent>(ProfileEvents.pinnedButtonTapped(expected)), analytics.events)
}

@Test
fun `tapping start chatting sends Pinned Button Tapped`() = runTest {
fee.value = Fiat(1, CurrencyCode.USD)
val model = viewModel()
openFull(model)

model.dispatchEvent(ChatProfileViewModel.Event.PinnedActionTapped)

pinnedTapSends(PinnedButton.START_CHATTING)
}

@Test
fun `tapping open chat sends Pinned Button Tapped`() = runTest {
members.value = listOf(mockk<ChatMember>())
val model = viewModel()
openFull(model)
assertEquals(ProfilePinnedAction.OpenChat, model.stateFlow.value.pinnedAction)

model.dispatchEvent(ChatProfileViewModel.Event.PinnedActionTapped)

pinnedTapSends(PinnedButton.OPEN_CHAT)
}

@Test
fun `tapping the pinned unblock sends Pinned Button Tapped and no menu action`() = runTest {
blocked.value = true
val model = viewModel()
openFull(model)

model.dispatchEvent(ChatProfileViewModel.Event.PinnedActionTapped)

pinnedTapSends(PinnedButton.UNBLOCK)
}

@Test
fun `tapping with no pinned button sends nothing`() = runTest {
val model = viewModel()
openFull(model)
assertNull(model.stateFlow.value.pinnedAction)

model.dispatchEvent(ChatProfileViewModel.Event.PinnedActionTapped)

assertEquals(emptyList<AnalyticsEvent>(), analytics.events)
}

@Test
fun `tapping while the chat is opening sends nothing`() = runTest {
fee.value = Fiat(1, CurrencyCode.USD)
coEvery { payer.pay(any(), any(), any(), any()) } returns Result.success(dmChatId)
val model = viewModel()
openFull(model)
model.dispatchEvent(ChatProfileViewModel.Event.ConfirmStartChatting)
advanceTimeBy(1_000)
assertEquals(ProfilePinnedAction.OpeningChat, model.stateFlow.value.pinnedAction)

model.dispatchEvent(ChatProfileViewModel.Event.PinnedActionTapped)

assertEquals(emptyList<AnalyticsEvent>(), analytics.events)
}

@Test
fun `menu Mute, Report, Block and Unblock each send Menu Action`() = runTest {
val model = viewModel()
openFull(model)

listOf(
ChatProfileAction.Mute,
ChatProfileAction.Report,
ChatProfileAction.Block,
ChatProfileAction.Unblock,
).forEach { model.dispatchEvent(ChatProfileViewModel.Event.MenuActionPicked(it)) }

assertEquals(
listOf<AnalyticsEvent>(
ProfileEvents.menuAction(ProfileMenuAction.MUTE),
ProfileEvents.menuAction(ProfileMenuAction.REPORT),
ProfileEvents.menuAction(ProfileMenuAction.BLOCK),
ProfileEvents.menuAction(ProfileMenuAction.UNBLOCK),
),
analytics.events,
)
}

@Test
fun `menu Silence and Remove send nothing`() = runTest {
val model = viewModel()
openFull(model)

model.dispatchEvent(ChatProfileViewModel.Event.MenuActionPicked(ChatProfileAction.Silence))
model.dispatchEvent(ChatProfileViewModel.Event.MenuActionPicked(ChatProfileAction.Remove))

assertEquals(emptyList<AnalyticsEvent>(), analytics.events)
}

// endregion
}
1 change: 1 addition & 0 deletions apps/flipcash/features/myaccount/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation(libs.compose.paging)

implementation(project(":apps:flipcash:shared:analytics"))
testImplementation(testFixtures(project(":apps:flipcash:shared:analytics")))
implementation(project(":apps:flipcash:shared:appsettings"))
implementation(project(":apps:flipcash:shared:appupdates"))
implementation(project(":apps:flipcash:shared:authentication"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.flipcash.app.core.ui.forViewer
import androidx.lifecycle.viewModelScope
import com.flipcash.analytics.AddMoneySource
import com.flipcash.analytics.events.AddMoneyEvents
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.FlipcashAnalytics
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.DisplayNameSource
Expand Down Expand Up @@ -93,6 +94,7 @@ internal class EditProfileViewModel @Inject constructor(
}

init {
analytics.track(ProfileEvents.editProfileViewed())
val profile = userManager.state
.filter { it.authState is AuthState.Ready }
.map { it.userProfile }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.flipcash.app.myaccount.internal.settings

import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.FlipcashAnalytics
import com.flipcash.app.appsettings.AppSettingValue
import com.flipcash.app.appsettings.AppSettingsCoordinator
import com.flipcash.app.auth.AuthManager
Expand Down Expand Up @@ -49,6 +51,7 @@ internal class SettingsViewModel @Inject constructor(
releaseStageProvider: ReleaseStageProvider,
private val resources: ResourceHelper,
private val authManager: AuthManager,
private val analytics: FlipcashAnalytics,
dispatchers: DispatcherProvider,
) : BaseViewModel<SettingsViewModel.State, SettingsViewModel.Event>(
initialState = State(),
Expand Down Expand Up @@ -119,6 +122,7 @@ internal class SettingsViewModel @Inject constructor(
}

init {
analytics.track(ProfileEvents.settingsViewed())
dispatchEvent(Event.OnAppVersionUpdated(versionInfo))

combine(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.flipcash.app.myaccount.internal

import com.flipcash.analytics.AnalyticsEvent
import com.flipcash.analytics.events.ProfileEvents
import com.flipcash.app.analytics.FlipcashAnalytics
import com.flipcash.app.analytics.RecordingAnalytics
import com.flipcash.app.appsettings.AppSettingsCoordinator
import com.flipcash.app.auth.AuthManager
import com.flipcash.app.core.MainCoroutineRule
Expand Down Expand Up @@ -168,7 +172,9 @@ class SettingsViewModelTest {

private val featureFlags = mockk<FeatureFlagController>(relaxed = true)

private fun TestScope.createViewModel(): SettingsViewModel {
private fun TestScope.createViewModel(
analytics: FlipcashAnalytics = RecordingAnalytics(),
): SettingsViewModel {
val userFlags = mockk<UserFlagsCoordinator>(relaxed = true)
val resolved = mockk<ResolvedUserFlags>(relaxed = true) {
every { isStaff } returns ResolvedFlag(serverValue = false, override = FieldOverride.None)
Expand All @@ -189,6 +195,7 @@ class SettingsViewModelTest {
releaseStageProvider = mockk<ReleaseStageProvider>(relaxed = true),
resources = FakeResourceHelper(),
authManager = mockk<AuthManager>(relaxed = true),
analytics = analytics,
dispatchers = TestDispatchers(testScheduler),
)
}
Expand Down Expand Up @@ -252,4 +259,13 @@ class SettingsViewModelTest {
}

// endregion

@Test
fun `opening the screen sends Settings Viewed once`() = runTest(mainCoroutineRule.dispatcher) {
val analytics = RecordingAnalytics()
createViewModel(analytics)
advanceUntilIdle()

assertEquals(listOf<AnalyticsEvent>(ProfileEvents.settingsViewed()), analytics.events)
}
}
Loading
Loading