diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index 5a9e2058b..8fba08333 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -25,6 +25,7 @@ import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.features.messenger.R import com.flipcash.services.models.UserProfile import com.flipcash.services.models.chat.ChatId +import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.DeliveryStatus import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.models.chat.TypingState @@ -251,6 +252,14 @@ internal class ChatViewModel @Inject constructor( .map { it.participant is ChatParticipant.TipUser } .distinctUntilChanged() + /** The [ChatType] backing this conversation, derived from the resolved participant. */ + private val ChatParticipant?.chatType: ChatType + get() = when (this) { + is ChatParticipant.TipUser -> ChatType.TIP_DM + is ChatParticipant.Contact -> ChatType.CONTACT_DM + null -> ChatType.UNKNOWN + } + private val amountStyleFlow by lazy { isTipFlow .map { amountStyle(isTip = it) } @@ -530,6 +539,7 @@ internal class ChatViewModel @Inject constructor( val textToSend = stateFlow.value.chatInputState.text.toString() val chatId = stateFlow.value.chatId ?: return@onEach if (textToSend.isBlank()) return@onEach + val chatType = stateFlow.value.participant.chatType stateFlow.value.chatInputState.setTextAndPlaceCursorAtEnd("") @@ -537,11 +547,11 @@ internal class ChatViewModel @Inject constructor( chatCoordinator.sendMessage(chatId, textToSend) .onSuccess { trace("message sent successfully") - analytics.messageSentInChat() + analytics.messageSentInChat(type = chatType) } .onFailure { cause -> trace("message failed to send - ${cause.localizedMessage}") - analytics.messageSentInChat(error = cause) + analytics.messageSentInChat(type = chatType, error = cause) } } } @@ -663,11 +673,18 @@ internal class ChatViewModel @Inject constructor( } } + // A payment into a tip DM is a tip; a contact DM is a plain cash send. + val transferEvent = if (stateFlow.value.participant is ChatParticipant.TipUser) { + Analytics.Transfer.SentTip + } else { + Analytics.Transfer.SentCash + } + result.onSuccess { dispatchEvent(Event.SendStateUpdated(success = true)) delay(400.milliseconds) analytics.transfer( - event = Analytics.Transfer.SentCash, + event = transferEvent, amount = verifiedFiat.localFiat, successful = true, ) @@ -678,7 +695,7 @@ internal class ChatViewModel @Inject constructor( }.onFailure { cause -> dispatchEvent(Event.SendStateUpdated()) analytics.transfer( - event = Analytics.Transfer.SentCash, + event = transferEvent, amount = verifiedFiat.localFiat, error = cause, ) diff --git a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Analytics.kt b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Analytics.kt index d5d539809..4696d2c6a 100644 --- a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Analytics.kt +++ b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Analytics.kt @@ -3,6 +3,7 @@ package com.flipcash.app.analytics import androidx.compose.runtime.Composable import com.flipcash.app.core.navigation.DeeplinkType import com.flipcash.services.internal.model.thirdparty.OnRampProvider +import com.flipcash.services.models.chat.ChatType import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.libs.analytics.AnalyticsService import com.getcode.libs.analytics.AppAction @@ -35,7 +36,9 @@ interface FlipcashAnalyticsService : AnalyticsService { fun openTokenInfo(source: Analytics.TokenInfoSource, mint: Mint) fun buy(method: Analytics.PurchaseMethod, mint: Mint, amount: Fiat, error: Throwable? = null) fun sell(mint: Mint, amount: Fiat, feeAmount: Fiat, error: Throwable? = null) - fun messageSentInChat(error: Throwable? = null) + fun messageSentInChat(type: ChatType, error: Throwable? = null) + fun tipCardScanned() + fun tipCardPresented() fun deeplinkOpened(url: String) fun deeplinkParsed(type: DeeplinkType?, url: String) fun deeplinkRouted(type: DeeplinkType, error: Throwable? = null) @@ -64,6 +67,7 @@ object Analytics { } data object SentCash : Transfer + data object SentTip : Transfer } enum class OnrampSource { Settings, Balance, Give } enum class AddMoneySource { Menu, GiveShortfall, BuyShortfall, Chat, Scanner, Balance } @@ -114,7 +118,9 @@ class StubFlipcashAnalytics : FlipcashAnalyticsService { override fun buy(method: Analytics.PurchaseMethod, mint: Mint, amount: Fiat, error: Throwable?) = Unit override fun sell(mint: Mint, amount: Fiat, feeAmount: Fiat, error: Throwable?) = Unit - override fun messageSentInChat(error: Throwable?) = Unit + override fun messageSentInChat(type: ChatType, error: Throwable?) = Unit + override fun tipCardScanned() = Unit + override fun tipCardPresented() = Unit override fun deeplinkOpened(url: String) = Unit override fun deeplinkParsed(type: DeeplinkType?, url: String) = Unit diff --git a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt index 7cc9c4470..1f8cbe15f 100644 --- a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt +++ b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/Events.kt @@ -3,6 +3,7 @@ package com.flipcash.app.analytics import androidx.core.net.toUri import com.flipcash.app.core.navigation.DeeplinkType import com.flipcash.services.internal.model.thirdparty.OnRampProvider +import com.flipcash.services.models.chat.ChatType import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.model.core.ID import com.getcode.opencode.model.financial.CurrencyCode @@ -133,22 +134,38 @@ internal sealed interface AnalyticsEvent { override val name = "Receive Cash Link" } - sealed interface ChatEvent: AnalyticsEvent { - data object SentCash : ChatEvent { - override val name = "Sent Cash" - } + data object SentTip : Transfer { + override val name = "Sent Tip" + } + data object SentCash : ChatEvent { + override val name = "Sent Cash" + } + + sealed interface ChatEvent: AnalyticsEvent { data class SentMessage( + val chatType: ChatType, val error: Throwable? = null ): ChatEvent { override val name = "Sent Message" override fun toProperties() = buildMap { + put("Chat Type", chatType.propertyValue) error?.let { put("Error", it.message.orEmpty()) } } } } + sealed interface TipCardEvent : AnalyticsEvent { + data object Scanned : TipCardEvent { + override val name = "Tip Card Scanned" + } + + data object Presented : TipCardEvent { + override val name = "Tip Card Presented" + } + } + sealed interface PoolEvent : AnalyticsEvent { val id: ID override fun toProperties() = mapOf("ID" to id.base58) @@ -359,6 +376,13 @@ internal val Analytics.AddMoneyMethod.propertyValue: String Analytics.AddMoneyMethod.Reserves -> "Reserves" } +internal val ChatType.propertyValue: String + get() = when (this) { + ChatType.CONTACT_DM -> "Contact" + ChatType.TIP_DM -> "Tip" + ChatType.UNKNOWN -> "Unknown" + } + internal fun LocalFiat.asProperties(): Map { return buildMap { putAll(underlyingTokenAmount.asProperties()) @@ -388,5 +412,6 @@ internal fun Analytics.Transfer.toAnalyticsEvent(): AnalyticsEvent = when (this) is Analytics.Transfer.ClaimedCashLink -> AnalyticsEvent.ClaimedCashLink is Analytics.Transfer.SentCashLink.Clipboard -> AnalyticsEvent.SentCashLink(clipboard = true) is Analytics.Transfer.SentCashLink.App -> AnalyticsEvent.SentCashLink(app = name) - is Analytics.Transfer.SentCash -> AnalyticsEvent.ChatEvent.SentCash + is Analytics.Transfer.SentCash -> AnalyticsEvent.SentCash + is Analytics.Transfer.SentTip -> AnalyticsEvent.SentTip } \ No newline at end of file diff --git a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/internal/MixpanelAnalyticsDelegate.kt b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/internal/MixpanelAnalyticsDelegate.kt index deb577524..c0217be32 100644 --- a/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/internal/MixpanelAnalyticsDelegate.kt +++ b/apps/flipcash/shared/analytics/src/main/kotlin/com/flipcash/app/analytics/internal/MixpanelAnalyticsDelegate.kt @@ -7,6 +7,7 @@ import com.flipcash.app.analytics.asProperties import com.flipcash.app.analytics.toAnalyticsEvent import com.flipcash.app.core.navigation.DeeplinkType import com.flipcash.services.internal.model.thirdparty.OnRampProvider +import com.flipcash.services.models.chat.ChatType import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.libs.analytics.AppAction import com.getcode.libs.analytics.AppActionSource @@ -238,8 +239,16 @@ internal class MixpanelAnalyticsDelegate @Inject constructor( track(AnalyticsEvent.TokenTransactionEvent.Sell(mint, amount, feeAmount, error)) } - override fun messageSentInChat(error: Throwable?) { - track(AnalyticsEvent.ChatEvent.SentMessage(error = error)) + override fun messageSentInChat(type: ChatType, error: Throwable?) { + track(AnalyticsEvent.ChatEvent.SentMessage(chatType = type, error = error)) + } + + override fun tipCardScanned() { + track(AnalyticsEvent.TipCardEvent.Scanned) + } + + override fun tipCardPresented() { + track(AnalyticsEvent.TipCardEvent.Presented) } override fun deeplinkOpened(url: String) { diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/CodeScanDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/CodeScanDelegate.kt index 20915ad21..95fcbb488 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/CodeScanDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/CodeScanDelegate.kt @@ -151,6 +151,7 @@ class CodeScanDelegate @Inject constructor( // rendezvous grab. Hand the id to the shell, which routes to TipCardOperations to // resolve and present the card. val userId = payload.userId ?: return + analytics.tipCardScanned() _events.trySend(Event.TipCardScanned(userId)) } } diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/TipCardDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/TipCardDelegate.kt index 7e3f6e794..6d49b0a0a 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/TipCardDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/TipCardDelegate.kt @@ -1,5 +1,6 @@ package com.flipcash.app.session.internal.delegates +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.bill.Scannable import com.flipcash.app.session.TipCardOperations import com.flipcash.libs.coroutines.DispatcherProvider @@ -36,6 +37,7 @@ import javax.inject.Singleton @Singleton class TipCardDelegate @Inject constructor( private val tippingCoordinator: TippingCoordinator, + private val analytics: FlipcashAnalyticsService, dispatchers: DispatcherProvider, ) : TipCardOperations { @@ -60,6 +62,7 @@ class TipCardDelegate @Inject constructor( // Always present the card. Whether the tip modal slides up (or an // add-money prompt shows instead) is decided in the UI from the // coordinator's affordability state — see TipCardDecorator. + analytics.tipCardPresented() _events.trySend(Event.Present(card)) } .onFailure { diff --git a/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt b/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt index ab90231b0..8b376e319 100644 --- a/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt +++ b/apps/flipcash/shared/tipping/src/main/kotlin/com/flipcash/shared/tipping/TippingCoordinator.kt @@ -1,5 +1,7 @@ package com.flipcash.shared.tipping +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.chat.ChatIdentifier import com.flipcash.app.core.tipping.TipAmount @@ -64,6 +66,7 @@ class TippingCoordinator @Inject constructor( private val verifiedFiatCalculator: VerifiedFiatCalculator, private val resources: ResourceHelper, private val purchaseMethodController: PurchaseMethodController, + private val analytics: FlipcashAnalyticsService, ) : TipSelectionHolder { /** The signed-in user's id ([UserManager.accountId]), or null if unavailable. */ val currentUserId: ID? @@ -179,24 +182,24 @@ class TippingCoordinator @Inject constructor( setSendState(LoadingSuccessState(success = true)) delay(400.milliseconds) setSendState(LoadingSuccessState()) -// analytics.transfer( -// event = Analytics.Transfer.SentCash, -// amount = verifiedFiat.localFiat, -// successful = true, -// ) + analytics.transfer( + event = Analytics.Transfer.SentTip, + amount = verifiedFiat.localFiat, + successful = true, + ) // Hand off to the tipped user's chat via the tips flow, so backing out of the // chat lands on the tips list. canonicalChatId?.let { _events.emit(TipEvent.LaunchChat(ChatIdentifier.ByChatId(it))) } - }.onFailure { + }.onFailure { cause -> setSendState(LoadingSuccessState()) -// analytics.transfer( -// event = Analytics.Transfer.SentCash, -// amount = verifiedFiat.localFiat, -// error = cause, -// ) + analytics.transfer( + event = Analytics.Transfer.SentTip, + amount = verifiedFiat.localFiat, + error = cause, + ) BottomBarManager.showError( title = resources.getString(R.string.error_title_cashFailedToSend), message = resources.getString(R.string.error_description_cashFailedToSend),