diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index b45c36ecb9..be0c324b89 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -148,6 +148,10 @@ class TransactionManagerImpl( } } + override fun getBlockchainId(networkId: String): String { + return requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }.id + } + private fun handleSendResult(result: SimpleResult): SendTxResult { when (result) { is SimpleResult.Success -> return SendTxResult.Success diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/InputFormatting.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/InputFormatting.kt new file mode 100644 index 0000000000..c1d9028cff --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/InputFormatting.kt @@ -0,0 +1,27 @@ +package com.tangem.core.ui.utils + +/** + * Formats input [String] for InputField, to remove wrong symbols, letters etc + * Use [decimals] for cut this number symbols after floating point + * + * Example (with 8 decimals): + * input string - ab123.46377372ab53 + * result string 123.46377372 + */ +fun getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String { + val filteredChars = text.filterIndexed { index, c -> + c.isDigit() + || (c == '.' && index != 0 && text.indexOf('.') == index) + || (c == '.' && index != 0 && text.count { it == '.' } <= 1) + } + // If dot is present, take first 3 digits before decimal and first decimals digits after decimal + return if (filteredChars.count { it == '.' } == 1) { + val beforeDecimal = filteredChars.substringBefore('.') + val afterDecimal = filteredChars.substringAfter('.') + beforeDecimal + "." + afterDecimal.take(decimals) + } + // If there is no dot, just take all digits + else { + filteredChars + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt new file mode 100644 index 0000000000..a05884f937 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.swap.domain + +import com.tangem.feature.swap.domain.models.domain.Currency + +interface BlockchainInteractor { + + fun getTokenDecimals(token: Currency): Int + + /** + * In app blockchain id, actual in blockchain sdk, not the same as networkId + * + * workaround till not use backend only and not integrated server vs sdk + */ + fun getBlockchainId(networkId: String): String +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt new file mode 100644 index 0000000000..ebad234c3c --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.swap.domain + +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.lib.crypto.TransactionManager +import javax.inject.Inject + +internal class BlockchainInteractorImpl @Inject constructor( + private val transactionManager: TransactionManager, +) : BlockchainInteractor { + + override fun getBlockchainId(networkId: String): String { + return transactionManager.getBlockchainId(networkId) + } + + override fun getTokenDecimals(token: Currency): Int { + return if (token is Currency.NonNativeToken) { + token.decimalCount + } else { + transactionManager.getNativeTokenDecimals(token.networkId) + } + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 40eb7cde50..547a2784fb 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -91,8 +91,6 @@ interface SwapInteractor { amountToSwap: String, ): TxState - fun getTokenDecimals(token: Currency): Int - /** * Returns token in wallet balance * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index e9a2bb67c2..863590f481 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -215,7 +215,7 @@ internal class SwapInteractorImpl @Inject constructor( userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet)) TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol), - toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToSend.symbol), + toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol), ) } SendTxResult.UserCancelledError -> TxState.UserCancelled @@ -225,7 +225,13 @@ internal class SwapInteractorImpl @Inject constructor( } } - override fun getTokenDecimals(token: Currency): Int { + override fun getTokenBalance(token: Currency): SwapAmount { + return userWalletManager.getCurrentWalletTokensBalance(token.networkId)[token.symbol]?.let { + SwapAmount(it.value, it.decimals) + } ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) + } + + fun getTokenDecimals(token: Currency): Int { return if (token is Currency.NonNativeToken) { token.decimalCount } else { @@ -233,12 +239,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - override fun getTokenBalance(token: Currency): SwapAmount { - return userWalletManager.getCurrentWalletTokensBalance(token.networkId)[token.symbol]?.let { - SwapAmount(it.value, it.decimals) - } ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) - } - private fun selectToToken( initialToken: Currency, tokensInWallet: List, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index c5e2782f7e..7f65454b85 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,6 +1,8 @@ package com.tangem.feature.swap.domain.di import com.tangem.feature.swap.domain.AllowPermissionsHandlerImpl +import com.tangem.feature.swap.domain.BlockchainInteractor +import com.tangem.feature.swap.domain.BlockchainInteractorImpl import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractorImpl import com.tangem.feature.swap.domain.SwapRepository @@ -32,4 +34,14 @@ class SwapDomainModule { allowPermissionsHandler = AllowPermissionsHandlerImpl(), ) } + + @Provides + @Singleton + fun provideBlockchainInteractor( + transactionManager: TransactionManager, + ): BlockchainInteractor { + return BlockchainInteractorImpl( + transactionManager = transactionManager, + ) + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt index fec99447b7..40cf980a73 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt @@ -29,4 +29,8 @@ sealed class Currency { val contractAddress: String, val decimalCount: Int, ) : Currency() +} + +fun Currency.isNonNative(): Boolean { + return this is Currency.NonNativeToken } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 969e973160..aeeaf50f3b 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -49,6 +49,7 @@ android { dependencies { /** Core modules */ + implementation(project(":core:analytics")) implementation(project(":core:utils")) implementation(project(":core:ui")) 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 new file mode 100644 index 0000000000..11dce2daa7 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.swap.analytics + +import com.tangem.core.analytics.AnalyticsEvent + +sealed class SwapEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(SWAP_CATEGORY, event, params) { + + data class SwapScreenOpened(val token: String) : SwapEvents( + event = "Swap Screen Opened", + params = mapOf("Token" to token), + ) + + object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") + object ReceiveTokenClicked : SwapEvents(event = "Receive Token Clicked") + object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened") + object SearchTokenClicked : SwapEvents(event = "Search Token Clicked") + data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + event = "Button - Swap", + params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), + ) + + object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") // ? + object ButtonPermissionApproveClicked : SwapEvents(event = "Button - Permission Approve") + object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") + object ButtonPermitAndSwapClicked : SwapEvents(event = "Button - Permit and Swap") // ? + 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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a6ac559be4..b2345dbff9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,12 +1,11 @@ package com.tangem.feature.swap.models -import androidx.annotation.DrawableRes - data class SwapStateHolder( val sendCardData: SwapCardData, val receiveCardData: SwapCardData, val networkCurrency: String, val networkId: String, + val blockchainId: String, // not the same as networkId, its local id in app val fee: FeeState = FeeState.Empty, val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, @@ -24,6 +23,8 @@ data class SwapStateHolder( val onSelectTokenClick: (() -> Unit)? = null, val onSuccess: (() -> Unit)? = null, val onMaxAmountSelected: (() -> Unit)? = null, + val onShowPermissionBottomSheet: () -> Unit = {}, + val onCancelPermissionBottomSheet: () -> Unit = {}, ) data class SwapCardData( @@ -34,7 +35,7 @@ data class SwapCardData( val tokenIconUrl: String, val tokenCurrency: String, val balance: String, - @DrawableRes val networkIconRes: Int? = null, + val isNotNativeToken: Boolean, val canSelectAnotherToken: Boolean = false, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index fdc4f6ff17..d5fe7389f1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -9,4 +9,6 @@ data class UiActions( val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, + val openPermissionBottomSheet: () -> Unit, + val hidePermissionBottomSheet: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 1d673deed0..4cdec15b75 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -34,6 +34,7 @@ class SwapFragment : Fragment() { customTabsManager = CustomTabsManager(WeakReference(activity?.application)), ), ) + viewModel.onScreenOpened() activity?.onBackPressedDispatcher?.addCallback( viewLifecycleOwner, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a3dc6ade38..51edee06d5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.isNonNative import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.FoundTokensState import com.tangem.feature.swap.domain.models.ui.PermissionDataState @@ -27,9 +28,10 @@ class StateBuilder(val actions: UiActions) { private val tokensDataConverter = TokensDataConverter(actions.onSearchEntered, actions.onTokenSelected) - fun createInitialLoadingState(initialCurrency: Currency): SwapStateHolder { + fun createInitialLoadingState(initialCurrency: Currency, blockchainId: String): SwapStateHolder { return SwapStateHolder( networkId = initialCurrency.networkId, + blockchainId = blockchainId, sendCardData = SwapCardData( type = TransactionCardType.SendCard(actions.onAmountChanged), amount = null, @@ -38,6 +40,7 @@ class StateBuilder(val actions: UiActions) { tokenCurrency = initialCurrency.symbol, coinId = initialCurrency.id, canSelectAnotherToken = false, + isNotNativeToken = initialCurrency.isNonNative(), balance = "", ), receiveCardData = SwapCardData( @@ -48,6 +51,7 @@ class StateBuilder(val actions: UiActions) { tokenCurrency = "", canSelectAnotherToken = false, balance = "", + isNotNativeToken = false, coinId = null, ), fee = FeeState.Loading, @@ -58,6 +62,8 @@ class StateBuilder(val actions: UiActions) { onChangeCardsClicked = actions.onChangeCardsClicked, onMaxAmountSelected = actions.onMaxAmountSelected, updateInProgress = true, + onShowPermissionBottomSheet = actions.openPermissionBottomSheet, + onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, ) } @@ -75,6 +81,7 @@ class StateBuilder(val actions: UiActions) { tokenIconUrl = fromToken.logoUrl, tokenCurrency = fromToken.symbol, coinId = fromToken.id, + isNotNativeToken = fromToken.isNonNative(), canSelectAnotherToken = mainTokenId != fromToken.id, balance = "", ), @@ -85,6 +92,7 @@ class StateBuilder(val actions: UiActions) { tokenIconUrl = toToken.logoUrl, tokenCurrency = toToken.symbol, coinId = toToken.id, + isNotNativeToken = toToken.isNonNative(), canSelectAnotherToken = mainTokenId != toToken.id, balance = "", ), @@ -119,6 +127,7 @@ class StateBuilder(val actions: UiActions) { amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = quoteModel.fromTokenInfo.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = quoteModel.fromTokenInfo.tokenWalletBalance, @@ -129,6 +138,7 @@ class StateBuilder(val actions: UiActions) { amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = quoteModel.toTokenInfo.coinId, + isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = quoteModel.toTokenInfo.tokenWalletBalance, @@ -159,6 +169,7 @@ class StateBuilder(val actions: UiActions) { amountEquivalent = "", tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = uiStateHolder.sendCardData.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = emptyAmountState.fromTokenWalletBalance, @@ -169,6 +180,7 @@ class StateBuilder(val actions: UiActions) { amountEquivalent = "", tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = uiStateHolder.receiveCardData.coinId, + isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = emptyAmountState.toTokenWalletBalance, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 2f7078e144..b907deef8d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -31,7 +31,12 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { if (stateHolder.permissionState is SwapPermissionState.ReadyForRequest) { SwapPermissionBottomSheetContent( data = stateHolder.permissionState, - onCancel = { coroutineScope.launch { bottomSheetState.hide() } }, + onCancel = { + coroutineScope.launch { + stateHolder.onCancelPermissionBottomSheet.invoke() + bottomSheetState.hide() + } + }, ) } else { // Required "else" block to prevent compose crash @@ -55,7 +60,12 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { val isBottomSheetReady = !bottomSheetState.isVisible && stateHolder.permissionState is SwapPermissionState.ReadyForRequest coroutineScope.launch { - if (isBottomSheetReady) bottomSheetState.show() else bottomSheetState.hide() + if (isBottomSheetReady) { + bottomSheetState.show() + stateHolder.onShowPermissionBottomSheet.invoke() + } else { + bottomSheetState.hide() + } } }, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 4fda317c65..163635c16d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -21,6 +21,7 @@ import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.SmallInfoCardWithWarning import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.FeeState @@ -161,6 +163,9 @@ private fun MainInfo(state: SwapStateHolder) { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { + val networkIconRes = remember { + getActiveIconRes(state.blockchainId) + } Column { TransactionCard( type = state.sendCardData.type, @@ -169,7 +174,7 @@ private fun MainInfo(state: SwapStateHolder) { amountEquivalent = state.sendCardData.amountEquivalent, tokenIconUrl = state.sendCardData.tokenIconUrl, tokenCurrency = state.sendCardData.tokenCurrency, - networkIconRes = state.sendCardData.networkIconRes, + networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null, iconPlaceholder = state.sendCardData.coinId?.let { getActiveIconResByCoinId(it, state.networkId) }, @@ -183,7 +188,7 @@ private fun MainInfo(state: SwapStateHolder) { amountEquivalent = state.receiveCardData.amountEquivalent, tokenIconUrl = state.receiveCardData.tokenIconUrl, tokenCurrency = state.receiveCardData.tokenCurrency, - networkIconRes = state.receiveCardData.networkIconRes, + networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null, iconPlaceholder = state.receiveCardData.coinId?.let { getActiveIconResByCoinId(it, state.networkId) }, @@ -314,7 +319,7 @@ private val sendCard = SwapCardData( amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", - networkIconRes = R.drawable.img_polygon_22, + isNotNativeToken = true, canSelectAnotherToken = false, balance = "123", coinId = "", @@ -326,7 +331,7 @@ private val receiveCard = SwapCardData( amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", - networkIconRes = R.drawable.img_polygon_22, + isNotNativeToken = true, canSelectAnotherToken = true, balance = "33333", coinId = "", @@ -342,6 +347,7 @@ private val state = SwapStateHolder( swapButton = SwapButton(enabled = true, loading = false, onClick = {}), onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, permissionState = SwapPermissionState.InProgress, + blockchainId = "POLYGON", // alert = SwapWarning.GenericWarning("There was an error. Please try again.") {}, ) 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 99a41ea8b1..e1f754edc4 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 @@ -7,6 +7,10 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.utils.getValidatedNumberWithFixedDecimals +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.domain.Currency import com.tangem.feature.swap.domain.models.domain.SwapDataModel @@ -24,7 +28,6 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching -import com.tangem.utils.toFormattedString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString @@ -35,7 +38,9 @@ import kotlin.properties.Delegates @HiltViewModel internal class SwapViewModel @Inject constructor( private val swapInteractor: SwapInteractor, + private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -51,7 +56,12 @@ internal class SwapViewModel @Inject constructor( private val singleTaskScheduler = SingleTaskScheduler() private var dataState by mutableStateOf(SwapProcessDataState(networkId = currency.networkId)) - var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState(currency)) + var uiState: SwapStateHolder by mutableStateOf( + stateBuilder.createInitialLoadingState( + initialCurrency = currency, + blockchainId = blockchainInteractor.getBlockchainId(currency.networkId), + ), + ) private set // shows currency order (direct - swap initial to selected, reversed = selected to initial) @@ -70,12 +80,21 @@ internal class SwapViewModel @Inject constructor( super.onCleared() } + fun onScreenOpened() { + analyticsEventHandler.send(SwapEvents.SwapScreenOpened(currency.symbol)) + } + fun setRouter(router: SwapRouter) { swapRouter = router uiState = uiState.copy( onBackClicked = router::back, - onSelectTokenClick = { router.openScreen(SwapScreen.SelectToken) }, - onSuccess = { router.openScreen(SwapScreen.Success) }, + onSelectTokenClick = { + router.openScreen(SwapScreen.SelectToken) + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened) + }, + onSuccess = { + router.openScreen(SwapScreen.Success) + }, ) } @@ -190,16 +209,17 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.onSwap( networkId = dataState.networkId, - swapData = dataState.swapModel!!, - currencyToSend = dataState.fromCurrency!!, - currencyToGet = dataState.toCurrency!!, - amountToSwap = dataState.amount!!, + swapData = requireNotNull(dataState.swapModel), + currencyToSend = requireNotNull(dataState.fromCurrency), + currencyToGet = requireNotNull(dataState.toCurrency), + amountToSwap = requireNotNull(dataState.amount), ) } .onSuccess { when (it) { is TxState.TxSent -> { uiState = stateBuilder.createSuccessState(uiState, it) + analyticsEventHandler.send(SwapEvents.SwapInProgressScreen) swapRouter.openScreen(SwapScreen.Success) } else -> { @@ -209,7 +229,7 @@ internal class SwapViewModel @Inject constructor( } } } - .onFailure { } + .onFailure { makeDefaultAlert() } } } @@ -230,16 +250,12 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.loadingPermissionState(uiState) } else -> { - uiState = stateBuilder.addAlert(uiState) { - uiState = stateBuilder.clearAlert(uiState) - } + makeDefaultAlert() } } } .onFailure { - uiState = stateBuilder.addAlert(uiState) { - uiState = stateBuilder.clearAlert(uiState) - } + makeDefaultAlert() } } } @@ -286,7 +302,8 @@ internal class SwapViewModel @Inject constructor( toCurrency = newToToken, ) isOrderReversed = !isOrderReversed - lastAmount.value = cutAmountWithDecimals(swapInteractor.getTokenDecimals(newFromToken), lastAmount.value) + lastAmount.value = + cutAmountWithDecimals(blockchainInteractor.getTokenDecimals(newFromToken), lastAmount.value) uiState = stateBuilder.updateSwapAmount(uiState, lastAmount.value) startLoadingQuotes(newFromToken, newToToken, lastAmount.value) } @@ -296,7 +313,7 @@ internal class SwapViewModel @Inject constructor( val fromToken = dataState.fromCurrency val toToken = dataState.toCurrency if (fromToken != null && toToken != null) { - val cutValue = cutAmountWithDecimals(swapInteractor.getTokenDecimals(fromToken), value) + val cutValue = cutAmountWithDecimals(blockchainInteractor.getTokenDecimals(fromToken), value) uiState = stateBuilder.updateSwapAmount(uiState, cutValue) lastAmount.value = cutValue amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) { @@ -313,19 +330,52 @@ internal class SwapViewModel @Inject constructor( } private fun cutAmountWithDecimals(maxDecimals: Int, amount: String): String { - return amount.toBigDecimalOrNull()?.toFormattedString(maxDecimals) ?: INITIAL_AMOUNT + return getValidatedNumberWithFixedDecimals(amount, maxDecimals) + } + + private fun makeDefaultAlert() { + uiState = stateBuilder.addAlert(uiState) { + uiState = stateBuilder.clearAlert(uiState) + } } private fun createUiActions(): UiActions { return UiActions( onSearchEntered = { onSearchEntered(it) }, - onTokenSelected = { onTokenSelect(it) }, + onTokenSelected = { + onTokenSelect(it) + analyticsEventHandler.send(SwapEvents.SearchTokenClicked) + }, onAmountChanged = { onAmountChanged(it) }, - onSwapClick = { onSwapClick() }, - onGivePermissionClick = { givePermissionsToSwap() }, - onChangeCardsClicked = { onChangeCardsClicked() }, + onSwapClick = { + onSwapClick() + val sendTokenSymbol = dataState.fromCurrency?.symbol + val receiveTokenSymbol = dataState.toCurrency?.symbol + if (sendTokenSymbol != null && receiveTokenSymbol != null) { + analyticsEventHandler.send( + SwapEvents.ButtonSwapClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + ), + ) + } + }, + onGivePermissionClick = { + givePermissionsToSwap() + analyticsEventHandler.send(SwapEvents.ButtonPermissionApproveClicked) + }, + onChangeCardsClicked = { + onChangeCardsClicked() + analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked) + }, onBackClicked = { onSearchEntered("") }, onMaxAmountSelected = { onMaxAmountClicked() }, + openPermissionBottomSheet = { + analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + }, + hidePermissionBottomSheet = { + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + }, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 8c1a7e6dd1..98b1478b2b 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -44,4 +44,12 @@ interface TransactionManager { suspend fun updateWalletManager(networkId: String) fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal + + /** + * In app blockchain id, actual in blockchain sdk, not the same as networkId + * + * workaround till not use backend only and not integrated server vs sdk + */ + @Throws(IllegalStateException::class) + fun getBlockchainId(networkId: String): String } \ No newline at end of file