diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 768624019f..b128e88b19 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -1,6 +1,10 @@ package com.tangem.feature.swap.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.ui.FeeType + +private const val SWAP_CATEGORY = "Swap" sealed class SwapEvents( event: String, @@ -13,14 +17,17 @@ sealed class SwapEvents( ) object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened") - data class SearchTokenClicked(val currencySymbol: String?) : SwapEvents( - event = "Searched Token Clicked", + data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( + event = "Choose Token Screen Opened", + params = mapOf("Available tokens" to if (availableTokens) "Yes" else "No"), + ) + + data class ChooseTokenScreenResult(val tokenChosen: Boolean, val token: String? = null) : SwapEvents( + event = "Choose Token Screen Result", params = buildMap { - if (currencySymbol != null) { - put("Token", currencySymbol) - } + put("Token Chosen", if (tokenChosen) "Yes" else "No") + token?.let { put("Token", it) } }, ) @@ -37,8 +44,61 @@ sealed class SwapEvents( ) object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - object SwapInProgressScreen : SwapEvents(event = "Swap in Progress Screen Opened") -} -private const val SWAP_CATEGORY = "Swap" \ No newline at end of file + object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + + data class SwapInProgressScreen( + val provider: SwapProvider, + val commission: FeeType, // Market / Fast + val sendToken: String, + val receiveToken: String, + ) : SwapEvents( + event = "Swap in Progress Screen Opened", + params = mapOf( + "Provider" to provider.name, + "Commission" to if (commission == FeeType.NORMAL) "Market" else "Fast", + "Send Token" to sendToken, + "Receive Token" to receiveToken, + ), + ) + + object ProviderClicked : SwapEvents("Provider Clicked") + + data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + event = "Provider Chosen", + params = mapOf("Provider" to provider.name), + ) + + data class ButtonStatus(val token: String) : SwapEvents( + event = "Button - Status", + params = mapOf("Token" to token), + ) + + data class ButtonExplore(val token: String) : SwapEvents( + event = "Button - Explore", + params = mapOf("Token" to token), + ) + + object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") + + data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + event = "Notice - Not Enough Fee", + params = mapOf( + "Token" to token, + "Blockchain" to blockchain, + ), + ) + + data class NoticeProviderError( + val token: String, + val provider: SwapProvider, + val errorCode: Int, + ) : SwapEvents( + event = "Notice - Express Error", + params = mapOf( + "Token" to token, + "Provider" to provider.name, + "Error code" to errorCode.toString(), + ), + ) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt deleted file mode 100644 index 617534b273..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt +++ /dev/null @@ -1,297 +0,0 @@ -package com.tangem.feature.swap.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import com.tangem.core.ui.components.* - -// @Composable -// fun SwapPermissionBottomSheetContent(data: SwapPermissionState.ReadyForRequest, onCancel: () -> Unit) { -// var isPermissionAlertShow by remember { mutableStateOf(false) } -// Column( -// modifier = Modifier -// .background(color = TangemTheme.colors.background.primary) -// .fillMaxWidth() -// .padding(horizontal = TangemTheme.dimens.spacing16), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// Hand() -// -// SpacerH10() -// -// Box(modifier = Modifier.fillMaxWidth()) { -// Text( -// modifier = Modifier.align(Alignment.Center), -// text = stringResource(id = R.string.swapping_permission_header), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// ) -// IconButton( -// modifier = Modifier.align(Alignment.CenterEnd), -// onClick = { isPermissionAlertShow = true }, -// ) { -// Icon( -// painter = painterResource(id = R.drawable.ic_question_24), -// contentDescription = null, -// ) -// } -// } -// -// SpacerH10() -// -// Text( -// text = stringResource( -// id = R.string.swapping_permission_subheader, -// data.currency, -// ), -// color = TangemTheme.colors.text.secondary, -// style = TangemTheme.typography.body2, -// textAlign = TextAlign.Center, -// modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), -// ) -// -// SpacerH16() -// -// ApprovalBottomSheetInfo(data) -// -// SpacerH28() -// -// PrimaryButtonIconEnd( -// text = stringResource(id = R.string.swapping_permission_buttons_approve), -// iconResId = R.drawable.ic_tangem_24, -// modifier = Modifier.fillMaxWidth(), -// onClick = data.approveButton.onClick, -// ) -// -// SpacerH12() -// -// SecondaryButton( -// text = stringResource(id = R.string.common_cancel), -// modifier = Modifier.fillMaxWidth(), -// onClick = { -// onCancel() -// }, -// ) -// -// SpacerH16() -// -// // region dialog -// if (isPermissionAlertShow) { -// BasicDialog( -// message = stringResource(id = R.string.swapping_approve_information_text), -// title = stringResource(id = R.string.swapping_approve_information_title), -// confirmButton = DialogButton { isPermissionAlertShow = false }, -// onDismissDialog = {}, -// ) -// } -// } -// } - -// @Composable -// private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { -// Column( -// modifier = Modifier -// .background(color = TangemTheme.colors.background.primary) -// .fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// AmountItem( -// currency = data.currency, -// approveType = data.approveType, -// onChangeApproveType = data.onChangeApproveType, -// approveItems = data.approveItems, -// ) -// SubtitleItem( -// subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), -// modifier = Modifier.fillMaxWidth(), -// ) -// SpacerH24() -// DividerBottomSheet() -// FeeItem(fee = data.fee.resolveReference()) -// SubtitleItem( -// subtitle = stringResource(id = R.string.swapping_permission_fee_footer), -// modifier = Modifier.fillMaxWidth(), -// ) -// } -// } -// -// @Composable -// private fun DividerBottomSheet() { -// Divider( -// color = TangemTheme.colors.stroke.primary, -// thickness = TangemTheme.dimens.size0_5, -// ) -// } -// -// @Composable -// private fun InformationItem(subtitle: String, value: String) { -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = TangemTheme.dimens.spacing16), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// text = subtitle, -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// maxLines = 1, -// ) -// -// MiddleEllipsisText( -// text = value, -// color = TangemTheme.colors.text.tertiary, -// style = TangemTheme.typography.body2, -// modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), -// ) -// } -// } -// -// @Composable -// private fun AmountItem( -// currency: String, -// approveType: ApproveType, -// approveItems: ImmutableList, -// onChangeApproveType: (ApproveType) -> Unit, -// ) { -// var isExpandSelector by remember { -// mutableStateOf(false) -// } -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = TangemTheme.dimens.spacing16), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// text = stringResource(id = R.string.swapping_permission_rows_amount, currency), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// maxLines = 1, -// ) -// Box { -// SelectorItem( -// getTitleForApproveType(approveType = approveType), -// ) { -// isExpandSelector = true -// } -// DropdownSelector( -// isExpanded = isExpandSelector, -// onDismiss = { isExpandSelector = false }, -// onItemClick = { approveType -> -// isExpandSelector = false -// onChangeApproveType.invoke(approveType) -// }, -// items = approveItems, -// ) -// } -// } -// } -// -// @Composable -// private fun SelectorItem(title: String, onClick: () -> Unit) { -// Row( -// modifier = Modifier.clickable { onClick() }, -// ) { -// Text( -// text = title, -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.body1, -// maxLines = 1, -// ) -// Icon( -// painter = painterResource(id = R.drawable.ic_chevron_24), -// tint = TangemTheme.colors.icon.primary1, -// contentDescription = null, -// ) -// } -// } -// -// @Composable -// private fun DropdownSelector( -// isExpanded: Boolean, -// onDismiss: () -> Unit, -// onItemClick: (ApproveType) -> Unit, -// items: ImmutableList, -// ) { -// DropdownMenu( -// expanded = isExpanded, -// onDismissRequest = onDismiss, -// modifier = Modifier -// .wrapContentSize() -// .background(TangemTheme.colors.background.secondary), -// ) { -// items.forEach { item -> -// DropdownMenuItem( -// onClick = { -// onItemClick.invoke(item) -// }, -// ) { -// Text( -// text = getTitleForApproveType(approveType = item), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.body1, -// maxLines = 1, -// ) -// } -// } -// } -// } -// -// @Composable -// private fun FeeItem(fee: String) { -// InformationItem( -// subtitle = stringResource(id = R.string.send_fee_label), -// value = fee, -// ) -// } -// -// @Composable -// private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { -// Text( -// modifier = modifier, -// text = subtitle, -// color = TangemTheme.colors.text.secondary, -// style = TangemTheme.typography.body2, -// ) -// } -// -// @Composable -// private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { -// ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) -// ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) -// } -// -// // region preview -// -// @Preview -// @Composable -// private fun Preview_AgreementBottomSheet_InLightTheme() { -// TangemTheme(isDark = false) { -// SwapPermissionBottomSheetContent(data = previewData) {} -// } -// } -// -// @Preview -// @Composable -// private fun Preview_AgreementBottomSheet_InDarkTheme() { -// TangemTheme(isDark = true) { -// SwapPermissionBottomSheetContent(data = previewData) {} -// } -// } -// -// private val previewData = SwapPermissionState.ReadyForRequest( -// currency = "DAI", -// amount = "∞", -// walletAddress = "", -// spenderAddress = "", -// fee = TextReference.Str("2,14$"), -// approveType = ApproveType.UNLIMITED, -// approveButton = ApprovePermissionButton(true) {}, -// cancelButton = CancelPermissionButton(true), -// onChangeApproveType = { ApproveType.UNLIMITED }, -// ) - -//endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index e1e6a80d73..82b05a7c86 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -33,6 +34,8 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) { + BackHandler(onBack = onBack) + Scaffold( modifier = Modifier .systemBarsPadding() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 631bdb18a8..62425f7111 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -18,15 +18,13 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor +import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.ApproveType -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.toDomainApproveType +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -140,7 +138,7 @@ internal class SwapViewModel @Inject constructor( uiState = uiState.copy( onSelectTokenClick = { router.openScreen(SwapNavScreen.SelectToken) - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened) + sendSelectTokenScreenOpenedEvent() }, onSuccess = { router.openScreen(SwapNavScreen.Success) @@ -148,9 +146,14 @@ internal class SwapViewModel @Inject constructor( ) } - @Suppress("UnusedPrivateMember") + private fun sendSelectTokenScreenOpenedEvent() { + val isAnyAvailableTokensTo = dataState.tokensDataState?.toGroup?.available?.isNotEmpty() ?: false + val isAnyAvailableTokensFrom = dataState.tokensDataState?.fromGroup?.available?.isNotEmpty() ?: false + val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens)) + } + private fun initTokens() { - // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.getTokensDataState(initialCryptoCurrency) @@ -190,6 +193,7 @@ internal class SwapViewModel @Inject constructor( tokensDataState = state, ) if (selectedCurrency == null) { + analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap) uiState = stateBuilder.createNoAvailableTokensToSwapState( uiStateHolder = uiState, fromToken = fromCurrencyStatus, @@ -317,6 +321,14 @@ internal class SwapViewModel @Inject constructor( isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) + if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { + analyticsEventHandler.send( + SwapEvents.NoticeNotEnoughFee( + token = initialCryptoCurrency.symbol, + blockchain = fromToken.currency.network.name, + ), + ) + } } is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( @@ -332,10 +344,21 @@ internal class SwapViewModel @Inject constructor( toToken = dataState.toCryptoCurrency, dataError = state.error, ) + sendErrorAnalyticsEvent(state.error, provider) } } } + private fun sendErrorAnalyticsEvent(error: DataError, provider: SwapProvider) { + analyticsEventHandler.send( + SwapEvents.NoticeProviderError( + token = initialCryptoCurrency.symbol, + provider = provider, + errorCode = error.code, + ), + ) + } + private fun updateLoadedQuotes(state: Map): Pair { val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { @@ -438,15 +461,22 @@ internal class SwapViewModel @Inject constructor( if (txHash.isNotEmpty()) { swapRouter.openUrl(url) } + analyticsEventHandler.send( + event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + ) }, onStatusClick = { val txExternalUrl = it.txExternalUrl if (!txExternalUrl.isNullOrBlank()) { swapRouter.openUrl(txExternalUrl) + analyticsEventHandler.send( + event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), + ) } }, ) - analyticsEventHandler.send(SwapEvents.SwapInProgressScreen) + sendSuccessEvent() + swapRouter.openScreen(SwapNavScreen.Success) } is TxState.UserCancelled -> { @@ -468,6 +498,22 @@ internal class SwapViewModel @Inject constructor( } } + private fun sendSuccessEvent() { + val provider = dataState.selectedProvider ?: return + val fee = dataState.selectedFee?.feeType ?: return + val sendToken = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val toToken = dataState.toCryptoCurrency?.currency?.symbol ?: return + + analyticsEventHandler.send( + SwapEvents.SwapInProgressScreen( + provider = provider, + commission = fee, + sendToken = sendToken, + receiveToken = toToken, + ), + ) + } + private fun givePermissionsToSwap() { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -563,9 +609,9 @@ internal class SwapViewModel @Inject constructor( it.currencyStatus.currency.id.value == id } } - analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), - ) + foundToken?.currencyStatus?.currency?.symbol?.let { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) + } if (foundToken != null) { val fromToken: CryptoCurrencyStatus @@ -702,6 +748,9 @@ internal class SwapViewModel @Inject constructor( if (bottomSheet != null && bottomSheet.isShow) { uiState = stateBuilder.dismissBottomSheet(uiState) } else { + if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = false)) + } swapRouter.back() } onSearchEntered("") @@ -752,6 +801,7 @@ internal class SwapViewModel @Inject constructor( } }, onProviderClick = { providerId -> + analyticsEventHandler.send(SwapEvents.ProviderClicked) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(states) val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) @@ -768,6 +818,7 @@ internal class SwapViewModel @Inject constructor( val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { + analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.updateSelectedProvider(uiState, provider.providerId) setupLoadedState( provider = provider,