diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt index 33655744a..bcc885a99 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt @@ -1,6 +1,8 @@ package com.flipcash.app.balance.internal import androidx.lifecycle.viewModelScope +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController @@ -28,6 +30,7 @@ internal class BalanceViewModel @Inject constructor( dispatchers: DispatcherProvider, purchaseMethodController: PurchaseMethodController, featureFlags: FeatureFlagController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel( initialState = State(), updateStateForEvent = updateStateForEvent, @@ -68,6 +71,7 @@ internal class BalanceViewModel @Inject constructor( dispatchEvent(Event.OpenScreen(AppRoute.Token.Discovery)) return@mapNotNull null } + analytics.addMoneyOpened(Analytics.AddMoneySource.Balance) purchaseMethodController.presentDepositOptions(popToRoot = true) } .onEach { route -> dispatchEvent(Event.OpenScreen(route)) } .launchIn(viewModelScope) diff --git a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt index fe2a7bb3e..f0d29752d 100644 --- a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt +++ b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt @@ -1,6 +1,8 @@ package com.flipcash.app.cash.internal import androidx.lifecycle.viewModelScope +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.core.bill.Bill import com.flipcash.app.core.tokens.SwapPurpose @@ -24,6 +26,7 @@ import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.SendLimit import com.getcode.opencode.model.financial.TokenWithLocalizedBalance import com.getcode.opencode.model.financial.minus +import com.getcode.opencode.model.financial.usdf import com.getcode.solana.keys.Mint import com.getcode.util.resources.ResourceHelper import com.getcode.view.BaseViewModel @@ -55,6 +58,7 @@ internal class CashScreenViewModel @Inject constructor( private val verifiedFiatCalculator: VerifiedFiatCalculator, tokenCoordinator: TokenCoordinator, transactionController: TransactionOperations, + analytics: FlipcashAnalyticsService, dispatchers: DispatcherProvider, ) : BaseViewModel( initialState = State(), @@ -272,6 +276,9 @@ internal class CashScreenViewModel @Inject constructor( .onEach { shortfall -> // route directly to the swap amount screen, skipping token info val mint = stateFlow.value.selectedTokenAddress!! + if (mint == Mint.usdf) { + analytics.addMoneyOpened(Analytics.AddMoneySource.GiveShortfall) + } dispatchEvent( Event.OpenScreen( AppRoute.Token.Swap( diff --git a/apps/flipcash/features/cash/src/test/kotlin/com/flipcash/app/cash/internal/CashScreenViewModelTest.kt b/apps/flipcash/features/cash/src/test/kotlin/com/flipcash/app/cash/internal/CashScreenViewModelTest.kt index f3b2f3439..ae4e368d1 100644 --- a/apps/flipcash/features/cash/src/test/kotlin/com/flipcash/app/cash/internal/CashScreenViewModelTest.kt +++ b/apps/flipcash/features/cash/src/test/kotlin/com/flipcash/app/cash/internal/CashScreenViewModelTest.kt @@ -1,6 +1,7 @@ package com.flipcash.app.cash.internal import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.flipcash.app.analytics.StubFlipcashAnalytics import com.flipcash.app.core.MainCoroutineRule import com.flipcash.app.core.dispatchers.TestDispatchers import com.flipcash.app.tokens.TokenCoordinator @@ -78,6 +79,7 @@ class CashScreenViewModelTest { verifiedFiatCalculator = verifiedFiatCalculator, tokenCoordinator = tokenCoordinator, transactionController = transactionController, + analytics = StubFlipcashAnalytics(), dispatchers = dispatchers, ) } diff --git a/apps/flipcash/features/deposit/build.gradle.kts b/apps/flipcash/features/deposit/build.gradle.kts index d317e6443..208119edc 100644 --- a/apps/flipcash/features/deposit/build.gradle.kts +++ b/apps/flipcash/features/deposit/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(project(":libs:messaging")) + implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":services:flipcash")) diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt index aa4656e8a..f30eed5a6 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/internal/DepositViewModel.kt @@ -2,6 +2,7 @@ package com.flipcash.app.deposit.internal import android.content.ClipboardManager import androidx.lifecycle.viewModelScope +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.extensions.onResult import com.flipcash.app.core.extensions.setText import com.flipcash.features.deposit.R @@ -34,6 +35,7 @@ internal class DepositViewModel @Inject constructor( resources: ResourceHelper, dispatchers: DispatcherProvider, featureFlags: FeatureFlagController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel( initialState = State(), updateStateForEvent = updateStateForEvent, @@ -106,6 +108,9 @@ internal class DepositViewModel @Inject constructor( text = stateFlow.value.depositAddress, label = resources.getString(R.string.title_clipboardLabelDepositAddress), ) + stateFlow.value.selectedTokenAddress?.let { mint -> + analytics.addMoneyAddressCopied(mint) + } } .onEach { dispatchEvent(Event.SetCopied(true)) diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt index faf3ca8a5..75174694c 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt @@ -1,6 +1,8 @@ package com.flipcash.app.menu.internal import androidx.lifecycle.viewModelScope +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.auth.AuthManager import com.flipcash.app.core.AppRoute import com.flipcash.app.core.android.VersionInfo @@ -53,6 +55,7 @@ internal class MenuScreenViewModel @Inject constructor( dispatchers: DispatcherProvider, releaseStageProvider: ReleaseStageProvider, purchaseMethodController: PurchaseMethodController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel( initialState = State(), @@ -165,6 +168,7 @@ internal class MenuScreenViewModel @Inject constructor( eventFlow .filterIsInstance() .mapNotNull { + analytics.addMoneyOpened(Analytics.AddMoneySource.Menu) val depositFirstUx = featureFlags.get(FeatureFlag.AddMoneyUX) if (!depositFirstUx) { return@mapNotNull AppRoute.Transfers.Deposit() 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 2f0210ad3..1c9cc1c1f 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 @@ -535,6 +535,7 @@ internal class ChatViewModel @Inject constructor( eventFlow .filterIsInstance() .onEach { + analytics.addMoneyOpened(Analytics.AddMoneySource.Chat) purchaseMethodController.presentDepositOptions()?.let { route -> dispatchEvent(Event.OpenScreen(route)) } 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 14dd41af8..d5d539809 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 @@ -21,6 +21,12 @@ interface FlipcashAnalyticsService : AnalyticsService { fun openOnramp(source: Analytics.OnrampSource) fun onrampVerification(step: Analytics.OnrampVerificationStep) fun onrampPurchase(step: Analytics.OnrampPurchaseStep, amount: Fiat? = null) + fun addMoneyOpened(source: Analytics.AddMoneySource) + fun addMoneyMethodSelected(method: Analytics.AddMoneyMethod) + fun addMoneyAmountConfirmed(method: Analytics.AddMoneyMethod, amount: Fiat) + fun addMoneyPaymentInvoked(method: Analytics.AddMoneyMethod, amount: Fiat) + fun addMoneyAddressCopied(mint: Mint) + fun addMoney(method: Analytics.AddMoneyMethod, amount: Fiat? = null, successful: Boolean = true, error: Throwable? = null) fun connectWallet(provider: OnRampProvider.UsesDeeplinks) fun amountSelectedForWalletTransfer(provider: OnRampProvider.UsesDeeplinks, amount: Fiat) fun transactionSubmittedToWallet(provider: OnRampProvider.UsesDeeplinks) @@ -60,6 +66,8 @@ object Analytics { data object SentCash : Transfer } enum class OnrampSource { Settings, Balance, Give } + enum class AddMoneySource { Menu, GiveShortfall, BuyShortfall, Chat, Scanner, Balance } + enum class AddMoneyMethod { Coinbase, Phantom, OtherWallet, Reserves } enum class OnrampVerificationStep { ShowInfo, EnterPhone, ConfirmPhone, EnterEmail, ConfirmEmail } enum class OnrampPurchaseStep { PresetSelected, EnterCustomAmount, InvokePayment, InvokePaymentCustom, Completed } enum class TokenInfoSource { Deeplink, Wallet, Give } @@ -89,6 +97,13 @@ class StubFlipcashAnalytics : FlipcashAnalyticsService { override fun onrampVerification(step: Analytics.OnrampVerificationStep) = Unit override fun onrampPurchase(step: Analytics.OnrampPurchaseStep, amount: Fiat?) = Unit + override fun addMoneyOpened(source: Analytics.AddMoneySource) = Unit + override fun addMoneyMethodSelected(method: Analytics.AddMoneyMethod) = Unit + override fun addMoneyAmountConfirmed(method: Analytics.AddMoneyMethod, amount: Fiat) = Unit + override fun addMoneyPaymentInvoked(method: Analytics.AddMoneyMethod, amount: Fiat) = Unit + override fun addMoneyAddressCopied(mint: Mint) = Unit + override fun addMoney(method: Analytics.AddMoneyMethod, amount: Fiat?, successful: Boolean, error: Throwable?) = Unit + override fun connectWallet(provider: OnRampProvider.UsesDeeplinks) = Unit override fun amountSelectedForWalletTransfer(provider: OnRampProvider.UsesDeeplinks, amount: Fiat) = Unit override fun transactionSubmittedToWallet(provider: OnRampProvider.UsesDeeplinks) = 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 e7b02f98a..7cc9c4470 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 @@ -242,6 +242,50 @@ internal sealed interface AnalyticsEvent { } } + sealed interface AddMoneyEvent : AnalyticsEvent { + data class Opened(val source: Analytics.AddMoneySource) : AddMoneyEvent { + override val name = "Add Money: Opened" + override fun toProperties() = mapOf("Source" to source.propertyValue) + } + + data class MethodSelected(val method: Analytics.AddMoneyMethod) : AddMoneyEvent { + override val name = "Add Money: Method Selected" + override fun toProperties() = mapOf("Method" to method.propertyValue) + } + + data class AmountConfirmed( + val method: Analytics.AddMoneyMethod, + val amount: Fiat, + ) : AddMoneyEvent { + override val name = "Add Money: Amount Confirmed" + override fun toProperties() = buildMap { + put("Method", method.propertyValue) + putAll(amount.asProperties()) + } + } + + data class PaymentInvoked( + val method: Analytics.AddMoneyMethod, + val amount: Fiat, + ) : AddMoneyEvent { + override val name = "Add Money: Payment Invoked" + override fun toProperties() = buildMap { + put("Method", method.propertyValue) + putAll(amount.asProperties()) + } + } + + data class AddressCopied(val mint: Mint) : AddMoneyEvent { + override val name = "Add Money: Address Copied" + override fun toProperties() = mapOf("Mint" to mint.base58()) + } + + data class Terminal(val method: Analytics.AddMoneyMethod) : AddMoneyEvent { + override val name = "Add Money" + override fun toProperties() = mapOf("Method" to method.propertyValue) + } + } + sealed interface OpenTokenInfoEvent : AnalyticsEvent { val mint: Mint override fun toProperties() = mapOf("Mint" to mint.base58()) @@ -297,6 +341,24 @@ internal sealed interface AnalyticsEvent { } } +internal val Analytics.AddMoneySource.propertyValue: String + get() = when (this) { + Analytics.AddMoneySource.Menu -> "Menu" + Analytics.AddMoneySource.GiveShortfall -> "Give Shortfall" + Analytics.AddMoneySource.BuyShortfall -> "Buy Shortfall" + Analytics.AddMoneySource.Chat -> "Chat" + Analytics.AddMoneySource.Scanner -> "Scanner" + Analytics.AddMoneySource.Balance -> "Balance" + } + +internal val Analytics.AddMoneyMethod.propertyValue: String + get() = when (this) { + Analytics.AddMoneyMethod.Coinbase -> "Coinbase" + Analytics.AddMoneyMethod.Phantom -> "Phantom" + Analytics.AddMoneyMethod.OtherWallet -> "Other Wallet" + Analytics.AddMoneyMethod.Reserves -> "Reserves" + } + internal fun LocalFiat.asProperties(): Map { return buildMap { putAll(underlyingTokenAmount.asProperties()) 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 c9bd30b76..deb577524 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 @@ -140,6 +140,40 @@ internal class MixpanelAnalyticsDelegate @Inject constructor( track(event) } + override fun addMoneyOpened(source: Analytics.AddMoneySource) { + track(AnalyticsEvent.AddMoneyEvent.Opened(source)) + } + + override fun addMoneyMethodSelected(method: Analytics.AddMoneyMethod) { + track(AnalyticsEvent.AddMoneyEvent.MethodSelected(method)) + } + + override fun addMoneyAmountConfirmed(method: Analytics.AddMoneyMethod, amount: Fiat) { + track(AnalyticsEvent.AddMoneyEvent.AmountConfirmed(method, amount)) + } + + override fun addMoneyPaymentInvoked(method: Analytics.AddMoneyMethod, amount: Fiat) { + track(AnalyticsEvent.AddMoneyEvent.PaymentInvoked(method, amount)) + } + + override fun addMoneyAddressCopied(mint: Mint) { + track(AnalyticsEvent.AddMoneyEvent.AddressCopied(mint)) + } + + override fun addMoney( + method: Analytics.AddMoneyMethod, + amount: Fiat?, + successful: Boolean, + error: Throwable? + ) { + track( + AnalyticsEvent.AddMoneyEvent.Terminal(method), + "State" to if (successful) "Success" else "Failure", + *amount?.asProperties()?.toList()?.toTypedArray() ?: emptyArray(), + *error.asProperty() + ) + } + override fun connectWallet(provider: OnRampProvider.UsesDeeplinks) { track(AnalyticsEvent.WalletConnect(provider)) } diff --git a/apps/flipcash/shared/payments/build.gradle.kts b/apps/flipcash/shared/payments/build.gradle.kts index b201ce60b..2fb1f6948 100644 --- a/apps/flipcash/shared/payments/build.gradle.kts +++ b/apps/flipcash/shared/payments/build.gradle.kts @@ -7,6 +7,7 @@ android { } dependencies { + implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:tokens:core")) implementation(project(":apps:flipcash:shared:userflags")) diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt index 53789af67..8bd02c72e 100644 --- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt +++ b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/internal/InternalPurchaseMethodController.kt @@ -1,5 +1,7 @@ package com.flipcash.app.payments.internal +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.core.tokens.FundingSource import com.flipcash.app.core.tokens.SwapPurpose @@ -54,6 +56,7 @@ class InternalPurchaseMethodController @Inject constructor( exchange: Exchange, private val resources: ResourceHelper, private val userManager: UserManager, + private val analytics: FlipcashAnalyticsService, ) : PurchaseMethodController { private val scope = CoroutineScope(SupervisorJob()) @@ -148,6 +151,18 @@ class InternalPurchaseMethodController @Inject constructor( _dismissals.map { null }, ).first() + when (result) { + PurchaseMethod.CoinbaseOnRamp -> + analytics.addMoneyMethodSelected(Analytics.AddMoneyMethod.Coinbase) + PurchaseMethod.PhantomWallet -> + analytics.addMoneyMethodSelected(Analytics.AddMoneyMethod.Phantom) + PurchaseMethod.OtherWallet -> + analytics.addMoneyMethodSelected(Analytics.AddMoneyMethod.OtherWallet) + is PurchaseMethod.CashReserves -> + analytics.addMoneyMethodSelected(Analytics.AddMoneyMethod.Reserves) + null -> Unit + } + return when (result) { PurchaseMethod.CoinbaseOnRamp -> { val profile = userManager.profile diff --git a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt index 01479b215..c7738f056 100644 --- a/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt +++ b/apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/delegates/DepositDelegate.kt @@ -1,5 +1,7 @@ package com.flipcash.app.session.internal.delegates +import com.flipcash.app.analytics.Analytics +import com.flipcash.app.analytics.FlipcashAnalyticsService import com.flipcash.app.core.AppRoute import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController @@ -38,6 +40,7 @@ class DepositDelegate @Inject constructor( private val usdcSweep: UsdcDepositSweep, private val userManager: UserManager, private val resources: ResourceHelper, + private val analytics: FlipcashAnalyticsService, dispatchers: DispatcherProvider, featureFlagController: FeatureFlagController, ) : DepositOperations { @@ -77,6 +80,8 @@ class DepositDelegate @Inject constructor( text = resources.getString(R.string.action_addMoney) ) { scope.launch { + // the scanner's Give action is the only session caller today + analytics.addMoneyOpened(Analytics.AddMoneySource.Scanner) val destination = purchaseMethodController.presentDepositOptions(popToRoot = true) if (destination != null) { onRoute?.invoke(destination) diff --git a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt index d98d81b59..733ef64e1 100644 --- a/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt +++ b/apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt @@ -320,6 +320,20 @@ class SwapViewModel @Inject constructor( data class OpenScreen(val screen: AppRoute) : Event } + // "Add Money" funnel events only apply when acquiring USDF via an external + // funding source; token buys funded from reserves or launchpad purchases are + // tracked separately via buy()/sell(). + private val addMoneyMethod: Analytics.AddMoneyMethod? + get() { + val purpose = stateFlow.value.purpose as? SwapPurpose.Buy ?: return null + if (purpose.mint != Mint.usdf) return null + return when (purpose.fundingSource) { + FundingSource.Coinbase -> Analytics.AddMoneyMethod.Coinbase + FundingSource.Phantom -> Analytics.AddMoneyMethod.Phantom + FundingSource.Flexible -> null + } + } + private val enteredAmount: Fiat get() { val delegateState = amountDelegate.state.value @@ -556,6 +570,15 @@ class SwapViewModel @Inject constructor( .onEach { (delegateState, purpose) -> val mustAddMoney = featureFlags.get(FeatureFlag.AddMoneyUX) val isAddingMoney = stateFlow.value.isAddingMoney + addMoneyMethod?.let { method -> + analytics.addMoneyAmountConfirmed( + method = method, + amount = Fiat( + delegateState.enteredAmount, + delegateState.currency.code ?: CurrencyCode.USD, + ), + ) + } when (purpose) { is SwapPurpose.Buy -> { val rate = exchange.preferredRate @@ -722,12 +745,28 @@ class SwapViewModel @Inject constructor( is OrderDeliveryResult.Delivered -> true OrderDeliveryResult.Failed -> { dispatchEvent(Event.UpdateProcessingState(loading = false, error = true)) + addMoneyMethod?.let { method -> + analytics.addMoney( + method = method, + amount = netTransferAmount, + successful = false, + error = IllegalStateException("Order delivery failed"), + ) + } false } // The deposit may still land later; the session's foreground sweep // will reconcile it. Don't claim success we can't confirm. OrderDeliveryResult.TimedOut -> { dispatchEvent(Event.UpdateProcessingState(loading = false, error = true)) + addMoneyMethod?.let { method -> + analytics.addMoney( + method = method, + amount = netTransferAmount, + successful = false, + error = IllegalStateException("Order delivery timed out"), + ) + } false } } @@ -743,6 +782,9 @@ class SwapViewModel @Inject constructor( tokenCoordinator.balanceForToken(Mint.usdf).first { it > baseline } feedCoordinator.fetchSinceLatest() dispatchEvent(Event.UpdateProcessingState(loading = false, success = true)) + addMoneyMethod?.let { method -> + analytics.addMoney(method, netTransferAmount) + } }.launchIn(viewModelScope) eventFlow @@ -898,7 +940,13 @@ class SwapViewModel @Inject constructor( coinbaseOnRampController.state .onEach { s -> when (s) { - is CoinbaseOnRampState.Failed, + is CoinbaseOnRampState.Failed -> { + addMoneyMethod?.let { method -> + analytics.addMoney(method, successful = false, error = s.error) + } + dispatchEvent(Event.UpdateBuyState()) + } + CoinbaseOnRampState.Idle -> dispatchEvent(Event.UpdateBuyState()) is CoinbaseOnRampState.Completed, @@ -990,6 +1038,7 @@ class SwapViewModel @Inject constructor( eventFlow .filterIsInstance() .mapNotNull { + analytics.addMoneyOpened(Analytics.AddMoneySource.BuyShortfall) if (!featureFlags.get(FeatureFlag.AddMoneyUX)) { return@mapNotNull AppRoute.Transfers.Deposit(showOtherOptions = false) } @@ -1071,6 +1120,9 @@ class SwapViewModel @Inject constructor( verifiedFiat = amountFiat, ).onSuccess { trackTransaction(token) + addMoneyMethod?.let { method -> + analytics.addMoneyPaymentInvoked(method, amountFiat.localFiat.nativeAmount) + } }.onFailure { error -> dispatchEvent(Event.UpdateBuyState()) when (error) { @@ -1082,6 +1134,14 @@ class SwapViewModel @Inject constructor( else -> { trackTransaction(token, error = error) + addMoneyMethod?.let { method -> + analytics.addMoney( + method = method, + amount = amountFiat.localFiat.nativeAmount, + successful = false, + error = error, + ) + } BottomBarManager.showError( title = resources.getString(R.string.error_title_buySellFailed), message = resources.getString(R.string.error_description_buySellFailed), @@ -1154,6 +1214,9 @@ class SwapViewModel @Inject constructor( OnRampProvider.Phantom, amount.localFiat.underlyingTokenAmount ) + addMoneyMethod?.let { method -> + analytics.addMoneyPaymentInvoked(method, amount.localFiat.nativeAmount) + } }, ).onSuccess { result -> when (result) { @@ -1180,8 +1243,13 @@ class SwapViewModel @Inject constructor( if (deeplinkError is DeeplinkOnRampError.WalletProvidedError && deeplinkError.code == DeeplinkError.UserRejectedRequest.code) { analytics.walletTransactionCancelled(OnRampProvider.Phantom) - } else if (deeplinkError is DeeplinkOnRampError.FailedToSendTransaction) { - analytics.walletTransactionFailed(OnRampProvider.Phantom) + } else { + if (deeplinkError is DeeplinkOnRampError.FailedToSendTransaction) { + analytics.walletTransactionFailed(OnRampProvider.Phantom) + } + addMoneyMethod?.let { method -> + analytics.addMoney(method, successful = false, error = deeplinkError) + } } trace(