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 ca2bb27fa..9c17a21ff 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 @@ -6,6 +6,9 @@ import com.flipcash.app.core.transfers.TransferDirection import com.flipcash.app.onramp.ConfirmationEvent import com.flipcash.app.onramp.OnRampAmount import com.flipcash.app.onramp.OnRampAmountController +import com.flipcash.features.balance.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.internal.model.thirdparty.OnRampType import com.flipcash.services.user.AuthState @@ -26,6 +29,7 @@ import javax.inject.Inject internal class BalanceViewModel @Inject constructor( userManager: UserManager, onrampController: OnRampAmountController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel2( initialState = State(), updateStateForEvent = updateStateForEvent @@ -58,6 +62,7 @@ internal class BalanceViewModel @Inject constructor( eventFlow .filterIsInstance() .onEach { + analytics.openOnramp(AnalyticsEvent.OnRampOpenEvent.Balance) val provider = stateFlow.value.preferredOnRampProvider if (provider is OnRampProvider.Coinbase && provider.type == OnRampType.Virtual) { // has coinbase provider supporting google pay - pop selection for quick add 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 bdcd053c1..ee2dc8cce 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 @@ -8,6 +8,8 @@ import com.flipcash.app.onramp.ConfirmationEvent import com.flipcash.app.onramp.OnRampAmount import com.flipcash.app.onramp.OnRampAmountController import com.flipcash.features.cash.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.internal.model.thirdparty.OnRampType import com.getcode.manager.BottomBarAction @@ -55,6 +57,7 @@ internal class CashScreenViewModel @Inject constructor( tokenController: TokenController, transactionController: TransactionController, onrampController: OnRampAmountController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel2( initialState = State(), updateStateForEvent = updateStateForEvent @@ -320,6 +323,7 @@ internal class CashScreenViewModel @Inject constructor( .filterIsInstance() .map { it.amount } .onEach { amount -> + analytics.openOnramp(AnalyticsEvent.OnRampOpenEvent.Give) val provider = stateFlow.value.preferredOnRampProvider if (provider is OnRampProvider.Coinbase && provider.type == OnRampType.Virtual) { // has coinbase provider supporting google pay - pop selection for quick add diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/VerificationFlowScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/VerificationFlowScreen.kt index 55fdb2ead..64254d37c 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/VerificationFlowScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/VerificationFlowScreen.kt @@ -24,6 +24,9 @@ import com.flipcash.app.navigation.FlowNavigator import com.flipcash.app.navigation.LocalFlowNavigator import com.flipcash.app.navigation.NavigationFlowStep import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.manager.BottomBarAction import com.getcode.manager.BottomBarManager import com.getcode.navigation.core.CodeNavigator diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailMagicLinkScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailMagicLinkScreen.kt index f828a53c3..fd84ef3b9 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailMagicLinkScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailMagicLinkScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -21,6 +23,9 @@ import com.flipcash.app.core.android.IntentUtils import com.flipcash.app.navigation.FlowNavigator import com.flipcash.app.navigation.LocalFlowNavigator import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.extensions.getStackScopedViewModel import com.getcode.navigation.screens.NamedScreen import com.getcode.ui.components.AppBarWithTitle @@ -42,6 +47,7 @@ class EmailMagicLinkScreen( override val name: String @Composable get() = stringResource(R.string.title_verifyEmailAddress) + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val flowNavigator = LocalFlowNavigator.current as FlowNavigator @@ -52,6 +58,11 @@ class EmailMagicLinkScreen( flowNavigator.exit(false) } + val analytics = LocalAnalytics.current as FlipcashAnalyticsService + LifecycleEffectOnce { + analytics.onrampVerification(AnalyticsEvent.OnRampVerificationEvent.ConfirmEmail) + } + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailVerificationScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailVerificationScreen.kt index 1361d0c23..368a47004 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailVerificationScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/email/EmailVerificationScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -18,6 +20,9 @@ import com.flipcash.app.contact.verification.EmailVerificationFlow import com.flipcash.app.contact.verification.internal.email.EmailEntryScreen import com.flipcash.app.contact.verification.internal.email.EmailVerificationViewModel import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.extensions.getStackScopedViewModel import com.getcode.navigation.screens.NamedScreen @@ -38,6 +43,7 @@ class EmailVerificationScreen : Screen, NamedScreen, Parcelable { override val name: String @Composable get() = stringResource(R.string.title_verifyEmailAddress) + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val codeNavigator = LocalCodeNavigator.current @@ -62,6 +68,11 @@ class EmailVerificationScreen : Screen, NamedScreen, Parcelable { EmailEntryScreen(viewModel) } + val analytics = LocalAnalytics.current as FlipcashAnalyticsService + LifecycleEffectOnce { + analytics.onrampVerification(AnalyticsEvent.OnRampVerificationEvent.EnterEmail) + } + BackHandler { keyboard.hideIfVisible { codeNavigator.pop() diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/VerificationFlowIntroScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/VerificationFlowIntroScreen.kt index 8b7002a12..1bf6f6ae0 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/VerificationFlowIntroScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/internal/VerificationFlowIntroScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -31,6 +33,9 @@ import com.flipcash.app.navigation.FlowNavigator import com.flipcash.app.navigation.LocalFlowNavigator import com.flipcash.app.theme.FlipcashDesignSystem import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.theme.CodeTheme import com.getcode.ui.theme.ButtonState @@ -48,6 +53,7 @@ class VerificationFlowIntroScreen( @IgnoredOnParcel override val key: ScreenKey = uniqueScreenKey + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val codeNavigator = LocalCodeNavigator.current @@ -59,6 +65,11 @@ class VerificationFlowIntroScreen( onClick = { flowNavigator.continueFlowFrom(VerificationFlowStep.Intro) }, ) + val analytics = LocalAnalytics.current as FlipcashAnalyticsService + LifecycleEffectOnce { + analytics.onrampVerification(AnalyticsEvent.OnRampVerificationEvent.ShowInfo) + } + BackHandler { keyboard.hideIfVisible { codeNavigator.pop() diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneCodeScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneCodeScreen.kt index 6517e54a1..1a9b99f12 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneCodeScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneCodeScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -19,6 +21,9 @@ import com.flipcash.app.contact.verification.internal.phone.PhoneVerificationVie import com.flipcash.app.navigation.FlowNavigator import com.flipcash.app.navigation.LocalFlowNavigator import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.extensions.getStackScopedViewModel import com.getcode.navigation.screens.NamedScreen import com.getcode.ui.components.AppBarWithTitle @@ -37,6 +42,7 @@ class PhoneCodeScreen: Screen, NamedScreen, Parcelable { override val name: String @Composable get() = stringResource(R.string.title_enterTheCode) + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val flowNavigator = LocalFlowNavigator.current as FlowNavigator @@ -57,6 +63,11 @@ class PhoneCodeScreen: Screen, NamedScreen, Parcelable { PhoneCodeScreen(viewModel) } + val analytics = LocalAnalytics.current as FlipcashAnalyticsService + LifecycleEffectOnce { + analytics.onrampVerification(AnalyticsEvent.OnRampVerificationEvent.ConfirmPhone) + } + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() diff --git a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneVerificationScreen.kt b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneVerificationScreen.kt index 7d6b58b3e..9e831f843 100644 --- a/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneVerificationScreen.kt +++ b/apps/flipcash/features/contact-verification/src/main/kotlin/com/flipcash/app/contact/verification/phone/PhoneVerificationScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.lifecycle.LifecycleEffectOnce import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey @@ -18,6 +20,9 @@ import com.flipcash.app.contact.verification.PhoneVerificationFlow import com.flipcash.app.contact.verification.internal.phone.PhoneEntryScreen import com.flipcash.app.contact.verification.internal.phone.PhoneVerificationViewModel import com.flipcash.features.contact.verification.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.extensions.getStackScopedViewModel import com.getcode.navigation.screens.NamedScreen @@ -38,6 +43,7 @@ class PhoneVerificationScreen : Screen, NamedScreen, Parcelable { override val name: String @Composable get() = stringResource(R.string.title_verifyPhoneNumber) + @OptIn(ExperimentalVoyagerApi::class) @Composable override fun Content() { val codeNavigator = LocalCodeNavigator.current @@ -63,6 +69,11 @@ class PhoneVerificationScreen : Screen, NamedScreen, Parcelable { PhoneEntryScreen(viewModel) } + val analytics = LocalAnalytics.current as FlipcashAnalyticsService + LifecycleEffectOnce { + analytics.onrampVerification(AnalyticsEvent.OnRampVerificationEvent.EnterPhone) + } + BackHandler { keyboard.hideIfVisible { codeNavigator.pop() 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 1bc6a8ded..f6c2c7358 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 @@ -13,6 +13,8 @@ import com.flipcash.app.onramp.ConfirmationEvent import com.flipcash.app.onramp.OnRampAmount import com.flipcash.app.onramp.OnRampAmountController import com.flipcash.features.menu.R +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.internal.model.thirdparty.OnRampType import com.flipcash.services.user.AuthState @@ -54,6 +56,7 @@ internal class MenuScreenViewModel @Inject constructor( mnemonicManager: MnemonicManager, featureFlags: FeatureFlagController, onrampController: OnRampAmountController, + analytics: FlipcashAnalyticsService, ) : BaseViewModel2( initialState = State(), @@ -126,6 +129,7 @@ internal class MenuScreenViewModel @Inject constructor( eventFlow .filterIsInstance() .onEach { + analytics.openOnramp(AnalyticsEvent.OnRampOpenEvent.Settings) val provider = stateFlow.value.preferredOnRampProvider if (provider is OnRampProvider.Coinbase && provider.type == OnRampType.Virtual) { // has coinbase provider supporting google pay - pop selection for quick add diff --git a/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/screens/OnRampProviderListScreenContent.kt b/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/screens/OnRampProviderListScreenContent.kt index 09fddbbd2..dfdcc579d 100644 --- a/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/screens/OnRampProviderListScreenContent.kt +++ b/apps/flipcash/features/onramp/src/main/kotlin/com/flipcash/app/onramp/internal/screens/OnRampProviderListScreenContent.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -63,7 +62,7 @@ private fun OnRampProviderListScreenContent( item { Column( modifier = Modifier.fillParentMaxWidth(), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x7), + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x5), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(CodeTheme.dimens.grid.x4)) @@ -72,11 +71,11 @@ private fun OnRampProviderListScreenContent( contentDescription = null ) Text( + modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x3), text = stringResource(R.string.subtitle_addUsdcToWallet), style = CodeTheme.typography.textMedium, color = CodeTheme.colors.textSecondary, ) - Spacer(Modifier.height(CodeTheme.dimens.grid.x1)) } } items(state.providers) { provider -> @@ -85,7 +84,6 @@ private fun OnRampProviderListScreenContent( modifier = Modifier.fillMaxWidth(), onClick = { dispatchEvent(OnRampViewModel.Event.OnProviderSelected(provider)) }, ) - Divider(color = CodeTheme.colors.divider) } } } diff --git a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/OnRampAmountSelectionModal.kt b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/OnRampAmountSelectionModal.kt index 366d8d1fe..fe4dee782 100644 --- a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/OnRampAmountSelectionModal.kt +++ b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/OnRampAmountSelectionModal.kt @@ -27,17 +27,17 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.flipcash.app.onramp.internal.InternalOnRampAmountController import com.flipcash.app.theme.FlipcashDesignSystem +import com.flipcash.services.analytics.StubFlipcashAnalytics import com.flipcash.services.internal.model.thirdparty.OnRampProvider -import com.flipcash.services.internal.model.thirdparty.OnRampType +import com.flipcash.shared.onramp.common.R import com.getcode.opencode.model.financial.Fiat import com.getcode.opencode.model.financial.toFiat import com.getcode.theme.CodeTheme import com.getcode.theme.extraSmall import com.getcode.ui.components.AppBarWithTitle +import com.getcode.ui.components.Modal import com.getcode.ui.theme.ButtonState import com.getcode.ui.theme.CodeButton -import com.flipcash.shared.onramp.common.R -import com.getcode.ui.components.Modal private val amounts = listOf( @@ -184,7 +184,7 @@ private fun ClickableCell( @Composable @Preview private fun OnRampAmountSelectionModal_Preview() { - val controller = InternalOnRampAmountController().apply { + val controller = InternalOnRampAmountController(StubFlipcashAnalytics()).apply { requestAmountSelection(OnRampProvider.Phantom) selectAmount(OnRampAmount.Predefined(amounts.first())) } diff --git a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/inject/OnRampAmountModule.kt b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/inject/OnRampAmountModule.kt index 7a653cb1a..7c625baf4 100644 --- a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/inject/OnRampAmountModule.kt +++ b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/inject/OnRampAmountModule.kt @@ -2,6 +2,7 @@ package com.flipcash.app.onramp.inject import com.flipcash.app.onramp.OnRampAmountController import com.flipcash.app.onramp.internal.InternalOnRampAmountController +import com.flipcash.services.analytics.FlipcashAnalyticsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,5 +15,7 @@ object OnRampAmountModule { @Provides @Singleton - fun providesOnRampAmountController(): OnRampAmountController = InternalOnRampAmountController() + fun providesOnRampAmountController( + analytics: FlipcashAnalyticsService + ): OnRampAmountController = InternalOnRampAmountController(analytics) } \ No newline at end of file diff --git a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/internal/InternalOnRampAmountController.kt b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/internal/InternalOnRampAmountController.kt index 0d5219b58..ebe508496 100644 --- a/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/internal/InternalOnRampAmountController.kt +++ b/apps/flipcash/shared/onramp/common/src/main/kotlin/com/flipcash/app/onramp/internal/InternalOnRampAmountController.kt @@ -4,6 +4,8 @@ import com.flipcash.app.onramp.ConfirmationEvent import com.flipcash.app.onramp.OnRampAmount import com.flipcash.app.onramp.OnRampAmountController import com.flipcash.app.onramp.State +import com.flipcash.services.analytics.AnalyticsEvent +import com.flipcash.services.analytics.FlipcashAnalyticsService import com.flipcash.services.internal.model.thirdparty.OnRampProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -18,7 +20,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class InternalOnRampAmountController : OnRampAmountController { +internal class InternalOnRampAmountController( + private val analytics: FlipcashAnalyticsService, +) : OnRampAmountController { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -43,6 +47,11 @@ internal class InternalOnRampAmountController : OnRampAmountController { if (amount.fiat == currentSelection.fiat) { it.copy(selectedAmount = null) } else { + analytics.onrampPurchase( + purchaseEvent = AnalyticsEvent.OnRampPurchaseEvent.PresetSelected, + fiat = amount.fiat + ) + it.copy(selectedAmount = amount) } } else { @@ -53,6 +62,10 @@ internal class InternalOnRampAmountController : OnRampAmountController { if (amount is OnRampAmount.Custom) { it.copy(selectedAmount = null) } else { + analytics.onrampPurchase( + purchaseEvent = AnalyticsEvent.OnRampPurchaseEvent.EnterCustomAmount, + ) + it.copy(selectedAmount = amount) } } diff --git a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt index 6f53448d3..178a44d7c 100644 --- a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt +++ b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampHandler.kt @@ -18,8 +18,11 @@ import com.flipcash.app.onramp.internal.ExternalWalletState import com.flipcash.app.onramp.internal.buildConnectDeeplink import com.flipcash.app.onramp.internal.buildTransactionDeeplink import com.flipcash.app.router.Router +import com.flipcash.services.analytics.FlipcashAnalyticsManager +import com.flipcash.services.analytics.FlipcashAnalyticsService import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.shared.onramp.deeplinks.R +import com.getcode.libs.analytics.LocalAnalytics import com.getcode.manager.BottomBarAction import com.getcode.manager.BottomBarManager import com.getcode.navigation.core.CodeNavigator @@ -46,6 +49,7 @@ fun ExternalWalletOnRampHandler( ) { val permissions = LocalPermissionChecker.current val composeScope = rememberCoroutineScope() + val analytics = LocalAnalytics.current as FlipcashAnalyticsService fun close(exit: Boolean) { if (exit) { @@ -91,6 +95,13 @@ fun ExternalWalletOnRampHandler( null -> "" } ) + + if (error is DeeplinkOnRampError.WalletProvidedError && error.code == DeeplinkError.UserRejectedRequest.code) { + analytics.walletTransactionCancelled(state.provider!!) + } else if (error is DeeplinkOnRampError.FailedToSendTransaction) { + analytics.walletTransactionFailed(state.provider!!) + } + trace( tag = TAG, message = "Something went wrong during deeplink onramp", @@ -149,6 +160,7 @@ fun ExternalWalletOnRampHandler( message = "wallet connect uri: $uri", type = TraceType.Process ) + analytics.connectWallet(state.provider!!) uriHandler.openUri(uri.toString()) state.deeplinkState = ExternalWalletState.CONNECTING } @@ -185,6 +197,7 @@ fun ExternalWalletOnRampHandler( message = "wallet transact uri: $uri", type = TraceType.Process ) + analytics.amountSelectedForWalletTransfer(state.provider!!, state.amount!!) uriHandler.openUri(uri.toString()) } @@ -211,6 +224,7 @@ fun ExternalWalletOnRampHandler( message = "transaction complete", type = TraceType.Process ) + analytics.transactionSubmittedToWallet(state.provider!!) state.reset() val hasPushPerms = permissions.isGranted(Manifest.permission.POST_NOTIFICATIONS) BottomBarManager.showMessage( diff --git a/libs/locale/impl/src/main/kotlin/com/getcode/util/locale/AndroidLocale.kt b/libs/locale/impl/src/main/kotlin/com/getcode/util/locale/AndroidLocale.kt index aaeedc36c..0820d7149 100644 --- a/libs/locale/impl/src/main/kotlin/com/getcode/util/locale/AndroidLocale.kt +++ b/libs/locale/impl/src/main/kotlin/com/getcode/util/locale/AndroidLocale.kt @@ -12,7 +12,7 @@ class AndroidLocale @Inject constructor( return LocaleUtils.getDefaultCurrency(context) } - override fun getDefaultCountry(): String { + override suspend fun getDefaultCountry(): String? { return LocaleUtils.getDefaultCountry(context) } } \ No newline at end of file diff --git a/libs/locale/public/build.gradle.kts b/libs/locale/public/build.gradle.kts index 8e94ea44c..62ba5df6b 100644 --- a/libs/locale/public/build.gradle.kts +++ b/libs/locale/public/build.gradle.kts @@ -32,6 +32,7 @@ android { dependencies { implementation(project(":libs:datetime")) implementation(project(":libs:currency")) + implementation(Libs.okhttp) api(Libs.androidx_annotation) api(Libs.kotlin_stdlib) api(Libs.kotlinx_coroutines_core) diff --git a/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleHelper.kt b/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleHelper.kt index 9fb6ec862..55d4993c9 100644 --- a/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleHelper.kt +++ b/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleHelper.kt @@ -2,5 +2,5 @@ package com.getcode.util.locale interface LocaleHelper { fun getDefaultCurrencyName(): String - fun getDefaultCountry(): String + suspend fun getDefaultCountry(): String? } \ No newline at end of file diff --git a/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleUtils.kt b/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleUtils.kt index ddac2f2d7..dd3a3c796 100644 --- a/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleUtils.kt +++ b/libs/locale/public/src/main/kotlin/com/getcode/util/locale/LocaleUtils.kt @@ -5,14 +5,44 @@ import android.telephony.TelephonyManager import com.getcode.model.CurrencyCode import com.getcode.model.CurrencyCode.Companion import com.getcode.model.RegionCode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject import java.util.* +import java.util.concurrent.TimeUnit object LocaleUtils { - fun getDefaultCountry(context: Context): String { + suspend fun getDefaultCountry(context: Context): String? { val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val networkCountryIso = tm.networkCountryIso.uppercase() val simCountryIso = tm.simCountryIso.uppercase() - return simCountryIso.ifBlank { networkCountryIso } + val localeCountry = context.resources.configuration.locale.country.uppercase() + + val simCountry = if (simCountryIso.isNotBlank() && simCountryIso.length == 2) simCountryIso else null + val localeCountryValid = if (localeCountry.isNotBlank() && localeCountry.length == 2) localeCountry else null + val networkCountry = if (networkCountryIso.isNotBlank() && networkCountryIso.length == 2) networkCountryIso else null + + return simCountry ?: localeCountryValid ?: networkCountry ?:getCountryFromIp() + } + + private suspend fun getCountryFromIp(): String? = withContext(Dispatchers.IO) { + try { + val client = OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .build() + val request = Request.Builder().url("http://ip-api.com/json/?fields=countryCode").build() + val response = client.newCall(request).execute() + if (response.isSuccessful) { + val json = JSONObject(response.body?.string() ?: "{}") + val ipCountry = json.optString("countryCode", "").uppercase() + if (ipCountry.length == 2) ipCountry else "US" + } else null + } catch (e: Exception) { + return@withContext null + } } fun getDefaultCurrency(context: Context): String { diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/analytics/Analytics.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/analytics/Analytics.kt index aeec8edbf..c717c696a 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/analytics/Analytics.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/analytics/Analytics.kt @@ -1,5 +1,6 @@ package com.flipcash.services.analytics +import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.libs.analytics.AnalyticsService import com.getcode.libs.analytics.AppAction @@ -9,8 +10,6 @@ import com.getcode.opencode.model.financial.CurrencyCode import com.getcode.opencode.model.financial.Fiat import com.getcode.opencode.model.financial.LocalFiat import com.getcode.services.flipcash.BuildConfig -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 import com.getcode.utils.TraceType import com.getcode.utils.base58 import com.getcode.utils.getPublicKeyBase58 @@ -46,6 +45,32 @@ interface FlipcashAnalyticsService : AnalyticsService { fun poolCreated(id: ID) fun placedBidInPool(id: ID) fun declaredOutcomeInPool(id: ID) + + fun openOnramp( + openEvent: AnalyticsEvent.OnRampOpenEvent, + ) + + fun onrampVerification( + verificationEvent: AnalyticsEvent.OnRampVerificationEvent, + ) + + fun onrampPurchase( + purchaseEvent: AnalyticsEvent.OnRampPurchaseEvent, + fiat: 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) + fun walletTransactionFailed(provider: OnRampProvider.UsesDeeplinks) + fun walletTransactionCancelled(provider: OnRampProvider.UsesDeeplinks) } class FlipcashAnalyticsManager @Inject constructor( @@ -122,6 +147,58 @@ class FlipcashAnalyticsManager @Inject constructor( track(event.name, *properties.toList().toTypedArray()) } + override fun openOnramp(openEvent: AnalyticsEvent.OnRampOpenEvent) { + val properties = openEvent.properties() + track(openEvent.name, *properties.toList().toTypedArray()) + } + + override fun onrampVerification(verificationEvent: AnalyticsEvent.OnRampVerificationEvent) { + val properties = verificationEvent.properties() + track(verificationEvent.name, *properties.toList().toTypedArray()) + } + + override fun onrampPurchase( + purchaseEvent: AnalyticsEvent.OnRampPurchaseEvent, + fiat: Fiat?, + successful: Boolean, + error: Throwable? + ) { + val properties = purchaseEvent.properties(nativeAmount = fiat, successful = successful, error = error) + track(purchaseEvent.name, *properties.toList().toTypedArray()) + } + + override fun connectWallet(provider: OnRampProvider.UsesDeeplinks) { + val event = AnalyticsEvent.WalletConnect(provider) + val properties = event.properties() + track(event.name, *properties.toList().toTypedArray()) + } + + override fun amountSelectedForWalletTransfer(provider: OnRampProvider.UsesDeeplinks, amount: Fiat) { + val event = AnalyticsEvent.WalletRequestAmount(provider) + val properties = event.properties( + nativeAmount = amount + ) + track(event.name, *properties.toList().toTypedArray()) + } + + override fun transactionSubmittedToWallet(provider: OnRampProvider.UsesDeeplinks) { + val event = AnalyticsEvent.WalletSubmitTransaction(provider) + val properties = event.properties() + track(event.name, *properties.toList().toTypedArray()) + } + + override fun walletTransactionFailed(provider: OnRampProvider.UsesDeeplinks) { + val event = AnalyticsEvent.WalletTransactionFailed(provider) + val properties = event.properties() + track(event.name, *properties.toList().toTypedArray()) + } + + override fun walletTransactionCancelled(provider: OnRampProvider.UsesDeeplinks) { + val event = AnalyticsEvent.WalletTransactionCancelled(provider) + val properties = event.properties() + track(event.name, *properties.toList().toTypedArray()) + } + private fun track(name: String, vararg properties: Pair) { if (BuildConfig.DEBUG) { trace( @@ -139,6 +216,28 @@ class FlipcashAnalyticsManager @Inject constructor( } } +class StubFlipcashAnalytics: FlipcashAnalyticsService { + override fun onAppStart() = Unit + override fun onAppStarted() = Unit + override fun unintentionalLogout() = Unit + override fun action(action: AppAction, source: AppActionSource?) = Unit + override fun transfer(event: AnalyticsEvent.Transfer, amount: LocalFiat?, successful: Boolean, error: Throwable?) = Unit + override fun transfer(event: AnalyticsEvent.Transfer, fiat: Fiat?, successful: Boolean, error: Throwable?) = Unit + override fun paidForAccount(price: Double, currency: CurrencyCode, owner: KeyPair) = Unit + override fun poolOpenedFromDeeplink(id: ID) = Unit + override fun poolCreated(id: ID) = Unit + override fun placedBidInPool(id: ID) = Unit + override fun declaredOutcomeInPool(id: ID) = Unit + override fun openOnramp(openEvent: AnalyticsEvent.OnRampOpenEvent) = Unit + override fun onrampVerification(verificationEvent: AnalyticsEvent.OnRampVerificationEvent) = Unit + override fun onrampPurchase(purchaseEvent: AnalyticsEvent.OnRampPurchaseEvent, fiat: 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 + override fun walletTransactionFailed(provider: OnRampProvider.UsesDeeplinks) = Unit + override fun walletTransactionCancelled(provider: OnRampProvider.UsesDeeplinks) = Unit +} + sealed class Action : AppAction { data object CreateAccount : Action() { override val value: String = "Button: Create Account" @@ -219,6 +318,88 @@ sealed interface AnalyticsEvent { data class DeclaredOutcome(override val id: ID) : PoolEvent { override val name: String = "Pool: Declared Outcome" } + + sealed interface OnRampOpenEvent: AnalyticsEvent { + data object Settings : OnRampOpenEvent { + override val name: String = "Onramp: Opened From Settings" + } + + data object Balance: OnRampOpenEvent { + override val name: String = "Onramp: Opened From Balance" + } + + data object Give: OnRampOpenEvent { + override val name: String = "Onramp: Opened From Give" + } + } + + sealed interface OnRampVerificationEvent: AnalyticsEvent { + data object ShowInfo : OnRampVerificationEvent { + override val name: String = "Onramp: Show Verification Info" + } + + data object EnterPhone: OnRampVerificationEvent { + override val name: String = "Onramp: Show Enter Phone" + } + + data object ConfirmPhone: OnRampVerificationEvent { + override val name: String = "Onramp: Show Confirm Phone" + } + + data object EnterEmail: OnRampVerificationEvent { + override val name: String = "Onramp: Show Enter Email" + } + + data object ConfirmEmail: OnRampVerificationEvent { + override val name: String = "Onramp: Show Confirm Email" + } + } + + sealed interface OnRampPurchaseEvent: AnalyticsEvent { + data object PresetSelected: OnRampPurchaseEvent { + override val name: String = "Onramp: Amount Selected" + } + + data object EnterCustomAmount: OnRampPurchaseEvent { + override val name: String = "Onramp: Enter Custom Amount" + } + + data object InvokePayment: OnRampPurchaseEvent { + override val name: String = "Onramp: Invoke Payment" + } + + data object InvokePaymentCustom: OnRampPurchaseEvent { + override val name: String = "Onramp: Invoke Payment Custom" + } + + data object Completed: OnRampPurchaseEvent { + override val name: String = "Onramp: Completed" + } + } + + sealed interface WalletEvent : AnalyticsEvent { + val provider: OnRampProvider.UsesDeeplinks + } + + data class WalletConnect(override val provider: OnRampProvider.UsesDeeplinks) : WalletEvent { + override val name: String = "Wallet: Connect" + } + + data class WalletRequestAmount(override val provider: OnRampProvider.UsesDeeplinks) : WalletEvent { + override val name: String = "Wallet: Request Amount" + } + + data class WalletSubmitTransaction(override val provider: OnRampProvider.UsesDeeplinks) : WalletEvent { + override val name: String = "Wallet: Transactions Submitted" + } + + data class WalletTransactionFailed(override val provider: OnRampProvider.UsesDeeplinks) : WalletEvent { + override val name: String = "Wallet: Transactions Failed" + } + + data class WalletTransactionCancelled(override val provider: OnRampProvider.UsesDeeplinks) : WalletEvent { + override val name: String = "Wallet: Cancel" + } } private fun AnalyticsEvent.properties( @@ -236,6 +417,16 @@ private fun AnalyticsEvent.properties( } } + val providerName = if (this@properties is AnalyticsEvent.WalletEvent) { + when (provider) { + OnRampProvider.Backpack -> "Backpack" + OnRampProvider.Solflare -> "Solflare" + OnRampProvider.Phantom -> "Phantom" + } + } else { + "" + } + when (val event = this@properties) { is AnalyticsEvent.SentCashLink -> { if (event.clipboard == true) { @@ -259,6 +450,44 @@ private fun AnalyticsEvent.properties( is AnalyticsEvent.PoolEvent -> { put("ID", event.id.base58) } + + is AnalyticsEvent.WalletConnect -> { + put("Provider", providerName) + } + is AnalyticsEvent.WalletRequestAmount -> { + put("Provider", providerName) + put("Fiat", nativeAmount?.doubleValue.toString()) + put("Currency", nativeAmount?.currencyCode?.name.orEmpty()) + } + is AnalyticsEvent.WalletSubmitTransaction -> { + put("Provider", providerName) + } + is AnalyticsEvent.WalletTransactionCancelled -> { + put("Provider", providerName) + } + is AnalyticsEvent.WalletTransactionFailed -> { + put("Provider", providerName) + } + + is AnalyticsEvent.OnRampOpenEvent -> Unit + is AnalyticsEvent.OnRampVerificationEvent -> Unit + AnalyticsEvent.OnRampPurchaseEvent.Completed -> { + put("Fiat", nativeAmount?.doubleValue.toString()) + put("Currency", nativeAmount?.currencyCode?.name.orEmpty()) + } + AnalyticsEvent.OnRampPurchaseEvent.EnterCustomAmount -> Unit + AnalyticsEvent.OnRampPurchaseEvent.InvokePayment -> { + put("Fiat", nativeAmount?.doubleValue.toString()) + put("Currency", nativeAmount?.currencyCode?.name.orEmpty()) + } + AnalyticsEvent.OnRampPurchaseEvent.InvokePaymentCustom -> { + put("Fiat", nativeAmount?.doubleValue.toString()) + put("Currency", nativeAmount?.currencyCode?.name.orEmpty()) + } + AnalyticsEvent.OnRampPurchaseEvent.PresetSelected -> { + put("Fiat", nativeAmount?.doubleValue.toString()) + put("Currency", nativeAmount?.currencyCode?.name.orEmpty()) + } } if (localizedAmount != null) { diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/AccountController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/AccountController.kt index 2bf1fa3a6..23baf26f2 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/AccountController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/AccountController.kt @@ -5,6 +5,7 @@ import com.flipcash.services.repository.AccountRepository import com.flipcash.services.user.UserManager import com.getcode.opencode.model.core.ID import com.getcode.util.locale.LocaleHelper +import com.getcode.utils.trace import javax.inject.Inject class AccountController @Inject constructor( @@ -31,7 +32,9 @@ class AccountController @Inject constructor( val userId = userManager.accountId ?: return Result.failure(Throwable("No user ID in UserManager")) - val countryCode = localeHelper.getDefaultCountry() + val countryCode = localeHelper.getDefaultCountry().orEmpty() + + trace("user's device country code is $countryCode") return repository.getUserFlags( owner = owner, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt index a312ac525..32b9218be 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt @@ -30,12 +30,6 @@ class ProfileController @Inject constructor( type = TraceType.Process, ) userManager.set(it) - }.onFailure { - trace( - tag = "Profile", - message = "Failed to update user profile", - type = TraceType.Error - ) } }