diff --git a/apps/flipcash/features/currency-creator/build.gradle.kts b/apps/flipcash/features/currency-creator/build.gradle.kts index 8730cd6264..ee7dbb4e71 100644 --- a/apps/flipcash/features/currency-creator/build.gradle.kts +++ b/apps/flipcash/features/currency-creator/build.gradle.kts @@ -7,6 +7,7 @@ android { } dependencies { + implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:features:bill-customization")) implementation(project(":apps:flipcash:features:tokens")) implementation(project(":apps:flipcash:shared:bills")) @@ -21,4 +22,9 @@ dependencies { implementation(project(":services:flipcash")) implementation(project(":services:opencode")) + + testImplementation(kotlin("test")) + testImplementation(libs.bundles.unit.testing) + testImplementation(project(":apps:flipcash:shared:analytics:test-fixtures")) + testImplementation(project(":ui:resources:test-fixtures")) } diff --git a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt index b084f05f61..91a8d44b0f 100644 --- a/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt +++ b/apps/flipcash/features/currency-creator/src/main/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorViewModel.kt @@ -4,6 +4,11 @@ import android.net.Uri import androidx.compose.foundation.text.input.TextFieldState import androidx.core.text.trimmedLength import androidx.lifecycle.viewModelScope +import com.flipcash.analytics.LaunchMethod +import com.flipcash.analytics.State as AnalyticsState +import com.flipcash.analytics.events.CurrencyLaunchEvents +import com.flipcash.app.analytics.FlipcashAnalytics +import com.flipcash.app.analytics.analytics import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.data.Loadable import com.flipcash.app.core.extensions.flatMapResult @@ -94,6 +99,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( val purchaseMethodController: PurchaseMethodController, private val currencyCreatorCoordinator: CurrencyCreatorCoordinator, private val exchange: Exchange, + private val analytics: FlipcashAnalytics, ) : BaseViewModel( initialState = State(), updateStateForEvent = updateStateForEvent, @@ -616,6 +622,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( if (fast.isSuccess) { val completed = finalizeLaunch(mint) + trackLaunch(mint, success = completed) if (completed) { delay(InlineCompletion.SUCCESS_HOLD) dispatchEvent(Event.LaunchFinished) @@ -628,6 +635,7 @@ internal class CurrencyCreatorViewModel @Inject constructor( } if (fast.exceptionOrNull() !is BalancePollError.Timeout) { + trackLaunch(mint, success = false, error = fast.exceptionOrNull()) dispatchEvent(Event.UpdateProcessingState()) showLaunchFailed() return @@ -642,11 +650,34 @@ internal class CurrencyCreatorViewModel @Inject constructor( interval = InlineCompletion.SLOW_POLL_INTERVAL, ) if (slow.isSuccess && finalizeLaunch(mint)) { + trackLaunch(mint, success = true) return } + trackLaunch(mint, success = false, error = slow.exceptionOrNull()) dispatchEvent(Event.UpdateProcessingState(error = true)) } + /** + * Reports how the launch ended, once it is settled one way or the other. Reserves is a launch + * funded with USDF, Currency one funded with another held currency. The cost is reported in + * the user's currency, as iOS does. + */ + private fun trackLaunch(launched: Mint, success: Boolean, error: Throwable? = null) { + val paymentMint = stateFlow.value.fundingMint ?: return + val cost = stateFlow.value.totalCost.convertingTo(exchange.preferredRate) + analytics.track( + CurrencyLaunchEvents.launched( + method = if (paymentMint == Mint.usdf) LaunchMethod.RESERVES else LaunchMethod.CURRENCY, + state = if (success) AnalyticsState.SUCCESS else AnalyticsState.FAILURE, + mint = launched.analytics, + paymentMint = paymentMint.analytics, + fiat = cost.decimalValue, + currency = cost.currencyCode.name, + error = error?.analytics, + ) + ) + } + /** Resolves the new token and its launch bill, then marks the launch successful. */ private suspend fun finalizeLaunch(mint: Mint): Boolean { val metadata = tokenCoordinator.getTokenMetadata(mint).getOrNull() ?: return false diff --git a/apps/flipcash/features/currency-creator/src/test/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorLaunchAnalyticsTest.kt b/apps/flipcash/features/currency-creator/src/test/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorLaunchAnalyticsTest.kt new file mode 100644 index 0000000000..24ee4b4585 --- /dev/null +++ b/apps/flipcash/features/currency-creator/src/test/kotlin/com/flipcash/app/currencycreator/internal/CurrencyCreatorLaunchAnalyticsTest.kt @@ -0,0 +1,115 @@ +package com.flipcash.app.currencycreator.internal + +import com.flipcash.analytics.LaunchMethod +import com.flipcash.analytics.State +import com.flipcash.analytics.events.CurrencyLaunchEvents +import com.flipcash.app.analytics.RecordingAnalytics +import com.flipcash.app.core.MainCoroutineRule +import com.flipcash.app.core.dispatchers.TestDispatchers +import com.flipcash.app.currencycreator.CurrencyCreatorCoordinator +import com.flipcash.app.funding.PurchaseMethodController +import com.flipcash.app.tokens.BalancePoller +import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.app.userflags.UserFlagsCoordinator +import com.flipcash.services.controllers.ModerationController +import com.flipcash.services.user.UserManager +import com.getcode.opencode.controllers.CurrencyController +import com.getcode.opencode.exchange.Exchange +import com.getcode.opencode.exchange.VerifiedFiatCalculator +import com.getcode.opencode.model.financial.CurrencyCode +import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.Rate +import com.getcode.solana.keys.Mint +import com.getcode.solana.keys.base58 +import com.getcode.util.resources.ContentReader +import com.getcode.util.resources.FakeResourceHelper +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class CurrencyCreatorLaunchAnalyticsTest { + + @get:Rule + var mainCoroutineRule = MainCoroutineRule(UnconfinedTestDispatcher()) + + private val userManager = mockk(relaxed = true) + private val tokenCoordinator = mockk(relaxed = true) + private val balancePoller = mockk(relaxed = true) + private val exchange = mockk(relaxed = true) + private val analytics = RecordingAnalytics() + + private fun mint(seed: Int) = Mint(List(32) { seed.toByte() }) + + private fun TestScope.launchFailing(fundedWith: Mint, launched: Mint, cause: Throwable) { + every { exchange.preferredRate } returns Rate.oneToOne + every { tokenCoordinator.balanceForToken(any()) } returns flowOf(Fiat.Zero) + coEvery { balancePoller.awaitBalanceChange(launched, any(), any(), any(), any()) } returns Result.failure(cause) + // No account cluster ends the LaunchToken pipeline early, which still records the funding mint. + every { userManager.accountCluster } returns null + + val vm = CurrencyCreatorViewModel( + dispatchers = TestDispatchers(testScheduler), + userManager = userManager, + userFlags = mockk(relaxed = true), + moderationController = mockk(relaxed = true), + currencyController = mockk(relaxed = true), + verifiedFiatCalculator = mockk(relaxed = true), + tokenCoordinator = tokenCoordinator, + balancePoller = balancePoller, + resources = FakeResourceHelper(), + contentReader = mockk(relaxed = true), + purchaseMethodController = mockk(relaxed = true), + currencyCreatorCoordinator = mockk(relaxed = true), + exchange = exchange, + analytics = analytics, + ) + vm.dispatchEvent(CurrencyCreatorViewModel.Event.LaunchToken(fundedWith)) + vm.dispatchEvent(CurrencyCreatorViewModel.Event.PurchaseSubmitted(mockk(relaxed = true), launched)) + advanceUntilIdle() + } + + @Test + fun `a launch funded with USDF tracks Currency Launch With Reserves on failure`() = + runTest(mainCoroutineRule.dispatcher) { + val launched = mint(1) + launchFailing(Mint.usdf, launched, RuntimeException("boom")) + + assertEquals( + listOf( + CurrencyLaunchEvents.launched( + LaunchMethod.RESERVES, State.FAILURE, launched.base58(), Mint.usdf.base58(), + 5.0, CurrencyCode.USD.name, "boom", + ) + ), + analytics.events, + ) + } + + @Test + fun `a launch funded with another currency tracks Currency Launch With Currency on failure`() = + runTest(mainCoroutineRule.dispatcher) { + val launched = mint(1) + val funding = mint(2) + launchFailing(funding, launched, RuntimeException("boom")) + + assertEquals( + listOf( + CurrencyLaunchEvents.launched( + LaunchMethod.CURRENCY, State.FAILURE, launched.base58(), funding.base58(), + 5.0, CurrencyCode.USD.name, "boom", + ) + ), + analytics.events, + ) + } +} diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt index 4242fc2530..bffb6bd18d 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/router/LoginViewModel.kt @@ -2,6 +2,7 @@ package com.flipcash.app.login.router import androidx.lifecycle.viewModelScope import com.flipcash.analytics.Button +import com.flipcash.analytics.events.AccountEvents import com.flipcash.analytics.events.ButtonEvents import com.flipcash.app.analytics.FlipcashAnalytics import com.flipcash.app.auth.AuthManager @@ -92,7 +93,10 @@ class LoginViewModel @Inject constructor( eventFlow .filterIsInstance() .filter { createInFlight.compareAndSet(false, true) } - .onEach { analytics.track(ButtonEvents.tapped(Button.CREATE_ACCOUNT)) } + .onEach { + analytics.track(ButtonEvents.tapped(Button.CREATE_ACCOUNT)) + analytics.track(AccountEvents.createAccount()) + } .onEach { try { authManager.createAccount() diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt index 6c89de23b3..aa7f80fe35 100644 --- a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt +++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelCreateAccountTest.kt @@ -2,6 +2,7 @@ package com.flipcash.app.login.router import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.flipcash.analytics.Button +import com.flipcash.analytics.events.AccountEvents import com.flipcash.analytics.events.ButtonEvents import com.flipcash.app.analytics.RecordingAnalytics import com.flipcash.app.auth.AuthManager @@ -92,7 +93,10 @@ class LoginViewModelCreateAccountTest { // The tap is tracked off the eventFlow pipeline, which runs on `dispatchers.Default` // rather than inline with dispatchEvent, so it needs a pump to have landed. runCurrent() - assertEquals(ButtonEvents.tapped(Button.CREATE_ACCOUNT), analytics.events.single()) + assertEquals( + listOf(ButtonEvents.tapped(Button.CREATE_ACCOUNT), AccountEvents.createAccount()), + analytics.events, + ) } @Test diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt index d47817f103..343ed9c5a9 100644 --- a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt +++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/router/LoginViewModelErrorTest.kt @@ -2,6 +2,7 @@ package com.flipcash.app.login.router import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.flipcash.analytics.Button +import com.flipcash.analytics.events.AccountEvents import com.flipcash.analytics.events.ButtonEvents import com.flipcash.app.analytics.RecordingAnalytics import com.flipcash.app.auth.AuthManager @@ -85,7 +86,10 @@ class LoginViewModelErrorTest { advanceUntilIdle() assertTrue(BottomBarManager.messages.value.any { it.title == "error_title_createAccountFailed" }) - assertEquals(ButtonEvents.tapped(Button.CREATE_ACCOUNT), analytics.events.single()) + assertEquals( + listOf(ButtonEvents.tapped(Button.CREATE_ACCOUNT), AccountEvents.createAccount()), + analytics.events, + ) } @Test diff --git a/apps/flipcash/features/menu/build.gradle.kts b/apps/flipcash/features/menu/build.gradle.kts index 8b8c8646a2..9e49d285e7 100644 --- a/apps/flipcash/features/menu/build.gradle.kts +++ b/apps/flipcash/features/menu/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(libs.bundles.haze) implementation(project(":apps:flipcash:shared:analytics")) - testImplementation(testFixtures(project(":apps:flipcash:shared:analytics"))) + testImplementation(project(":apps:flipcash:shared:analytics:test-fixtures")) implementation(project(":apps:flipcash:shared:appupdates")) implementation(project(":apps:flipcash:shared:authentication")) implementation(project(":apps:flipcash:shared:bills")) diff --git a/apps/flipcash/features/myaccount/build.gradle.kts b/apps/flipcash/features/myaccount/build.gradle.kts index 3bd972b867..c47d70e1db 100644 --- a/apps/flipcash/features/myaccount/build.gradle.kts +++ b/apps/flipcash/features/myaccount/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(libs.compose.paging) implementation(project(":apps:flipcash:shared:analytics")) - testImplementation(testFixtures(project(":apps:flipcash:shared:analytics"))) + testImplementation(project(":apps:flipcash:shared:analytics:test-fixtures")) implementation(project(":apps:flipcash:shared:appsettings")) implementation(project(":apps:flipcash:shared:appupdates")) implementation(project(":apps:flipcash:shared:authentication")) diff --git a/apps/flipcash/shared/authentication/build.gradle.kts b/apps/flipcash/shared/authentication/build.gradle.kts index 515100ebcc..34c8839dcb 100644 --- a/apps/flipcash/shared/authentication/build.gradle.kts +++ b/apps/flipcash/shared/authentication/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.kotlinx.coroutines.play.services) implementation(libs.androidx.datastore) + implementation(project(":apps:flipcash:shared:analytics")) implementation(project(":apps:flipcash:shared:appsettings")) implementation(project(":apps:flipcash:shared:chat")) implementation(project(":apps:flipcash:shared:contacts")) @@ -28,4 +29,5 @@ dependencies { testImplementation(libs.robolectric) testImplementation(libs.grpc.stub) testImplementation(project(":libs:coroutines:test-fixtures")) + testImplementation(project(":apps:flipcash:shared:analytics:test-fixtures")) } diff --git a/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt b/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt index 3e9cad3532..7621ffbe29 100644 --- a/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt +++ b/apps/flipcash/shared/authentication/src/main/kotlin/com/flipcash/app/auth/AuthManager.kt @@ -40,6 +40,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import com.flipcash.analytics.events.AccountEvents +import com.flipcash.app.analytics.FlipcashAnalytics import javax.inject.Inject import javax.inject.Singleton @@ -67,7 +69,7 @@ class AuthManager @Inject constructor( private val wrongKeyChats: WrongKeyChats, private val networkObserver: NetworkConnectivityListener, private val dispatchers: DispatcherProvider, -// private val analytics: AnalyticsService, + private val analytics: FlipcashAnalytics, ) : CoroutineScope by CoroutineScope(dispatchers.IO) { init { @@ -129,7 +131,10 @@ class AuthManager @Inject constructor( is LookupResult.ExistingAccountFound -> { val token = result.entropy softLogin(token) - .onSuccess { onInitialized() } + .onSuccess { + analytics.track(AccountEvents.autoLoginComplete()) + onInitialized() + } } // No account on this device: resolve to a terminal LoggedOut state so the diff --git a/apps/flipcash/shared/authentication/src/test/kotlin/com/flipcash/app/auth/AuthManagerTest.kt b/apps/flipcash/shared/authentication/src/test/kotlin/com/flipcash/app/auth/AuthManagerTest.kt index 451b86a3bd..de5746decb 100644 --- a/apps/flipcash/shared/authentication/src/test/kotlin/com/flipcash/app/auth/AuthManagerTest.kt +++ b/apps/flipcash/shared/authentication/src/test/kotlin/com/flipcash/app/auth/AuthManagerTest.kt @@ -1,6 +1,8 @@ package com.flipcash.app.auth import androidx.core.app.NotificationManagerCompat +import com.flipcash.analytics.events.AccountEvents +import com.flipcash.app.analytics.RecordingAnalytics import com.flipcash.app.appsettings.AppSettingsCoordinator import com.flipcash.app.auth.internal.accounts.AccountStore import com.flipcash.app.auth.internal.credentials.AccountMetadata @@ -80,6 +82,7 @@ class AuthManagerTest { private val networkConnectivityListener = mockk(relaxed = true) + private val analytics = RecordingAnalytics() private lateinit var authManager: AuthManager @Before @@ -130,6 +133,7 @@ class AuthManagerTest { wrongKeyChats = wrongKeyChats, dispatchers = dispatchers, networkObserver = networkConnectivityListener, + analytics = analytics, ) } @@ -566,6 +570,7 @@ class AuthManagerTest { coVerify { credentialManager.lookup() } verify { persistence.openDatabase(entropy) } + assertEquals(listOf(AccountEvents.autoLoginComplete()), analytics.events) } // The trusted-website list is device-level and must outlive Log Out and Switch Accounts, both of diff --git a/apps/flipcash/shared/tokens/src/androidHostTest/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt b/apps/flipcash/shared/tokens/src/androidHostTest/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt index 9be2f9977d..7516c00407 100644 --- a/apps/flipcash/shared/tokens/src/androidHostTest/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt +++ b/apps/flipcash/shared/tokens/src/androidHostTest/kotlin/com/flipcash/app/tokens/ui/SwapViewModelErrorTest.kt @@ -213,6 +213,37 @@ class SwapViewModelErrorTest { ) } + @Test + fun `a buy paid with a held currency tracks Token Purchase With Currency`() = runTest(mainCoroutineRule.dispatcher) { + dispatchers = TestDispatchers(testScheduler) + transactionController.stub { + onBlocking { swap(any(), any(), anyOrNull(), any(), any(), anyOrNull(), any(), anyOrNull()) } doReturn + Result.failure(RuntimeException("swap failed")) + } + + val target = mockk(relaxed = true) { every { address } returns Mint.usdf } + val targetWithBalance = mockk(relaxed = true) { + every { this@mockk.token } returns target + } + val fundingMint = Mint(ByteArray(32) { 3 }.toList()) + val funding = mockk(relaxed = true) { every { address } returns fundingMint } + val fundingWithBalance = mockk(relaxed = true) { + every { this@mockk.token } returns funding + } + val amount = VerifiedFiat(mockk(relaxed = true), null) + + val vm = createViewModel() + vm.dispatchEvent(SwapViewModel.Event.OnPurposeChanged(SwapPurpose.Buy(mockk(relaxed = true)))) + vm.dispatchEvent(SwapViewModel.Event.OnSelectedTokenChanged(targetWithBalance)) + vm.dispatchEvent(SwapViewModel.Event.OnFundingTokenResolved(fundingWithBalance)) + vm.dispatchEvent(SwapViewModel.Event.ProceedWithPurchase(amount)) + advanceUntilIdle() + + val result = analytics.events.single { it.name == "Token Purchase With Currency" } + assertEquals(PropertyValue.Text("swap failed"), result.properties["Error"]) + assertTrue(analytics.events.none { it.name == "Token Purchase With Reserves" }) + } + @Test fun `sell failure shows buySellFailed error`() = runTest(mainCoroutineRule.dispatcher) { dispatchers = TestDispatchers(testScheduler) diff --git a/apps/flipcash/shared/tokens/src/androidMain/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt b/apps/flipcash/shared/tokens/src/androidMain/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt index 5ee50ff105..3603908aae 100644 --- a/apps/flipcash/shared/tokens/src/androidMain/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt +++ b/apps/flipcash/shared/tokens/src/androidMain/kotlin/com/flipcash/app/tokens/ui/SwapViewModel.kt @@ -1886,7 +1886,11 @@ class SwapViewModel @Inject constructor( is SwapPurpose.Buy -> when (purpose.fundingSource) { FundingSource.Phantom -> AnalyticsPurchaseMethod.PHANTOM FundingSource.Coinbase -> AnalyticsPurchaseMethod.COINBASE - else -> AnalyticsPurchaseMethod.RESERVES + // A buy paid with a held currency other than USDF reserves, as iOS reports it. + else -> when (stateFlow.value.fundingMint) { + null, Mint.usdf -> AnalyticsPurchaseMethod.RESERVES + else -> AnalyticsPurchaseMethod.CURRENCY + } } else -> null diff --git a/kmp/shared-core/spm/Sources/SharedCoreKit/AnalyticsEvents.swift b/kmp/shared-core/spm/Sources/SharedCoreKit/AnalyticsEvents.swift index cf3c54da3a..8bd947c935 100644 --- a/kmp/shared-core/spm/Sources/SharedCoreKit/AnalyticsEvents.swift +++ b/kmp/shared-core/spm/Sources/SharedCoreKit/AnalyticsEvents.swift @@ -16,6 +16,7 @@ public typealias AddMoneyMethod = SharedCore.AddMoneyMethod public typealias DisplayNameSource = SharedCore.DisplayNameSource public typealias TokenInfoSource = SharedCore.TokenInfoSource public typealias PurchaseMethod = SharedCore.PurchaseMethod +public typealias LaunchMethod = SharedCore.LaunchMethod public typealias WalletProvider = SharedCore.WalletProvider public typealias CashLinkChoice = SharedCore.CashLinkChoice public typealias OnrampStep = SharedCore.OnrampStep @@ -41,6 +42,7 @@ public typealias OnrampEvents = SharedCore.OnrampEvents public typealias WalletEvents = SharedCore.WalletEvents public typealias TokenInfoEvents = SharedCore.TokenInfoEvents public typealias SwapEvents = SharedCore.SwapEvents +public typealias CurrencyLaunchEvents = SharedCore.CurrencyLaunchEvents public typealias ScanEvents = SharedCore.ScanEvents public typealias DeeplinkEvents = SharedCore.DeeplinkEvents public typealias DisplayNameEvents = SharedCore.DisplayNameEvents diff --git a/kmp/shared-core/spm/Tests/SharedCoreKitTests/AnalyticsEventsTests.swift b/kmp/shared-core/spm/Tests/SharedCoreKitTests/AnalyticsEventsTests.swift index b1b8294a13..8f50c49fae 100644 --- a/kmp/shared-core/spm/Tests/SharedCoreKitTests/AnalyticsEventsTests.swift +++ b/kmp/shared-core/spm/Tests/SharedCoreKitTests/AnalyticsEventsTests.swift @@ -32,8 +32,8 @@ import Testing /// A value added in Kotlin compiles on both sides and would be silently missing from any /// Swift switch that lists the cases; this is the check that it is not. - @Test func tokenInfoSourcesAreTheFiveTheSpecNames() { - #expect(TokenInfoSource.entries.map(\.value) == ["Deeplink", "Wallet", "Discovery", "Chat", "Chat Gate"]) + @Test func tokenInfoSourcesAreTheSpecNames() { + #expect(TokenInfoSource.entries.map(\.value) == ["Deeplink", "Wallet", "Discovery", "Chat", "Chat Gate", "Give", "Send"]) } @Test func buildsAGroupEventWithAnOptionalLeftOut() { diff --git a/libs/analytics-events/EVENTS.md b/libs/analytics-events/EVENTS.md index aa6ab2313f..6b7d957cb4 100644 --- a/libs/analytics-events/EVENTS.md +++ b/libs/analytics-events/EVENTS.md @@ -133,7 +133,7 @@ The gate first appeared for a chat. Sent once per visit to the chat. ### Group: Joined -JoinChat returned. +JoinChat returned. Neither app sends it for a join from an invite link. | Property | Type | Optional | Values | |---|---|---|---| @@ -360,7 +360,7 @@ Buying and selling a token. ### Token Purchase With {method} -Phantom and Coinbase are Android only; part 2 decides whether iOS sends them. +Phantom and Coinbase are Android only; part 2 decides whether iOS sends them. Currency is a buy paid with a held currency other than USDF reserves. `{method}` in the name is one of [PurchaseMethod](#purchasemethod). @@ -370,7 +370,7 @@ Phantom and Coinbase are Android only; part 2 decides whether iOS sends them. | amount | group | no | [amount](#amount) | | Error | text | yes | | -> **Drift:** (Reserves) iOS also sends State, Payment Mint and Payment Token Symbol, and sends only Fiat and Currency of the amount, with no USDC or Quarks. +> **Drift:** (Reserves and Currency) iOS also sends State, Payment Mint and Payment Token Symbol, and sends only Fiat and Currency of the amount, with no USDC or Quarks. ### Token Sell @@ -383,6 +383,23 @@ Phantom and Coinbase are Android only; part 2 decides whether iOS sends them. > **Drift:** iOS also sends State, and sends only Fiat and Currency of the amount, with no USDC, Quarks or Fee. +## Currency Launch + +### Currency Launch With {method} + +A currency launch finished, on success or failure. Mint is the launched currency, Payment Mint the token that paid the launch cost, and Fiat and Currency the launch cost in the user's currency. Reserves is a launch paid with USDF; Currency is one paid with another held currency. + +`{method}` in the name is one of [LaunchMethod](#launchmethod). + +| Property | Type | Optional | Values | +|---|---|---|---| +| State | list | no | [State](#state) | +| Mint | text | no | | +| Payment Mint | text | no | | +| Fiat | decimal | no | | +| Currency | text | no | | +| Error | text | yes | | + ## Scan The gallery scan path, and the tip card. @@ -401,7 +418,7 @@ No properties. | Zoom | decimal | no | | | Time | duration | no | | -> **Drift:** iOS sends this as Gallery Scan: Code Found, with Type {Kik, QR}, Tier as {wholeImage, quadrant, window}, Zoom, and Elapsed in seconds instead of Time in ms. +> **Drift:** iOS sends this as Gallery Scan: Code Found, with Type {Kik, QR}, Tier as {wholeImage, quadrant, window}, Zoom, and Elapsed in seconds instead of Time in ms. iOS will move to this event, and the Gallery Scan: Code Found series in Mixpanel ends on the iOS release that does. ### Gallery Scan: Failed @@ -412,7 +429,7 @@ No properties. | Time | duration | no | | | Exhausted | yes/no | no | | -> **Drift:** iOS sends this as Gallery Scan: Nothing Found, with State {Exhausted, Cancelled, Route Refused} and Elapsed in seconds instead of Time in ms and Exhausted. +> **Drift:** iOS sends this as Gallery Scan: Nothing Found, with State {Exhausted, Cancelled, Route Refused} and Elapsed in seconds instead of Time in ms and Exhausted. iOS will move to this event, and the Gallery Scan: Nothing Found series in Mixpanel ends on the iOS release that does. ### Tip Card Scanned @@ -440,12 +457,14 @@ Sent in 2 forms. |---|---|---|---| | Type | text | no | | -> **Drift:** iOS's Type vocabulary is {Login, CashLink, EmailVerification, TokenInfo, Chat, ChatSendCash, Tip, Username, Wallet, DiscoverCurrencies, "Sheet:"}, and iOS sends this for every link where Android sends it only on the gallery QR path. +> **Drift:** iOS's Type vocabulary is {Login, CashLink, EmailVerification, TokenInfo, Chat, ChatSendCash, ChatMessage, GroupInvite, Tip, Profile, Wallet, DiscoverCurrencies, "Sheet:"}. iOS sends this for every link that resolves to an action and for no other, where Android sends it only on the gallery QR path. | Property | Type | Optional | Values | |---|---|---|---| | Error | text | no | `Failed to parse deeplink => {url}` | +> **Drift:** iOS never sends this form: it reports a parse only for links that resolve to an action. + ### Deeplink: Routed | Property | Type | Optional | Values | @@ -453,7 +472,7 @@ Sent in 2 forms. | Type | text | no | | | Error | text | yes | | -> **Drift:** iOS never passes Error, and sends this for every kind where Android sends it only for a cash link. +> **Drift:** iOS never passes Error. It sends this for every kind except access-key and verify-email links, which never report a route, and only when a user is logged in; Android sends it only for a cash link. ## Display Name @@ -498,6 +517,20 @@ Android only; part 2 decides whether iOS sends it. iOS's Create Account fires at | Currency | text | no | | | Owner Public Key | text | no | | +### Create Account + +The user chose to create an account and registration began, before the server answers. + +No properties. + +> **Drift:** iOS also sends Owner Public Key, a wallet identifier that isn't part of the shared contract. iOS stops sending it when it moves to this event. + +### Auto-login complete + +A saved account on the device logged in at launch with no action from the user. + +No properties. + ### Entered Phone Number No properties. @@ -667,7 +700,7 @@ The `Provider` property on the Wallet events: the wallet app the flow goes throu ### TokenInfoSource -Where Token Info was opened from. Discovery, Chat and Chat Gate are new in part 1; Android previously sent Wallet for all three. iOS's Give and Send sources stay native until part 2. +Where Token Info was opened from. Discovery, Chat and Chat Gate are new in part 1; Android previously sent Wallet for all three. Give and Send are iOS's, from the Add More Cash prompt on the Give and Send amount screens; the event names iOS sends today are unchanged. | Value sent | Name in code | Drift | |---|---|---| @@ -676,6 +709,8 @@ Where Token Info was opened from. Discovery, Chat and Chat Gate are new in part | Discovery | `DISCOVERY` | | | Chat | `CHAT` | | | Chat Gate | `CHAT_GATE` | iOS doesn't send this source: its gate buy button pushes the buy screen and never opens Token Info. Group: Gate Funding Tapped covers that tap on both apps. | +| Give | `GIVE` | Android has no such flow: its Give screen sends Add Money: Opened with source Give Shortfall instead of opening Token Info. | +| Send | `SEND` | Android has no such flow: its Send screen has no Add More Cash prompt that opens Token Info. | ### MuteDuration @@ -699,11 +734,11 @@ The `Field` property on Group: Edited: what the edit changed. ### GroupInviteSheetSource -The `Source` property on Group: Invite Sheet Opened. Chat is Android's transcript CTA and iOS's head card. +The `Source` property on Group: Invite Sheet Opened. Both apps send Profile, from the group profile and the join requests screen. | Value sent | Name in code | Drift | |---|---|---| -| Chat | `CHAT` | | +| Chat | `CHAT` | Neither app sends this today. It was Android's transcript CTA and iOS's card at the head of chat history; Android dropped its CTA in #1707 and iOS removed its card in cb036f487. | | Profile | `PROFILE` | | ### GroupInviteMethod @@ -724,7 +759,7 @@ The `Source` property on Group: Invite Followed: what the group was opened from. |---|---|---| | Link | `LINK` | | | QR | `QR` | | -| Chat Card | `CHAT_CARD` | | +| Chat Card | `CHAT_CARD` | iOS sends this when a group card in a chat is tapped and its link names a chat other than the one on screen; a card whose link carries no chat id opens through the link path and sends Link. | ### GroupAccess @@ -753,6 +788,16 @@ How a token is paid for, sent in the event name `Token Purchase With {method}`. | Reserves | `RESERVES` | | | Phantom | `PHANTOM` | | | Coinbase | `COINBASE` | | +| Currency | `CURRENCY` | | + +### LaunchMethod + +How a currency launch is paid for, sent in the event name `Currency Launch With {method}`. + +| Value sent | Name in code | Drift | +|---|---|---| +| Reserves | `RESERVES` | | +| Currency | `CURRENCY` | | ### DisplayNameSource @@ -816,6 +861,8 @@ The button tapped, sent in the event name `Button: {button}`. | Buy With Coinbase | `BUY_WITH_COINBASE` | | | Buy With Other Wallet | `BUY_WITH_OTHER_WALLET` | | | Share Token Info | `SHARE_TOKEN_INFO` | | +| Buy With Currency | `BUY_WITH_CURRENCY` | iOS sends this when a buy is paid with a held currency other than USDF reserves. Android sends no Button event for that buy. | +| Give | `GIVE` | iOS sends this on Currency Info's Give button, which opens its send-to-person amount screen. Android's matching tile is labelled Send and sends no Button event. | ## People counters diff --git a/libs/analytics-events/events.toml b/libs/analytics-events/events.toml index 0dae85176d..54581db899 100644 --- a/libs/analytics-events/events.toml +++ b/libs/analytics-events/events.toml @@ -115,13 +115,15 @@ description = "The `Provider` property on the Wallet events: the wallet app the PHANTOM = "Phantom" [lists.TokenInfoSource] -description = "Where Token Info was opened from. Discovery, Chat and Chat Gate are new in part 1; Android previously sent Wallet for all three. iOS's Give and Send sources stay native until part 2." +description = "Where Token Info was opened from. Discovery, Chat and Chat Gate are new in part 1; Android previously sent Wallet for all three. Give and Send are iOS's, from the Add More Cash prompt on the Give and Send amount screens; the event names iOS sends today are unchanged." [lists.TokenInfoSource.values] DEEPLINK = "Deeplink" WALLET = "Wallet" DISCOVERY = "Discovery" CHAT = "Chat" CHAT_GATE = { value = "Chat Gate", drift = "iOS doesn't send this source: its gate buy button pushes the buy screen and never opens Token Info. Group: Gate Funding Tapped covers that tap on both apps." } +GIVE = { value = "Give", drift = "Android has no such flow: its Give screen sends Add Money: Opened with source Give Shortfall instead of opening Token Info." } +SEND = { value = "Send", drift = "Android has no such flow: its Send screen has no Add More Cash prompt that opens Token Info." } [lists.MuteDuration] description = "The `Duration` property on Chat Muted: the mute option picked. Both apps offer the same four." @@ -138,9 +140,9 @@ NAME = "Name" PICTURE = "Picture" [lists.GroupInviteSheetSource] -description = "The `Source` property on Group: Invite Sheet Opened. Chat is Android's transcript CTA and iOS's head card." +description = "The `Source` property on Group: Invite Sheet Opened. Both apps send Profile, from the group profile and the join requests screen." [lists.GroupInviteSheetSource.values] -CHAT = "Chat" +CHAT = { value = "Chat", drift = "Neither app sends this today. It was Android's transcript CTA and iOS's card at the head of chat history; Android dropped its CTA in #1707 and iOS removed its card in cb036f487." } PROFILE = "Profile" [lists.GroupInviteMethod] @@ -155,7 +157,7 @@ description = "The `Source` property on Group: Invite Followed: what the group w [lists.GroupInviteSource.values] LINK = "Link" QR = "QR" -CHAT_CARD = "Chat Card" +CHAT_CARD = { value = "Chat Card", drift = "iOS sends this when a group card in a chat is tapped and its link names a chat other than the one on screen; a card whose link carries no chat id opens through the link path and sends Link." } [lists.GroupAccess] description = "The `Access` property on Group: Gate Shown: whether the viewer can join as they are." @@ -175,6 +177,13 @@ description = "How a token is paid for, sent in the event name `Token Purchase W RESERVES = "Reserves" PHANTOM = "Phantom" COINBASE = "Coinbase" +CURRENCY = "Currency" + +[lists.LaunchMethod] +description = "How a currency launch is paid for, sent in the event name `Currency Launch With {method}`." +[lists.LaunchMethod.values] +RESERVES = "Reserves" +CURRENCY = "Currency" [lists.DisplayNameSource] description = "The `Source` property on the Display Name events: the screen the name was set on." @@ -223,6 +232,8 @@ BUY_WITH_PHANTOM = "Buy With Phantom" BUY_WITH_COINBASE = "Buy With Coinbase" BUY_WITH_OTHER_WALLET = "Buy With Other Wallet" SHARE_TOKEN_INFO = "Share Token Info" +BUY_WITH_CURRENCY = { value = "Buy With Currency", drift = "iOS sends this when a buy is paid with a held currency other than USDF reserves. Android sends no Button event for that buy." } +GIVE = { value = "Give", drift = "iOS sends this on Currency Info's Give button, which opens its send-to-person amount screen. Android's matching tile is labelled Send and sends no Button event." } # --------------------------------------------------------------------------------------- # Groups: blocks of properties several events send the same way. @@ -389,7 +400,7 @@ properties = [ { name = "Member Count", type = "count" }, { name = "Gated", type = "yes/no" }, ] -description = "JoinChat returned." +description = "JoinChat returned. Neither app sends it for a join from an invite link." [[event]] domain = "Group" @@ -577,8 +588,8 @@ properties = [ { group = "amount" }, { name = "Error", type = "text", optional = true }, ] -description = "Phantom and Coinbase are Android only; part 2 decides whether iOS sends them." -drift = "(Reserves) iOS also sends State, Payment Mint and Payment Token Symbol, and sends only Fiat and Currency of the amount, with no USDC or Quarks." +description = "Phantom and Coinbase are Android only; part 2 decides whether iOS sends them. Currency is a buy paid with a held currency other than USDF reserves." +drift = "(Reserves and Currency) iOS also sends State, Payment Mint and Payment Token Symbol, and sends only Fiat and Currency of the amount, with no USDC or Quarks." [[event]] domain = "Swap" @@ -592,6 +603,23 @@ properties = [ ] drift = "iOS also sends State, and sends only Fiat and Currency of the amount, with no USDC, Quarks or Fee." +# Currency launch + +[[event]] +domain = "Currency Launch" +name = "Currency Launch With {method}" +builder = "launched" +placeholders = { method = "LaunchMethod" } +properties = [ + { name = "State", type = "State" }, + { name = "Mint", type = "text" }, + { name = "Payment Mint", type = "text" }, + { name = "Fiat", type = "decimal" }, + { name = "Currency", type = "text" }, + { name = "Error", type = "text", optional = true }, +] +description = "A currency launch finished, on success or failure. Mint is the launched currency, Payment Mint the token that paid the launch cost, and Fiat and Currency the launch cost in the user's currency. Reserves is a launch paid with USDF; Currency is one paid with another held currency." + # Scan [[event]] @@ -609,7 +637,7 @@ properties = [ { name = "Time", type = "duration" }, ] description = "@param tier which rung of the crop ladder decoded, 1 to 3." -drift = "iOS sends this as Gallery Scan: Code Found, with Type {Kik, QR}, Tier as {wholeImage, quadrant, window}, Zoom, and Elapsed in seconds instead of Time in ms." +drift = "iOS sends this as Gallery Scan: Code Found, with Type {Kik, QR}, Tier as {wholeImage, quadrant, window}, Zoom, and Elapsed in seconds instead of Time in ms. iOS will move to this event, and the Gallery Scan: Code Found series in Mixpanel ends on the iOS release that does." [[event]] domain = "Scan" @@ -620,7 +648,7 @@ properties = [ { name = "Exhausted", type = "yes/no" }, ] description = "@param exhausted true when the budget ran out rather than the ladder ending." -drift = "iOS sends this as Gallery Scan: Nothing Found, with State {Exhausted, Cancelled, Route Refused} and Elapsed in seconds instead of Time in ms and Exhausted." +drift = "iOS sends this as Gallery Scan: Nothing Found, with State {Exhausted, Cancelled, Route Refused} and Elapsed in seconds instead of Time in ms and Exhausted. iOS will move to this event, and the Gallery Scan: Nothing Found series in Mixpanel ends on the iOS release that does." [[event]] domain = "Scan" @@ -642,13 +670,14 @@ domain = "Deeplink" name = "Deeplink: Parse" builder = "parsed" properties = [{ name = "Type", type = "text" }] -drift = "iOS's Type vocabulary is {Login, CashLink, EmailVerification, TokenInfo, Chat, ChatSendCash, Tip, Username, Wallet, DiscoverCurrencies, \"Sheet:\"}, and iOS sends this for every link where Android sends it only on the gallery QR path." +drift = "iOS's Type vocabulary is {Login, CashLink, EmailVerification, TokenInfo, Chat, ChatSendCash, ChatMessage, GroupInvite, Tip, Profile, Wallet, DiscoverCurrencies, \"Sheet:\"}. iOS sends this for every link that resolves to an action and for no other, where Android sends it only on the gallery QR path." [[event]] domain = "Deeplink" name = "Deeplink: Parse" builder = "parseFailed" properties = [{ name = "Error", format = "Failed to parse deeplink => {url}" }] +drift = "iOS never sends this form: it reports a parse only for links that resolve to an action." [[event]] domain = "Deeplink" @@ -657,7 +686,7 @@ properties = [ { name = "Type", type = "text" }, { name = "Error", type = "text", optional = true }, ] -drift = "iOS never passes Error, and sends this for every kind where Android sends it only for a cash link." +drift = "iOS never passes Error. It sends this for every kind except access-key and verify-email links, which never report a route, and only when a user is logged in; Android sends it only for a cash link." # Display name @@ -701,6 +730,18 @@ properties = [ ] description = "Android only; part 2 decides whether iOS sends it. iOS's Create Account fires at registration and is a different moment." +[[event]] +domain = "Account" +name = "Create Account" +description = "The user chose to create an account and registration began, before the server answers." +drift = "iOS also sends Owner Public Key, a wallet identifier that isn't part of the shared contract. iOS stops sending it when it moves to this event." + +[[event]] +domain = "Account" +name = "Auto-login complete" +builder = "autoLoginComplete" +description = "A saved account on the device logged in at launch with no action from the user." + [[event]] domain = "Account" name = "Entered Phone Number" diff --git a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/AccountEventsTest.kt b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/AccountEventsTest.kt index 1afc5a8151..f42e92c498 100644 --- a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/AccountEventsTest.kt +++ b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/AccountEventsTest.kt @@ -25,12 +25,16 @@ class AccountEventsTest { fun propertylessEvents() { assertEquals( listOf( + AnalyticsEvent("Create Account", emptyMap()), + AnalyticsEvent("Auto-login complete", emptyMap()), AnalyticsEvent("Entered Phone Number", emptyMap()), AnalyticsEvent("Verified Phone Number", emptyMap()), AnalyticsEvent("Linked Phone Number", emptyMap()), AnalyticsEvent("Complete Onboarding", emptyMap()), ), listOf( + AccountEvents.createAccount(), + AccountEvents.autoLoginComplete(), AccountEvents.enteredPhoneNumber(), AccountEvents.verifiedPhoneNumber(), AccountEvents.linkedPhoneNumber(), diff --git a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/ButtonEventsTest.kt b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/ButtonEventsTest.kt index 10595c14ef..c3fe4bf64a 100644 --- a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/ButtonEventsTest.kt +++ b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/ButtonEventsTest.kt @@ -22,6 +22,8 @@ class ButtonEventsTest { "Button: Buy With Coinbase", "Button: Buy With Other Wallet", "Button: Share Token Info", + "Button: Buy With Currency", + "Button: Give", ).map { AnalyticsEvent(it, emptyMap()) }, Button.entries.map(ButtonEvents::tapped), ) diff --git a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/CurrencyLaunchEventsTest.kt b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/CurrencyLaunchEventsTest.kt new file mode 100644 index 0000000000..07fded0b95 --- /dev/null +++ b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/CurrencyLaunchEventsTest.kt @@ -0,0 +1,43 @@ +package com.flipcash.analytics + +import com.flipcash.analytics.events.CurrencyLaunchEvents +import kotlin.test.Test +import kotlin.test.assertEquals + +class CurrencyLaunchEventsTest { + + private val properties = mapOf( + "State" to PropertyValue.Text("Success"), + "Mint" to PropertyValue.Text("mint"), + "Payment Mint" to PropertyValue.Text("payment"), + "Fiat" to PropertyValue.Number(25.0), + "Currency" to PropertyValue.Text("USD"), + ) + + @Test + fun launchIsNamedForItsMethod() { + assertEquals( + listOf( + AnalyticsEvent("Currency Launch With Reserves", properties), + AnalyticsEvent("Currency Launch With Currency", properties), + ), + listOf(LaunchMethod.RESERVES, LaunchMethod.CURRENCY).map { + CurrencyLaunchEvents.launched(it, State.SUCCESS, "mint", "payment", 25.0, "USD", null) + }, + ) + } + + @Test + fun launchCarriesTheErrorOnFailure() { + assertEquals( + AnalyticsEvent( + "Currency Launch With Currency", + properties + mapOf( + "State" to PropertyValue.Text("Failure"), + "Error" to PropertyValue.Text("e"), + ), + ), + CurrencyLaunchEvents.launched(LaunchMethod.CURRENCY, State.FAILURE, "mint", "payment", 25.0, "USD", "e"), + ) + } +} diff --git a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/SwapEventsTest.kt b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/SwapEventsTest.kt index 6d56dec442..497d7784b6 100644 --- a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/SwapEventsTest.kt +++ b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/SwapEventsTest.kt @@ -22,8 +22,14 @@ class SwapEventsTest { AnalyticsEvent("Token Purchase With Reserves", properties), AnalyticsEvent("Token Purchase With Phantom", properties), AnalyticsEvent("Token Purchase With Coinbase", properties), + AnalyticsEvent("Token Purchase With Currency", properties), ), - listOf(PurchaseMethod.RESERVES, PurchaseMethod.PHANTOM, PurchaseMethod.COINBASE) + listOf( + PurchaseMethod.RESERVES, + PurchaseMethod.PHANTOM, + PurchaseMethod.COINBASE, + PurchaseMethod.CURRENCY, + ) .map { SwapEvents.purchase(it, "mint", amount, null) }, ) } diff --git a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/TokenInfoEventsTest.kt b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/TokenInfoEventsTest.kt index 9ea7a76b07..c5edcc1baf 100644 --- a/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/TokenInfoEventsTest.kt +++ b/libs/analytics-events/src/commonTest/kotlin/com/flipcash/analytics/TokenInfoEventsTest.kt @@ -17,6 +17,8 @@ class TokenInfoEventsTest { AnalyticsEvent("Token Info: Opened From Discovery", mint), AnalyticsEvent("Token Info: Opened From Chat", mint), AnalyticsEvent("Token Info: Opened From Chat Gate", mint), + AnalyticsEvent("Token Info: Opened From Give", mint), + AnalyticsEvent("Token Info: Opened From Send", mint), ), listOf( TokenInfoSource.DEEPLINK, @@ -24,6 +26,8 @@ class TokenInfoEventsTest { TokenInfoSource.DISCOVERY, TokenInfoSource.CHAT, TokenInfoSource.CHAT_GATE, + TokenInfoSource.GIVE, + TokenInfoSource.SEND, ).map { TokenInfoEvents.opened(it, "mint") }, ) }